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 */
016package org.kuali.rice.krad.uif.modifier;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
021import org.kuali.rice.krad.uif.UifConstants;
022import org.kuali.rice.krad.uif.UifPropertyPaths;
023import org.kuali.rice.krad.uif.container.Group;
024import org.kuali.rice.krad.uif.field.DataField;
025import org.kuali.rice.krad.uif.view.View;
026import org.kuali.rice.krad.uif.component.Component;
027import org.kuali.rice.krad.uif.field.HeaderField;
028import org.kuali.rice.krad.uif.util.ComponentUtils;
029import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
030
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038/**
039 * Generates <code>Field</code> instances to produce a comparison view among
040 * objects of the same type
041 *
042 * <p>
043 * Modifier is initialized with a List of <code>ComparableInfo</code> instances.
044 * For each comparable info, a copy of the configured group field is made and
045 * adjusted to the binding object path for the comparable. The comparison fields
046 * are ordered based on the configured order property of the comparable. In
047 * addition, a <code>HeaderField<code> can be generated to label each group
048 * of comparison fields.
049 * </p>
050 *
051 * @author Kuali Rice Team (rice.collab@kuali.org)
052 */
053public class CompareFieldCreateModifier extends ComponentModifierBase {
054    private static final Logger LOG = Logger.getLogger(CompareFieldCreateModifier.class);
055
056    private static final long serialVersionUID = -6285531580512330188L;
057
058    private int defaultOrderSequence;
059    private boolean generateCompareHeaders;
060
061    private HeaderField headerFieldPrototype;
062    private List<ComparableInfo> comparables;
063
064    public CompareFieldCreateModifier() {
065        defaultOrderSequence = 1;
066        generateCompareHeaders = true;
067
068        comparables = new ArrayList<ComparableInfo>();
069    }
070
071    /**
072     * Calls <code>ViewHelperService</code> to initialize the header field prototype
073     *
074     * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performInitialization(org.kuali.rice.krad.uif.view.View,
075     *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
076     */
077    @Override
078    public void performInitialization(View view, Object model, Component component) {
079        super.performInitialization(view, model, component);
080
081        if (headerFieldPrototype != null) {
082            view.getViewHelperService().performComponentInitialization(view, model, headerFieldPrototype);
083        }
084    }
085
086    /**
087     * Generates the comparison fields
088     *
089     * <p>
090     * First the configured List of <code>ComparableInfo</code> instances are
091     * sorted based on their order property. Then if generateCompareHeaders is
092     * set to true, a <code>HeaderField</code> is created for each comparable
093     * using the headerFieldPrototype and the headerText given by the
094     * comparable. Finally for each field configured on the <code>Group</code>,
095     * a corresponding comparison field is generated for each comparable and
096     * adjusted to the binding object path given by the comparable in addition
097     * to suffixing the id and setting the readOnly property
098     * </p>
099     *
100     * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performModification(org.kuali.rice.krad.uif.view.View,
101     *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
102     */
103    @SuppressWarnings("unchecked")
104    @Override
105    public void performModification(View view, Object model, Component component) {
106        if ((component != null) && !(component instanceof Group)) {
107            throw new IllegalArgumentException(
108                    "Compare field initializer only support Group components, found type: " + component.getClass());
109        }
110
111        if (component == null) {
112            return;
113        }
114
115        // list to hold the generated compare items
116        List<Component> comparisonItems = new ArrayList<Component>();
117
118        // sort comparables by their order property
119        List<ComparableInfo> groupComparables = (List<ComparableInfo>) ComponentUtils.sort(comparables,
120                defaultOrderSequence);
121
122        // evaluate expressions on comparables
123        Map<String, Object> context = new HashMap<String, Object>();
124        context.putAll(view.getContext());
125        context.put(UifConstants.ContextVariableNames.COMPONENT, component);
126
127        for (ComparableInfo comparable : groupComparables) {
128            KRADServiceLocatorWeb.getExpressionEvaluatorService().evaluateObjectExpressions(comparable, model,
129                    context);
130        }
131
132        // generate compare header
133        if (isGenerateCompareHeaders()) {
134            for (ComparableInfo comparable : groupComparables) {
135                HeaderField compareHeaderField = ComponentUtils.copy(headerFieldPrototype, comparable.getIdSuffix());
136                compareHeaderField.setHeaderText(comparable.getHeaderText());
137
138                comparisonItems.add(compareHeaderField);
139            }
140        }
141
142        // find the comparable to use for comparing value changes (if
143        // configured)
144        boolean performValueChangeComparison = false;
145        String compareValueObjectBindingPath = null;
146        for (ComparableInfo comparable : groupComparables) {
147            if (comparable.isCompareToForValueChange()) {
148                performValueChangeComparison = true;
149                compareValueObjectBindingPath = comparable.getBindingObjectPath();
150            }
151        }
152
153        // generate the compare items from the configured group
154        Group group = (Group) component;
155        for (Component item : group.getItems()) {
156            int defaultSuffix = 0;
157            for (ComparableInfo comparable : groupComparables) {
158                String idSuffix = comparable.getIdSuffix();
159                if (StringUtils.isBlank(idSuffix)) {
160                    idSuffix = UifConstants.IdSuffixes.COMPARE + defaultSuffix;
161                }
162
163                Component compareItem = ComponentUtils.copy(item, idSuffix);
164
165                ComponentUtils.setComponentPropertyDeep(compareItem, UifPropertyPaths.BIND_OBJECT_PATH,
166                        comparable.getBindingObjectPath());
167                if (comparable.isReadOnly()) {
168                    compareItem.setReadOnly(true);
169                    if (compareItem.getPropertyExpressions().containsKey("readOnly")) {
170                        compareItem.getPropertyExpressions().remove("readOnly");
171                    }
172                }
173
174                // do value comparison
175                if (performValueChangeComparison && comparable.isHighlightValueChange() && !comparable
176                        .isCompareToForValueChange()) {
177                    performValueComparison(group, compareItem, model, compareValueObjectBindingPath);
178                }
179
180                comparisonItems.add(compareItem);
181                defaultSuffix++;
182            }
183        }
184
185        // update the group's list of components
186        group.setItems(comparisonItems);
187    }
188
189    /**
190     * For each attribute field in the compare item, retrieves the field value and compares against the value for the
191     * main comparable. If the value is different, adds script to the field on ready event to add the change icon to
192     * the field and the containing group header
193     *
194     * @param group - group that contains the item and whose header will be highlighted for changes
195     * @param compareItem - the compare item being generated and to pull attribute fields from
196     * @param model - object containing the data
197     * @param compareValueObjectBindingPath - object path for the comparison item
198     */
199    protected void performValueComparison(Group group, Component compareItem, Object model,
200            String compareValueObjectBindingPath) {
201        // get any attribute fields for the item so we can compare the values
202        List<DataField> itemFields = ComponentUtils.getComponentsOfTypeDeep(compareItem, DataField.class);
203        for (DataField field : itemFields) {
204            String fieldBindingPath = field.getBindingInfo().getBindingPath();
205            Object fieldValue = ObjectPropertyUtils.getPropertyValue(model, fieldBindingPath);
206
207            String compareBindingPath = StringUtils.replaceOnce(fieldBindingPath,
208                    field.getBindingInfo().getBindingObjectPath(), compareValueObjectBindingPath);
209            Object compareValue = ObjectPropertyUtils.getPropertyValue(model, compareBindingPath);
210
211            boolean valueChanged = false;
212            if (!((fieldValue == null) && (compareValue == null))) {
213                // if one is null then value changed
214                if ((fieldValue == null) || (compareValue == null)) {
215                    valueChanged = true;
216                } else {
217                    // both not null, compare values
218                    valueChanged = !fieldValue.equals(compareValue);
219                }
220            }
221
222            // add script to show change icon
223            if (valueChanged) {
224                String onReadyScript = "showChangeIcon('" + field.getId() + "');";
225
226                // add icon to group header
227                Component headerField = group.getHeader();
228                onReadyScript += "showChangeIconOnHeader('" + headerField.getId() + "');";
229
230                field.setOnDocumentReadyScript(onReadyScript);
231            }
232
233            // TODO: add script for value changed?
234        }
235    }
236
237    /**
238     * Generates an id suffix for the comparable item
239     *
240     * <p>
241     * If the idSuffix to use if configured on the <code>ComparableInfo</code>
242     * it will be used, else the given integer index will be used with an
243     * underscore
244     * </p>
245     *
246     * @param comparable - comparable info to check for id suffix
247     * @param index - sequence integer
248     * @return String id suffix
249     * @see org.kuali.rice.krad.uif.modifier.ComparableInfo.getIdSuffix()
250     */
251    protected String getIdSuffix(ComparableInfo comparable, int index) {
252        String idSuffix = comparable.getIdSuffix();
253        if (StringUtils.isBlank(idSuffix)) {
254            idSuffix = "_" + index;
255        }
256
257        return idSuffix;
258    }
259
260    /**
261     * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#getSupportedComponents()
262     */
263    @Override
264    public Set<Class<? extends Component>> getSupportedComponents() {
265        Set<Class<? extends Component>> components = new HashSet<Class<? extends Component>>();
266        components.add(Group.class);
267
268        return components;
269    }
270
271    /**
272     * @see org.kuali.rice.krad.uif.modifier.ComponentModifierBase#getComponentPrototypes()
273     */
274    public List<Component> getComponentPrototypes() {
275        List<Component> components = new ArrayList<Component>();
276
277        components.add(headerFieldPrototype);
278
279        return components;
280    }
281
282    /**
283     * Indicates the starting integer sequence value to use for
284     * <code>ComparableInfo</code> instances that do not have the order property
285     * set
286     *
287     * @return int default sequence starting value
288     */
289    public int getDefaultOrderSequence() {
290        return this.defaultOrderSequence;
291    }
292
293    /**
294     * Setter for the default sequence starting value
295     *
296     * @param defaultOrderSequence
297     */
298    public void setDefaultOrderSequence(int defaultOrderSequence) {
299        this.defaultOrderSequence = defaultOrderSequence;
300    }
301
302    /**
303     * Indicates whether a <code>HeaderField</code> should be created for each
304     * group of comparison fields
305     *
306     * <p>
307     * If set to true, for each group of comparison fields a header field will
308     * be created using the headerFieldPrototype configured on the modifier with
309     * the headerText property of the comparable
310     * </p>
311     *
312     * @return boolean true if the headers should be created, false if no
313     *         headers should be created
314     */
315    public boolean isGenerateCompareHeaders() {
316        return this.generateCompareHeaders;
317    }
318
319    /**
320     * Setter for the generate comparison headers indicator
321     *
322     * @param generateCompareHeaders
323     */
324    public void setGenerateCompareHeaders(boolean generateCompareHeaders) {
325        this.generateCompareHeaders = generateCompareHeaders;
326    }
327
328    /**
329     * Prototype instance to use for creating the <code>HeaderField</code> for
330     * each group of comparison fields (if generateCompareHeaders is true)
331     *
332     * @return HeaderField header field prototype
333     */
334    public HeaderField getHeaderFieldPrototype() {
335        return this.headerFieldPrototype;
336    }
337
338    /**
339     * Setter for the header field prototype
340     *
341     * @param headerFieldPrototype
342     */
343    public void setHeaderFieldPrototype(HeaderField headerFieldPrototype) {
344        this.headerFieldPrototype = headerFieldPrototype;
345    }
346
347    /**
348     * List of <code>ComparableInfo</code> instances the compare fields should
349     * be generated for
350     *
351     * <p>
352     * For each comparable, a copy of the fields configured for the
353     * <code>Group</code> will be created for the comparison view
354     * </p>
355     *
356     * @return List<ComparableInfo> comparables to generate fields for
357     */
358    public List<ComparableInfo> getComparables() {
359        return this.comparables;
360    }
361
362    /**
363     * Setter for the list of comparable info instances
364     *
365     * @param comparables
366     */
367    public void setComparables(List<ComparableInfo> comparables) {
368        this.comparables = comparables;
369    }
370
371}