001/**
002 * Copyright 2005-2015 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.krad.uif.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.datadictionary.state.StateMapping;
020import org.kuali.rice.krad.datadictionary.validation.constraint.BaseConstraint;
021import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
022import org.kuali.rice.krad.uif.view.View;
023
024import java.util.ArrayList;
025import java.util.List;
026
027/**
028 * Various utility methods for determining when to use constraints during states
029 *
030 * @author Kuali Rice Team (rice.collab@kuali.org)
031  *@since 2.2
032 */
033public class ConstraintStateUtils {
034
035    /**
036     * Determines if the constraint passed in applies for the applicableState, based on the stateMapping
037     *
038     * <p>Note: this method will automatically return TRUE if the stateMapping is null, the Constraint is not a BaseConstraint,
039     * if there are no states defined on the Constraint, or if the state it applies to cannot be found in the stateMapping,
040     * because in all these cases the Constraint is considered
041     * stateless and will apply to any state</p>
042     *
043     * @param applicableState the state to check to see if the constraint applies
044     * @param constraint the Constraint to check
045     * @param stateMapping the StateMapping object containing state information
046     * @return true if the Constraint applies to the applicableState, false otherwise
047     */
048    public static boolean constraintAppliesForState(String applicableState, Constraint constraint,
049            StateMapping stateMapping) {
050        List<String> stateOrder = new ArrayList<String>();
051        if (stateMapping != null) {
052            stateOrder = stateMapping.getStates();
053        }
054
055        if (stateMapping == null || !(constraint instanceof BaseConstraint) || StringUtils.isEmpty(applicableState)) {
056            //process constraint because it is considered "stateless" if not a BaseConstraint
057            //or no associated state mapping or no has no state to compare to
058            return true;
059        } else if (((BaseConstraint) constraint).getStates() == null || ((BaseConstraint) constraint).getStates()
060                .isEmpty()) {
061            //simple case - no states for this constraint, so always apply
062            return true;
063        } else if (((BaseConstraint) constraint).getStates().contains(applicableState) && stateOrder.contains(
064                applicableState)) {
065            //constraint applies for the applicableState and the state exists for the object
066            return true;
067        } else {
068            for (String constraintState : ((BaseConstraint) constraint).getStates()) {
069                //range case
070                if (constraintState.contains(">")) {
071                    String[] rangeArray = constraintState.split(">");
072                    if (rangeArray[1].endsWith("+")) {
073                        //make 2nd part of range current state being checked if nothing is
074                        //matched below for the range case
075                        constraintState = rangeArray[1];
076                        rangeArray[1] = StringUtils.removeEnd(rangeArray[1], "+");
077                    }
078                    if (stateOrder.contains(rangeArray[0]) && stateOrder.contains(rangeArray[1])) {
079                        for (int i = stateOrder.indexOf(rangeArray[0]); i <= stateOrder.indexOf(rangeArray[1]); i++) {
080                            if (stateOrder.get(i).equals(applicableState)) {
081                                return true;
082                            }
083                        }
084                    } else {
085                        throw new RuntimeException("Invalid state range: " + constraintState);
086                    }
087                }
088
089                //+ case (everything after and including this state)
090                if (constraintState.contains("+")) {
091                    constraintState = StringUtils.removeEnd(constraintState, "+");
092                    if (stateOrder.contains(constraintState)) {
093                        for (int i = stateOrder.indexOf(constraintState); i < stateOrder.size(); i++) {
094                            if (stateOrder.get(i).equals(applicableState)) {
095                                return true;
096                            }
097                        }
098                    } else {
099                        throw new RuntimeException("Invalid constraint state: " + constraintState);
100                    }
101                }
102            }
103        }
104        //if no case is matched, return false
105        return false;
106    }
107
108    /**
109     * Gets the constraint that applies for the validationState passed in with the appropriate StateMapping (which
110     * should include validationState as one of its states).
111     *
112     * <p>This method will essentially return the constraint
113     * passed in, in most cases, if the constraint applies.  In cases where there are constraintStateOverrides
114     * set for the constraint, those will be evaluated to get the appropriate replacement.  If the constraint does
115     * not apply for the validationState (as well as none of the replacements), this method will return null.
116     * If stateMapping passed in is null, the constraint is not a BaseConstraint, or validationState is blank,
117     * the original constraint will be returned
118     * (assumed stateless).</p>
119     *
120     * @param constraint the original constraint
121     * @param validationState the validation state
122     * @param stateMapping the state information for the model being evaluated
123     * @param <T> constraint type
124     * @return the applicable constraint, null if this constraint does not apply to the validationState
125     * @throws RuntimeException if the type of constraint passed in cannot be cast to the replacement constraint
126     */
127    public static <T extends Constraint> T getApplicableConstraint(T constraint, String validationState,
128            StateMapping stateMapping) {
129
130        //is state information setup?
131        if (constraint != null && constraint instanceof BaseConstraint && stateMapping != null &&
132                StringUtils.isNotBlank(validationState)) {
133
134            //Does the constraint have overrides?
135            if (((BaseConstraint) constraint).getConstraintStateOverrides() != null && !((BaseConstraint) constraint)
136                    .getConstraintStateOverrides().isEmpty()) {
137                T override = null;
138                BaseConstraint theConstraint = ((BaseConstraint) constraint);
139                for (BaseConstraint bc : theConstraint.getConstraintStateOverrides()) {
140                    //does the override apply for this state?
141                    if (!bc.getStates().isEmpty() && ConstraintStateUtils.constraintAppliesForState(validationState, bc,
142                            stateMapping)) {
143                        try {
144                            //Last on the list takes precedence
145                            override = (T) bc;
146                        } catch (ClassCastException e) {
147                            throw new RuntimeException("Replacement state constraint for this constraint is not an "
148                                    + "appropriate type: "
149                                    + constraint.getClass().toString()
150                                    + " cannot be cast to "
151                                    + bc.getClass().toString());
152                        }
153                    }
154                }
155
156                if(override != null){
157                    return override;
158                }
159                else if(override == null && ConstraintStateUtils.constraintAppliesForState(validationState, constraint, stateMapping)){
160                    //use base constraint if no overrides apply and it still applies for this state
161                    return constraint;
162                }
163                else{
164                    //the constaint AND its overrides do not apply
165                    return null;
166                }
167            } else if(ConstraintStateUtils.constraintAppliesForState(validationState, constraint, stateMapping)) {
168                //Constraint applies for this state
169                return constraint;
170            }
171            else{
172                //Constraint does not apply for this state
173                return null;
174            }
175        }
176
177        //state information either not setup or not setup correctly for this constraint/stateMapping,
178        //so constraint will apply by default
179        return constraint;
180    }
181
182    /**
183     * Gets the client validation state.  If there are customClientSideValidationStates configured for the view's
184     * stateMapping, these are used, otherwise client side validation state is assumed to be the next state (or
185     * current state if there is no next state).  Returns null if there is no state for client side validation (ie
186     * stateless).
187     *
188     * @param model
189     * @param view
190     * @return the state to validate against for client side view validation
191     */
192    public static String getClientViewValidationState(Object model, View view){
193        String validationState = null;
194        String path = view.getStateObjectBindingPath();
195        Object stateObject;
196
197        if (StringUtils.isNotBlank(path)) {
198            stateObject = ObjectPropertyUtils.getPropertyValue(model, path);
199        } else {
200            stateObject = model;
201        }
202        StateMapping stateMapping = view.getStateMapping();
203        if (stateMapping != null) {
204            validationState = stateMapping.getNextState(stateObject);
205
206            if (stateMapping.getCustomClientSideValidationStates() != null) {
207                String currentState = stateMapping.getCurrentState(stateObject);
208                validationState = stateMapping.getCustomClientSideValidationStates().get(currentState);
209                if (StringUtils.isBlank(validationState)) {
210                    validationState = stateMapping.getNextState(stateObject);
211                }
212            }
213        }
214        return validationState;
215    }
216}