1 /*
2 * Copyright 2011 The Kuali Foundation Licensed under the Educational Community
3 * License, Version 1.0 (the "License"); you may not use this file except in
4 * compliance with the License. You may obtain a copy of the License at
5 * http://www.opensource.org/licenses/ecl1.php Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
10 */
11 package org.kuali.rice.krad.uif.modifier;
12
13 import org.apache.commons.lang.StringUtils;
14 import org.apache.log4j.Logger;
15 import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
16 import org.kuali.rice.krad.uif.UifConstants;
17 import org.kuali.rice.krad.uif.UifPropertyPaths;
18 import org.kuali.rice.krad.uif.container.Group;
19 import org.kuali.rice.krad.uif.view.View;
20 import org.kuali.rice.krad.uif.component.Component;
21 import org.kuali.rice.krad.uif.field.AttributeField;
22 import org.kuali.rice.krad.uif.field.HeaderField;
23 import org.kuali.rice.krad.uif.util.ComponentUtils;
24 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
25
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32
33 /**
34 * Generates <code>Field</code> instances to produce a comparison view among
35 * objects of the same type
36 *
37 * <p>
38 * Modifier is initialized with a List of <code>ComparableInfo</code> instances.
39 * For each comparable info, a copy of the configured group field is made and
40 * adjusted to the binding object path for the comparable. The comparison fields
41 * are ordered based on the configured order property of the comparable. In
42 * addition, a <code>HeaderField<code> can be generated to label each group
43 * of comparison fields.
44 * </p>
45 *
46 * @author Kuali Rice Team (rice.collab@kuali.org)
47 */
48 public class CompareFieldCreateModifier extends ComponentModifierBase {
49 private static final Logger LOG = Logger.getLogger(CompareFieldCreateModifier.class);
50
51 private static final long serialVersionUID = -6285531580512330188L;
52
53 private int defaultOrderSequence;
54 private boolean generateCompareHeaders;
55
56 private HeaderField headerFieldPrototype;
57 private List<ComparableInfo> comparables;
58
59 public CompareFieldCreateModifier() {
60 defaultOrderSequence = 1;
61 generateCompareHeaders = true;
62
63 comparables = new ArrayList<ComparableInfo>();
64 }
65
66 /**
67 * Calls <code>ViewHelperService</code> to initialize the header field prototype
68 *
69 * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performInitialization(org.kuali.rice.krad.uif.view.View,
70 * java.lang.Object, org.kuali.rice.krad.uif.component.Component)
71 */
72 @Override
73 public void performInitialization(View view, Object model, Component component) {
74 super.performInitialization(view, model, component);
75
76 if (headerFieldPrototype != null) {
77 view.getViewHelperService().performComponentInitialization(view, model, headerFieldPrototype);
78 }
79 }
80
81 /**
82 * Generates the comparison fields
83 *
84 * <p>
85 * First the configured List of <code>ComparableInfo</code> instances are
86 * sorted based on their order property. Then if generateCompareHeaders is
87 * set to true, a <code>HeaderField</code> is created for each comparable
88 * using the headerFieldPrototype and the headerText given by the
89 * comparable. Finally for each field configured on the <code>Group</code>,
90 * a corresponding comparison field is generated for each comparable and
91 * adjusted to the binding object path given by the comparable in addition
92 * to suffixing the id and setting the readOnly property
93 * </p>
94 *
95 * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performModification(org.kuali.rice.krad.uif.view.View,
96 * java.lang.Object, org.kuali.rice.krad.uif.component.Component)
97 */
98 @SuppressWarnings("unchecked")
99 @Override
100 public void performModification(View view, Object model, Component component) {
101 if ((component != null) && !(component instanceof Group)) {
102 throw new IllegalArgumentException(
103 "Compare field initializer only support Group components, found type: " + component.getClass());
104 }
105
106 if (component == null) {
107 return;
108 }
109
110 // list to hold the generated compare items
111 List<Component> comparisonItems = new ArrayList<Component>();
112
113 // sort comparables by their order property
114 List<ComparableInfo> groupComparables = (List<ComparableInfo>) ComponentUtils.sort(comparables,
115 defaultOrderSequence);
116
117 // evaluate expressions on comparables
118 Map<String, Object> context = new HashMap<String, Object>();
119 context.putAll(view.getContext());
120 context.put(UifConstants.ContextVariableNames.COMPONENT, component);
121
122 for (ComparableInfo comparable : groupComparables) {
123 KRADServiceLocatorWeb.getExpressionEvaluatorService().evaluateObjectExpressions(comparable, model,
124 context);
125 }
126
127 // generate compare header
128 if (isGenerateCompareHeaders()) {
129 for (ComparableInfo comparable : groupComparables) {
130 HeaderField compareHeaderField = ComponentUtils.copy(headerFieldPrototype, comparable.getIdSuffix());
131 compareHeaderField.setHeaderText(comparable.getHeaderText());
132
133 comparisonItems.add(compareHeaderField);
134 }
135 }
136
137 // find the comparable to use for comparing value changes (if
138 // configured)
139 boolean performValueChangeComparison = false;
140 String compareValueObjectBindingPath = null;
141 for (ComparableInfo comparable : groupComparables) {
142 if (comparable.isCompareToForValueChange()) {
143 performValueChangeComparison = true;
144 compareValueObjectBindingPath = comparable.getBindingObjectPath();
145 }
146 }
147
148 // generate the compare items from the configured group
149 Group group = (Group) component;
150 for (Component item : group.getItems()) {
151 int defaultSuffix = 0;
152 for (ComparableInfo comparable : groupComparables) {
153 String idSuffix = comparable.getIdSuffix();
154 if (StringUtils.isBlank(idSuffix)) {
155 idSuffix = "_c" + defaultSuffix;
156 }
157
158 Component compareItem = ComponentUtils.copy(item, idSuffix);
159
160 ComponentUtils.setComponentPropertyDeep(compareItem, UifPropertyPaths.BIND_OBJECT_PATH,
161 comparable.getBindingObjectPath());
162 if (comparable.isReadOnly()) {
163 compareItem.setReadOnly(true);
164 if (compareItem.getPropertyExpressions().containsKey("readOnly")) {
165 compareItem.getPropertyExpressions().remove("readOnly");
166 }
167 }
168
169 // do value comparison
170 if (performValueChangeComparison && comparable.isHighlightValueChange() && !comparable
171 .isCompareToForValueChange()) {
172 performValueComparison(group, compareItem, model, compareValueObjectBindingPath);
173 }
174
175 comparisonItems.add(compareItem);
176 defaultSuffix++;
177 }
178 }
179
180 // update the group's list of components
181 group.setItems(comparisonItems);
182 }
183
184 /**
185 * For each attribute field in the compare item, retrieves the field value and compares against the value for the
186 * main comparable. If the value is different, adds script to the field on ready event to add the change icon to
187 * the field and the containing group header
188 *
189 * @param group - group that contains the item and whose header will be highlighted for changes
190 * @param compareItem - the compare item being generated and to pull attribute fields from
191 * @param model - object containing the data
192 * @param compareValueObjectBindingPath - object path for the comparison item
193 */
194 protected void performValueComparison(Group group, Component compareItem, Object model,
195 String compareValueObjectBindingPath) {
196 // get any attribute fields for the item so we can compare the values
197 List<AttributeField> itemFields = ComponentUtils.getComponentsOfTypeDeep(compareItem, AttributeField.class);
198 for (AttributeField field : itemFields) {
199 String fieldBindingPath = field.getBindingInfo().getBindingPath();
200 Object fieldValue = ObjectPropertyUtils.getPropertyValue(model, fieldBindingPath);
201
202 String compareBindingPath = StringUtils.replaceOnce(fieldBindingPath,
203 field.getBindingInfo().getBindingObjectPath(), compareValueObjectBindingPath);
204 Object compareValue = ObjectPropertyUtils.getPropertyValue(model, compareBindingPath);
205
206 boolean valueChanged = false;
207 if (!((fieldValue == null) && (compareValue == null))) {
208 // if one is null then value changed
209 if ((fieldValue == null) || (compareValue == null)) {
210 valueChanged = true;
211 } else {
212 // both not null, compare values
213 valueChanged = !fieldValue.equals(compareValue);
214 }
215 }
216
217 // add script to show change icon
218 if (valueChanged) {
219 String onReadyScript = "showChangeIcon('" + field.getId() + "');";
220
221 // add icon to group header
222 Component headerField = group.getHeader();
223 onReadyScript += "showChangeIconOnHeader('" + headerField.getId() + "');";
224
225 field.setOnDocumentReadyScript(onReadyScript);
226 }
227
228 // TODO: add script for value changed?
229 }
230 }
231
232 /**
233 * Generates an id suffix for the comparable item
234 *
235 * <p>
236 * If the idSuffix to use if configured on the <code>ComparableInfo</code>
237 * it will be used, else the given integer index will be used with an
238 * underscore
239 * </p>
240 *
241 * @param comparable - comparable info to check for id suffix
242 * @param index - sequence integer
243 * @return String id suffix
244 * @see org.kuali.rice.krad.uif.modifier.ComparableInfo.getIdSuffix()
245 */
246 protected String getIdSuffix(ComparableInfo comparable, int index) {
247 String idSuffix = comparable.getIdSuffix();
248 if (StringUtils.isBlank(idSuffix)) {
249 idSuffix = "_" + index;
250 }
251
252 return idSuffix;
253 }
254
255 /**
256 * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#getSupportedComponents()
257 */
258 @Override
259 public Set<Class<? extends Component>> getSupportedComponents() {
260 Set<Class<? extends Component>> components = new HashSet<Class<? extends Component>>();
261 components.add(Group.class);
262
263 return components;
264 }
265
266 /**
267 * Indicates the starting integer sequence value to use for
268 * <code>ComparableInfo</code> instances that do not have the order property
269 * set
270 *
271 * @return int default sequence starting value
272 */
273 public int getDefaultOrderSequence() {
274 return this.defaultOrderSequence;
275 }
276
277 /**
278 * Setter for the default sequence starting value
279 *
280 * @param defaultOrderSequence
281 */
282 public void setDefaultOrderSequence(int defaultOrderSequence) {
283 this.defaultOrderSequence = defaultOrderSequence;
284 }
285
286 /**
287 * Indicates whether a <code>HeaderField</code> should be created for each
288 * group of comparison fields
289 *
290 * <p>
291 * If set to true, for each group of comparison fields a header field will
292 * be created using the headerFieldPrototype configured on the modifier with
293 * the headerText property of the comparable
294 * </p>
295 *
296 * @return boolean true if the headers should be created, false if no
297 * headers should be created
298 */
299 public boolean isGenerateCompareHeaders() {
300 return this.generateCompareHeaders;
301 }
302
303 /**
304 * Setter for the generate comparison headers indicator
305 *
306 * @param generateCompareHeaders
307 */
308 public void setGenerateCompareHeaders(boolean generateCompareHeaders) {
309 this.generateCompareHeaders = generateCompareHeaders;
310 }
311
312 /**
313 * Prototype instance to use for creating the <code>HeaderField</code> for
314 * each group of comparison fields (if generateCompareHeaders is true)
315 *
316 * @return HeaderField header field prototype
317 */
318 public HeaderField getHeaderFieldPrototype() {
319 return this.headerFieldPrototype;
320 }
321
322 /**
323 * Setter for the header field prototype
324 *
325 * @param headerFieldPrototype
326 */
327 public void setHeaderFieldPrototype(HeaderField headerFieldPrototype) {
328 this.headerFieldPrototype = headerFieldPrototype;
329 }
330
331 /**
332 * List of <code>ComparableInfo</code> instances the compare fields should
333 * be generated for
334 *
335 * <p>
336 * For each comparable, a copy of the fields configured for the
337 * <code>Group</code> will be created for the comparison view
338 * </p>
339 *
340 * @return List<ComparableInfo> comparables to generate fields for
341 */
342 public List<ComparableInfo> getComparables() {
343 return this.comparables;
344 }
345
346 /**
347 * Setter for the list of comparable info instances
348 *
349 * @param comparables
350 */
351 public void setComparables(List<ComparableInfo> comparables) {
352 this.comparables = comparables;
353 }
354
355 }