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}