001 /**
002 * Copyright 2005-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kns.rules;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.apache.struts.action.ActionForm;
020 import org.kuali.rice.core.api.util.RiceConstants;
021 import org.kuali.rice.kns.rule.PromptBeforeValidation;
022 import org.kuali.rice.kns.rule.event.PromptBeforeValidationEvent;
023 import org.kuali.rice.kns.web.struts.form.KualiForm;
024 import org.kuali.rice.krad.document.Document;
025 import org.kuali.rice.kns.question.ConfirmationQuestion;
026 import org.kuali.rice.krad.util.KRADConstants;
027
028 import javax.servlet.http.HttpServletRequest;
029 import java.util.Arrays;
030 import java.util.Iterator;
031 import java.util.NoSuchElementException;
032
033 /**
034 *
035 * This class simplifies requesting clarifying user input prior to applying business rules. It mostly shields the classes that
036 * extend it from being aware of the web layer, even though the input is collected via a series of one or more request/response
037 * cycles.
038 *
039 * Beware: method calls with side-effects will have unusual results. While it looks like the doRules method is executed
040 * sequentially, in fact, it is more of a geometric series: if n questions are asked, then the code up to and including the first
041 * question is executed n times, the second n-1 times, ..., the last question only one time.
042 *
043 *
044 */
045 public abstract class PromptBeforeValidationBase implements PromptBeforeValidation {
046
047 protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PromptBeforeValidationBase.class);
048
049 protected String question;
050 protected String buttonClicked;
051 protected PromptBeforeValidationEvent event;
052 protected KualiForm form;
053
054 private class IsAskingException extends RuntimeException {
055 }
056
057 /**
058 *
059 * This class acts similarly to HTTP session, but working inside a REQUEST parameter
060 *
061 *
062 */
063 /**
064 * This is a description of what this class does - wliang don't forget to fill this in.
065 *
066 * @author Kuali Rice Team (rice.collab@kuali.org)
067 *
068 */
069 public class ContextSession {
070 private final static String DELIMITER = ".";
071 PromptBeforeValidationEvent event;
072
073 public ContextSession(String context, PromptBeforeValidationEvent event) {
074 this.event = event;
075
076 this.event.setQuestionContext(context);
077 if (this.event.getQuestionContext() == null) {
078 this.event.setQuestionContext("");
079 }
080
081 }
082
083 /**
084 * Whether a question with a given ID has already been asked
085 *
086 * @param id the ID of the question, an arbitrary value, but must be consistent
087 * @return
088 */
089 public boolean hasAsked(String id) {
090 return StringUtils.contains(event.getQuestionContext(), id);
091 }
092
093 /**
094 * Invoked to indicate that the user should be prompted a question
095 *
096 * @param id the ID of the question, an arbitrary value, but must be consistent
097 * @param text the question text, to be displayed to the user
098 */
099 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 }