1 /**
2 * Copyright 2005-2012 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.krad.uif.modifier;
17
18 import org.apache.commons.lang.StringUtils;
19 import org.apache.log4j.Logger;
20 import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
21 import org.kuali.rice.krad.uif.UifConstants;
22 import org.kuali.rice.krad.uif.UifPropertyPaths;
23 import org.kuali.rice.krad.uif.container.Group;
24 import org.kuali.rice.krad.uif.field.DataField;
25 import org.kuali.rice.krad.uif.view.View;
26 import org.kuali.rice.krad.uif.component.Component;
27 import org.kuali.rice.krad.uif.field.HeaderField;
28 import org.kuali.rice.krad.uif.util.ComponentUtils;
29 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
30
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37
38 /**
39 * Generates <code>Field</code> instances to produce a comparison view among
40 * objects of the same type
41 *
42 * <p>
43 * Modifier is initialized with a List of <code>ComparableInfo</code> instances.
44 * For each comparable info, a copy of the configured group field is made and
45 * adjusted to the binding object path for the comparable. The comparison fields
46 * are ordered based on the configured order property of the comparable. In
47 * addition, a <code>HeaderField<code> can be generated to label each group
48 * of comparison fields.
49 * </p>
50 *
51 * @author Kuali Rice Team (rice.collab@kuali.org)
52 */
53 public class CompareFieldCreateModifier extends ComponentModifierBase {
54 private static final Logger LOG = Logger.getLogger(CompareFieldCreateModifier.class);
55
56 private static final long serialVersionUID = -6285531580512330188L;
57
58 private int defaultOrderSequence;
59 private boolean generateCompareHeaders;
60
61 private HeaderField headerFieldPrototype;
62 private List<ComparableInfo> comparables;
63
64 public CompareFieldCreateModifier() {
65 defaultOrderSequence = 1;
66 generateCompareHeaders = true;
67
68 comparables = new ArrayList<ComparableInfo>();
69 }
70
71 /**
72 * Calls <code>ViewHelperService</code> to initialize the header field prototype
73 *
74 * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performInitialization(org.kuali.rice.krad.uif.view.View,
75 * java.lang.Object, org.kuali.rice.krad.uif.component.Component)
76 */
77 @Override
78 public void performInitialization(View view, Object model, Component component) {
79 super.performInitialization(view, model, component);
80
81 if (headerFieldPrototype != null) {
82 view.getViewHelperService().performComponentInitialization(view, model, headerFieldPrototype);
83 }
84 }
85
86 /**
87 * Generates the comparison fields
88 *
89 * <p>
90 * First the configured List of <code>ComparableInfo</code> instances are
91 * sorted based on their order property. Then if generateCompareHeaders is
92 * set to true, a <code>HeaderField</code> is created for each comparable
93 * using the headerFieldPrototype and the headerText given by the
94 * comparable. Finally for each field configured on the <code>Group</code>,
95 * a corresponding comparison field is generated for each comparable and
96 * adjusted to the binding object path given by the comparable in addition
97 * to suffixing the id and setting the readOnly property
98 * </p>
99 *
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 }