001    /**
002     * Copyright 2005-2014 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     * @deprecated Only used in KNS classes, use KRAD.
044     */
045    @Deprecated
046    public abstract class PromptBeforeValidationBase implements PromptBeforeValidation {
047    
048        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PromptBeforeValidationBase.class);
049    
050        protected String question;
051        protected String buttonClicked;
052        protected PromptBeforeValidationEvent event;
053        protected KualiForm form;
054    
055        private class IsAskingException extends RuntimeException {
056        }
057    
058        /**
059         * 
060         * This class acts similarly to HTTP session, but working inside a REQUEST parameter
061         * 
062         * 
063         */
064        /**
065         * This is a description of what this class does - wliang don't forget to fill this in. 
066         * 
067         * @author Kuali Rice Team (rice.collab@kuali.org)
068         *
069         */
070        public class ContextSession {
071            private final static String DELIMITER = ".";
072            PromptBeforeValidationEvent event;
073    
074            public ContextSession(String context, PromptBeforeValidationEvent event) {
075                this.event = event;
076    
077                this.event.setQuestionContext(context);
078                if (this.event.getQuestionContext() == null) {
079                    this.event.setQuestionContext("");
080                }
081    
082            }
083    
084            /**
085             * Whether a question with a given ID has already been asked
086             * 
087             * @param id the ID of the question, an arbitrary value, but must be consistent
088             * @return
089             */
090            public boolean hasAsked(String id) {
091                return StringUtils.contains(event.getQuestionContext(), id);
092            }
093    
094            /**
095             * Invoked to indicate that the user should be prompted a question
096             * 
097             * @param id the ID of the question, an arbitrary value, but must be consistent
098             * @param text the question text, to be displayed to the user
099             */
100            public void askQuestion(String id, String text) {
101                event.setQuestionId(id);
102                event.setQuestionType(KRADConstants.CONFIRMATION_QUESTION);
103                event.setQuestionText(text);
104                event.setPerformQuestion(true);
105            }
106    
107            public void setAttribute(String name, String value) {
108                if (LOG.isDebugEnabled()) {
109                    LOG.debug("setAttribute(" + name + "," + value + ")");
110                }
111                event.setQuestionContext(event.getQuestionContext() + DELIMITER + name + DELIMITER + value);
112    
113            }
114    
115            public String getAttribute(String name) {
116                if (LOG.isDebugEnabled()) {
117                    LOG.debug("getAttribute(" + name + ")");
118                }
119                String result = null;
120    
121                Iterator values = Arrays.asList(event.getQuestionContext().split("\\" + DELIMITER)).iterator();
122    
123                while (values.hasNext()) {
124                    if (values.next().equals(name)) {
125                        try {
126                            result = (String) values.next();
127                        }
128                        catch (NoSuchElementException e) {
129                            result = null;
130                        }
131                    }
132                }
133                if (LOG.isDebugEnabled()) {
134                    LOG.debug("returning " + result);
135                }
136                return result;
137            }
138    
139        }
140    
141        /**
142         * Implementations will override this method to do perform the actual prompting and/or logic
143         * 
144         * They are able to utilize the following methods:
145         * <li> {@link PromptBeforeValidationBase#abortRulesCheck()}
146         * <li> {@link PromptBeforeValidationBase#askOrAnalyzeYesNoQuestion(String, String)}
147         * <li> {@link #hasAsked(String)}
148         * 
149         * @param document
150         * @return
151         */
152        public abstract boolean doPrompts(Document document);
153    
154        private boolean isAborting;
155    
156        ContextSession session;
157    
158        public PromptBeforeValidationBase() {
159        }
160    
161    
162        public boolean processPrompts(ActionForm form, HttpServletRequest request, PromptBeforeValidationEvent event) {
163            question = request.getParameter(KRADConstants.QUESTION_INST_ATTRIBUTE_NAME);
164            buttonClicked = request.getParameter(KRADConstants.QUESTION_CLICKED_BUTTON);
165            this.event = event;
166            this.form = (KualiForm) form;
167    
168            if (LOG.isDebugEnabled()) {
169                LOG.debug("Question is: " + question);
170                LOG.debug("ButtonClicked: " + buttonClicked);
171                LOG.debug("QuestionContext() is: " + event.getQuestionContext());
172            }
173    
174            session = new ContextSession(request.getParameter(KRADConstants.QUESTION_CONTEXT), event);
175    
176            boolean result = false;
177    
178            try {
179                result = doPrompts(event.getDocument());
180            }
181            catch (IsAskingException e) {
182                return false;
183            }
184    
185            if (isAborting) {
186                return false;
187            }
188    
189            return result;
190        }
191    
192        /**
193         * This bounces the user back to the document as if they had never tried to routed it. (Business rules are not invoked
194         * and the action is not executed.)
195         * 
196         */
197        public void abortRulesCheck() {
198            event.setActionForwardName(RiceConstants.MAPPING_BASIC);
199            isAborting = true;
200        }
201    
202        /**
203         * This method poses a Y/N question to the user.  If the user has already answered the question, then it returns whether
204         * the answer to the question was yes or no
205         * 
206         * Code that invokes this method will behave a bit strangely, so you should try to keep it as simple as possible.
207         * 
208         * @param id an ID for the question
209         * @param text the text of the question, to be displayed on the screen
210         * @return true if the user answered Yes, false if the user answers no
211         * @throws IsAskingException if the user needs to be prompted the question
212         */
213        public boolean askOrAnalyzeYesNoQuestion(String id, String text) throws IsAskingException {
214    
215            if (LOG.isDebugEnabled()) {
216                LOG.debug("Entering askOrAnalyzeYesNoQuestion(" + id + "," + text + ")");
217            }
218    
219            String cached = (String) session.getAttribute(id);
220            if (cached != null) {
221                LOG.debug("returning cached value: " + id + "=" + cached);
222                return new Boolean(cached).booleanValue();
223            }
224    
225            if (id.equals(question)) {
226                session.setAttribute(id, Boolean.toString(!ConfirmationQuestion.NO.equals(buttonClicked)));
227                return !ConfirmationQuestion.NO.equals(buttonClicked);
228            }
229            else if (!session.hasAsked(id)) {
230                if (LOG.isDebugEnabled()) {
231                    LOG.debug("Forcing question to be asked: " + id);
232                }
233                session.askQuestion(id, text);
234            }
235    
236            LOG.debug("Throwing Exception to force return to Action");
237            throw new IsAskingException();
238        }
239    
240    }