View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.rules;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.struts.action.ActionForm;
20  import org.kuali.rice.core.api.util.RiceConstants;
21  import org.kuali.rice.kns.rule.PromptBeforeValidation;
22  import org.kuali.rice.kns.rule.event.PromptBeforeValidationEvent;
23  import org.kuali.rice.kns.web.struts.form.KualiForm;
24  import org.kuali.rice.krad.document.Document;
25  import org.kuali.rice.kns.question.ConfirmationQuestion;
26  import org.kuali.rice.krad.util.KRADConstants;
27  
28  import javax.servlet.http.HttpServletRequest;
29  import java.util.Arrays;
30  import java.util.Iterator;
31  import java.util.NoSuchElementException;
32  
33  /**
34   * 
35   * This class simplifies requesting clarifying user input prior to applying business rules. It mostly shields the classes that
36   * extend it from being aware of the web layer, even though the input is collected via a series of one or more request/response
37   * cycles.
38   * 
39   * Beware: method calls with side-effects will have unusual results. While it looks like the doRules method is executed
40   * sequentially, in fact, it is more of a geometric series: if n questions are asked, then the code up to and including the first
41   * question is executed n times, the second n-1 times, ..., the last question only one time.
42   * 
43   * 
44   */
45  public abstract class PromptBeforeValidationBase implements PromptBeforeValidation {
46  
47      protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PromptBeforeValidationBase.class);
48  
49      protected String question;
50      protected String buttonClicked;
51      protected PromptBeforeValidationEvent event;
52      protected KualiForm form;
53  
54      private class IsAskingException extends RuntimeException {
55      }
56  
57      /**
58       * 
59       * This class acts similarly to HTTP session, but working inside a REQUEST parameter
60       * 
61       * 
62       */
63      /**
64       * This is a description of what this class does - wliang don't forget to fill this in. 
65       * 
66       * @author Kuali Rice Team (rice.collab@kuali.org)
67       *
68       */
69      public class ContextSession {
70          private final static String DELIMITER = ".";
71          PromptBeforeValidationEvent event;
72  
73          public ContextSession(String context, PromptBeforeValidationEvent event) {
74              this.event = event;
75  
76              this.event.setQuestionContext(context);
77              if (this.event.getQuestionContext() == null) {
78                  this.event.setQuestionContext("");
79              }
80  
81          }
82  
83          /**
84           * Whether a question with a given ID has already been asked
85           * 
86           * @param id the ID of the question, an arbitrary value, but must be consistent
87           * @return
88           */
89          public boolean hasAsked(String id) {
90              return StringUtils.contains(event.getQuestionContext(), id);
91          }
92  
93          /**
94           * Invoked to indicate that the user should be prompted a question
95           * 
96           * @param id the ID of the question, an arbitrary value, but must be consistent
97           * @param text the question text, to be displayed to the user
98           */
99          public void askQuestion(String id, String text) {
100             event.setQuestionId(id);
101             event.setQuestionType(KRADConstants.CONFIRMATION_QUESTION);
102             event.setQuestionText(text);
103             event.setPerformQuestion(true);
104         }
105 
106         public void setAttribute(String name, String value) {
107             if (LOG.isDebugEnabled()) {
108                 LOG.debug("setAttribute(" + name + "," + value + ")");
109             }
110             event.setQuestionContext(event.getQuestionContext() + DELIMITER + name + DELIMITER + value);
111 
112         }
113 
114         public String getAttribute(String name) {
115             if (LOG.isDebugEnabled()) {
116                 LOG.debug("getAttribute(" + name + ")");
117             }
118             String result = null;
119 
120             Iterator values = Arrays.asList(event.getQuestionContext().split("\\" + DELIMITER)).iterator();
121 
122             while (values.hasNext()) {
123                 if (values.next().equals(name)) {
124                     try {
125                         result = (String) values.next();
126                     }
127                     catch (NoSuchElementException e) {
128                         result = null;
129                     }
130                 }
131             }
132             if (LOG.isDebugEnabled()) {
133                 LOG.debug("returning " + result);
134             }
135             return result;
136         }
137 
138     }
139 
140     /**
141      * Implementations will override this method to do perform the actual prompting and/or logic
142      * 
143      * They are able to utilize the following methods:
144      * <li> {@link PromptBeforeValidationBase#abortRulesCheck()}
145      * <li> {@link PromptBeforeValidationBase#askOrAnalyzeYesNoQuestion(String, String)}
146      * <li> {@link #hasAsked(String)}
147      * 
148      * @param document
149      * @return
150      */
151     public abstract boolean doPrompts(Document document);
152 
153     private boolean isAborting;
154 
155     ContextSession session;
156 
157     public PromptBeforeValidationBase() {
158     }
159 
160 
161     public boolean processPrompts(ActionForm form, HttpServletRequest request, PromptBeforeValidationEvent event) {
162         question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
163         buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
164         this.event = event;
165         this.form = (KualiForm) form;
166 
167         if (LOG.isDebugEnabled()) {
168             LOG.debug("Question is: " + question);
169             LOG.debug("ButtonClicked: " + buttonClicked);
170             LOG.debug("QuestionContext() is: " + event.getQuestionContext());
171         }
172 
173         session = new ContextSession(request.getParameter(KRADConstants.QUESTION_CONTEXT), event);
174 
175         boolean result = false;
176 
177         try {
178             result = doPrompts(event.getDocument());
179         }
180         catch (IsAskingException e) {
181             return false;
182         }
183 
184         if (isAborting) {
185             return false;
186         }
187 
188         return result;
189     }
190 
191     /**
192      * This bounces the user back to the document as if they had never tried to routed it. (Business rules are not invoked
193      * and the action is not executed.)
194      * 
195      */
196     public void abortRulesCheck() {
197         event.setActionForwardName(RiceConstants.MAPPING_BASIC);
198         isAborting = true;
199     }
200 
201     /**
202      * This method poses a Y/N question to the user.  If the user has already answered the question, then it returns whether
203      * the answer to the question was yes or no
204      * 
205      * Code that invokes this method will behave a bit strangely, so you should try to keep it as simple as possible.
206      * 
207      * @param id an ID for the question
208      * @param text the text of the question, to be displayed on the screen
209      * @return true if the user answered Yes, false if the user answers no
210      * @throws IsAskingException if the user needs to be prompted the question
211      */
212     public boolean askOrAnalyzeYesNoQuestion(String id, String text) throws IsAskingException {
213 
214         if (LOG.isDebugEnabled()) {
215             LOG.debug("Entering askOrAnalyzeYesNoQuestion(" + id + "," + text + ")");
216         }
217 
218         String cached = (String) session.getAttribute(id);
219         if (cached != null) {
220             LOG.debug("returning cached value: " + id + "=" + cached);
221             return new Boolean(cached).booleanValue();
222         }
223 
224         if (id.equals(question)) {
225             session.setAttribute(id, Boolean.toString(!ConfirmationQuestion.NO.equals(buttonClicked)));
226             return !ConfirmationQuestion.NO.equals(buttonClicked);
227         }
228         else if (!session.hasAsked(id)) {
229             if (LOG.isDebugEnabled()) {
230                 LOG.debug("Forcing question to be asked: " + id);
231             }
232             session.askQuestion(id, text);
233         }
234 
235         LOG.debug("Throwing Exception to force return to Action");
236         throw new IsAskingException();
237     }
238 
239 }