001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kns.rules;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.struts.action.ActionForm;
020import org.kuali.rice.core.api.util.RiceConstants;
021import org.kuali.rice.kns.rule.PromptBeforeValidation;
022import org.kuali.rice.kns.rule.event.PromptBeforeValidationEvent;
023import org.kuali.rice.kns.web.struts.form.KualiForm;
024import org.kuali.rice.krad.document.Document;
025import org.kuali.rice.kns.question.ConfirmationQuestion;
026import org.kuali.rice.krad.util.KRADConstants;
027
028import javax.servlet.http.HttpServletRequest;
029import java.util.Arrays;
030import java.util.Iterator;
031import 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 */
045public 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}