001    /**
002     * Copyright 2005-2012 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.krad.uif.component;
017    
018    import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase;
019    import org.kuali.rice.krad.datadictionary.validator.ErrorReport;
020    import org.kuali.rice.krad.datadictionary.validator.RDValidator;
021    import org.kuali.rice.krad.datadictionary.validator.TracerToken;
022    
023    import java.io.Serializable;
024    import java.util.ArrayList;
025    import java.util.List;
026    import java.util.Map;
027    
028    /**
029     * Configuration for replacing a property value based on a condition
030     *
031     * <p>
032     * A <code>Component</code> may be configured with one or more <code>PropertyReplacer</code> instances. Each defines
033     * a condition to evaluate during the apply model phase, and if that condition succeeds the property on the component
034     * given by {@link #getPropertyName()}, will be replaced with the value given by {@link #getReplacement()}. Conditions
035     * are defined using an expression language and may reference any variables available in the component's context.
036     * </p>
037     *
038     * <p>
039     * Property replacers can be used to change out an entire Component or List/Map of Components. For example, based on a
040     * condition you might want to display a <code>TextControl</code> or <code>RadioControl</code> for an
041     * <code>InputField</code>. You can define the field with a text control, then include a property replacer as
042     * follows:
043     * <pre>
044            <bean parent="PropertyReplacer" p:propertyName="control"
045                  p:condition="field1 eq '10985'" p:replacement-ref="RadioControl"/>
046     *
047     * </pre>
048     *
049     * Note <code>Component</code> contains a <code>List</code> or property replacers which will be evaluated in the order
050     * contained within the list. So in the above example if we wanted to now add a further condition which sets the control
051     * to a checkbox, we would just add another property replacer bean.
052     * <pre>
053     *   <property name="propertyReplacers">
054           <list>
055            <bean parent="PropertyReplacer" p:propertyName="control"
056                  p:condition="field1 eq '10985'" p:replacement-ref="RadioControl"/>
057            <bean parent="PropertyReplacer" p:propertyName="control"
058                  p:condition="field1 eq '11456'" p:replacement-ref="CheckboxControl"/>
059     *     </list>
060     *   </property>
061     * </pre>
062     *
063     * Property replacers may be used to substitute primitive properties as well, such as Strings
064     * </p>
065     *
066     * @author Kuali Rice Team (rice.collab@kuali.org)
067     */
068    public class PropertyReplacer extends UifDictionaryBeanBase implements Serializable {
069        private static final long serialVersionUID = -8405429643299461398L;
070    
071        private String propertyName;
072    
073        @KeepExpression
074        private String condition;
075        private Object replacement;
076    
077        public PropertyReplacer() {
078            super();
079        }
080    
081        /**
082         * Returns a list of nested components
083         *
084         * <p>
085         * All nested components will be returned in the list. Current assumption is that
086         * <code>PropertyReplacer</code> can only contain a <code>Component</code>, <code>List</code> or
087         * <code>Map</code> for nested components
088         * </p>
089         *
090         * @return List<Component> nested components
091         */
092        public List<Component> getNestedComponents() {
093            ArrayList<Component> nestedComponents = new ArrayList<Component>();
094    
095            if (replacement instanceof Component) {
096                nestedComponents.add(((Component) replacement));
097            } else if (replacement instanceof List) {
098                for (Object replacementItem : (List<?>) replacement) {
099                    if (replacementItem instanceof Component) {
100                        nestedComponents.add((Component) replacementItem);
101                    }
102                }
103            } else if (replacement instanceof Map) {
104                for (Object replacementItem : ((Map<?, ?>) replacement).values()) {
105                    if (replacementItem instanceof Component) {
106                        nestedComponents.add((Component) replacementItem);
107                    }
108                }
109            }
110    
111            return nestedComponents;
112        }
113    
114        /**
115         * Name of the property on the Component the property replacer is associated with that
116         * will be set when the condition for the replacer succeeds
117         *
118         * <p>
119         * Note the property name must be readable/writable on the component. The property name may
120         * be nested, and include Map or List references.
121         * </p>
122         *
123         * @return String property name to set
124         */
125        public String getPropertyName() {
126            return this.propertyName;
127        }
128    
129        /**
130         * Setter for the property name that will be set
131         *
132         * @param propertyName
133         */
134        public void setPropertyName(String propertyName) {
135            this.propertyName = propertyName;
136        }
137    
138        /**
139         * Gives the expression that should be evaluated to determine whether or not
140         * the property replacement should be made
141         *
142         * <p>
143         * Expression follows SPEL and may access any model data along with any variables
144         * available in the context for the Component. The expression should evaluate to
145         * a boolean. If the resulting boolean is true, the object given by {@link #getReplacement()}
146         * will be set as the value for the associated property on the component. If the resulting
147         * boolean is false, no action will take place
148         * </p>
149         *
150         * <p>
151         * Note the value does not need to contain the expression placeholder @{}
152         * </p>
153         *
154         * @return String expression that should be evaluated
155         * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService
156         * @see org.kuali.rice.krad.uif.UifConstants.ContextVariableNames
157         */
158        public String getCondition() {
159            return this.condition;
160        }
161    
162        /**
163         * Setter for the replacement condition
164         *
165         * @param condition
166         */
167        public void setCondition(String condition) {
168            this.condition = condition;
169        }
170    
171        /**
172         * Gives the Object that should be used to set the property value if the replacers condition
173         * evaluates to true
174         *
175         * <p>
176         * Note the configured Object must be valid for the type given by the property on the Component. Standard
177         * property editors will be used for setting the property value
178         * </p>
179         *
180         * @return Object instance to set
181         */
182        public Object getReplacement() {
183            return this.replacement;
184        }
185    
186        /**
187         * Setter for the replacement Object
188         *
189         * @param replacement
190         */
191        public void setReplacement(Object replacement) {
192            this.replacement = replacement;
193        }
194    
195        /**
196         * Validates different requirements of component compiling a series of reports detailing information on errors
197         * found in the component.  Used by the RiceDictionaryValidator.
198         *
199         * @param tracer Record of component's location
200         * @param parser Set of tools for parsing the xml files which were used to create the component
201         * @return A list of ErrorReports detailing errors found within the component and referenced within it
202         */
203        public ArrayList<ErrorReport> completeValidation(TracerToken tracer){
204            ArrayList<ErrorReport> reports=new ArrayList<ErrorReport>();
205            tracer.addBean("PropertyReplacer",getPropertyName());
206    
207            // Checking that required fields are set
208            if(getPropertyName()==null || getCondition()==null || getReplacement()==null){
209                ErrorReport error = new ErrorReport(ErrorReport.WARNING);
210                error.setValidationFailed("PropertyName, condition and replacement should be set");
211                error.setBeanLocation(tracer.getBeanLocation());
212                error.addCurrentValue("propertyName ="+getPropertyName());
213                error.addCurrentValue("condition ="+getCondition());
214                error.addCurrentValue("replacement ="+getReplacement());
215                reports.add(error);
216            }
217    
218            // Validating Spring EL in condition
219            if(!RDValidator.validateSpringEL(getCondition())){
220                ErrorReport error = new ErrorReport(ErrorReport.ERROR);
221                error.setValidationFailed("Invalid Spring Expression Language");
222                error.setBeanLocation(tracer.getBeanLocation());
223                error.addCurrentValue("condition ="+getCondition());
224                reports.add(error);
225            }
226    
227            return reports;
228        }
229    }