001/** 002 * Copyright 2005-2018 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.kuali.rice.krad.datadictionary.parse.BeanTag; 020import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 021import org.kuali.rice.krad.datadictionary.parse.BeanTags; 022import org.kuali.rice.krad.uif.UifConstants; 023import org.kuali.rice.krad.uif.UifPropertyPaths; 024import org.kuali.rice.krad.uif.component.Component; 025import org.kuali.rice.krad.uif.container.Group; 026import org.kuali.rice.krad.uif.element.Header; 027import org.kuali.rice.krad.uif.field.DataField; 028import org.kuali.rice.krad.uif.field.Field; 029import org.kuali.rice.krad.uif.field.SpaceField; 030import org.kuali.rice.krad.uif.layout.GridLayoutManager; 031import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 032import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils; 033import org.kuali.rice.krad.uif.util.ComponentFactory; 034import org.kuali.rice.krad.uif.util.ComponentUtils; 035import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 036import org.kuali.rice.krad.uif.view.ExpressionEvaluator; 037import org.kuali.rice.krad.uif.view.View; 038 039import java.util.ArrayList; 040import java.util.HashMap; 041import java.util.HashSet; 042import java.util.List; 043import java.util.Map; 044import java.util.Set; 045 046/** 047 * Generates <code>Field</code> instances to produce a comparison view among 048 * objects of the same type 049 * 050 * <p> 051 * Modifier is initialized with a List of <code>ComparableInfo</code> instances. 052 * For each comparable info, a copy of the configured group field is made and 053 * adjusted to the binding object path for the comparable. The comparison fields 054 * are ordered based on the configured order property of the comparable. In 055 * addition, a <code>HeaderField<code> can be generated to label each group 056 * of comparison fields. 057 * </p> 058 * 059 * @author Kuali Rice Team (rice.collab@kuali.org) 060 */ 061@BeanTags({@BeanTag(name = "compareFieldCreateModifier", parent = "Uif-CompareFieldCreate-Modifier"), 062 @BeanTag(name = "maintenanceCompareModifier", parent = "Uif-MaintenanceCompare-Modifier")}) 063public class CompareFieldCreateModifier extends ComponentModifierBase { 064 private static final long serialVersionUID = -6285531580512330188L; 065 066 private int defaultOrderSequence; 067 private boolean generateCompareHeaders; 068 069 private Header headerFieldPrototype; 070 private List<ComparableInfo> comparables; 071 072 public CompareFieldCreateModifier() { 073 defaultOrderSequence = 1; 074 generateCompareHeaders = true; 075 076 comparables = new ArrayList<ComparableInfo>(); 077 } 078 079 /** 080 * {@inheritDoc} 081 */ 082 @Override 083 public void performInitialization(Object model, Component component) { 084 super.performInitialization(model, component); 085 086 if ((component != null) && !(component instanceof Group)) { 087 throw new IllegalArgumentException( 088 "Compare field initializer only support Group components, found type: " + component.getClass()); 089 } 090 091 if (component == null) { 092 return; 093 } 094 095 Group group = (Group) component; 096 097 // add the renderOnComparableModifier to allow for optional field rendering based on the comparable 098 for (Component item : group.getItems()) { 099 item.pushObjectToContext("renderOnComparableModifier", Boolean.TRUE); 100 } 101 } 102 103 /** 104 * Generates the comparison fields 105 * 106 * <p> 107 * First the configured List of ComparableInfo instances are 108 * sorted based on their order property. Then if generateCompareHeaders is 109 * set to true, a HeaderField is created for each comparable 110 * using the headerFieldPrototype and the headerText given by the 111 * comparable. Finally for each field configured on the Group, 112 * a corresponding comparison field is generated for each comparable and 113 * adjusted to the binding object path given by the comparable in addition 114 * to suffixing the id and setting the readOnly property 115 * </p> 116 * 117 * {@inheritDoc} 118 */ 119 @Override 120 public void performModification(Object model, Component component) { 121 if ((component != null) && !(component instanceof Group)) { 122 throw new IllegalArgumentException( 123 "Compare field initializer only support Group components, found type: " + component.getClass()); 124 } 125 126 if (component == null) { 127 return; 128 } 129 130 Group group = (Group) component; 131 132 // list to hold the generated compare items 133 List<Component> comparisonItems = new ArrayList<Component>(); 134 135 // sort comparables by their order property 136 List<ComparableInfo> groupComparables = ComponentUtils.sort(comparables, defaultOrderSequence); 137 138 // evaluate expressions on comparables 139 Map<String, Object> context = new HashMap<String, Object>(); 140 141 View view = ViewLifecycle.getView(); 142 143 Map<String, Object> viewContext = view.getContext(); 144 if (viewContext != null) { 145 context.putAll(view.getContext()); 146 } 147 148 context.put(UifConstants.ContextVariableNames.COMPONENT, component); 149 150 ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator(); 151 152 for (ComparableInfo comparable : groupComparables) { 153 expressionEvaluator.evaluateExpressionsOnConfigurable(view, comparable, context); 154 } 155 156 // generate compare header 157 if (isGenerateCompareHeaders()) { 158 // add space field for label column 159 SpaceField spaceField = ComponentFactory.getSpaceField(); 160 comparisonItems.add(spaceField); 161 162 for (ComparableInfo comparable : groupComparables) { 163 Header compareHeaderField = ComponentUtils.copy(headerFieldPrototype, comparable.getComparableId()); 164 compareHeaderField.setHeaderText(comparable.getHeaderText()); 165 comparisonItems.add(compareHeaderField); 166 } 167 168 // if group is using grid layout then some extra processing needed 169 if (group.getLayoutManager() instanceof GridLayoutManager) { 170 // make first row a header 171 ((GridLayoutManager) group.getLayoutManager()).setRenderFirstRowHeader(true); 172 // add blank row CSS class 173 ((GridLayoutManager) group.getLayoutManager()).getRowCssClasses().add(""); 174 } 175 } 176 177 // find the comparable to use for comparing value changes (if configured) 178 boolean performValueChangeComparison = false; 179 String compareValueObjectBindingPath = null; 180 for (ComparableInfo comparable : groupComparables) { 181 if (comparable.isCompareToForValueChange()) { 182 performValueChangeComparison = true; 183 compareValueObjectBindingPath = comparable.getBindingObjectPath(); 184 } 185 } 186 187 // generate the compare items from the configured group 188 boolean changeIconShowedOnHeader = false; 189 for (Component item : group.getItems()) { 190 191 // leave Header object as is, just increase colSpan and change css class 192 if (item instanceof Header) { 193 comparisonItems.add(item); 194 item.setColSpan(groupComparables.size() + 1); 195 196 // if group is using grid layout then some extra processing needed 197 if (group.getLayoutManager() instanceof GridLayoutManager) { 198 // add row CSS class 199 ((GridLayoutManager) group.getLayoutManager()).getRowCssClasses().add("row-separator"); 200 } 201 202 continue; 203 } 204 205 int defaultSuffix = 0; 206 boolean suppressLabel = false; 207 208 String rowCssClass = ""; 209 210 for (ComparableInfo comparable : groupComparables) { 211 String comparableId = comparable.getComparableId(); 212 if (StringUtils.isBlank(comparableId)) { 213 comparableId = UifConstants.IdSuffixes.COMPARE + defaultSuffix; 214 } 215 216 Component compareItem = ComponentUtils.copy(item, comparableId); 217 218 ComponentUtils.setComponentPropertyDeep(compareItem, UifPropertyPaths.BIND_OBJECT_PATH, 219 comparable.getBindingObjectPath()); 220 if (comparable.isReadOnly()) { 221 compareItem.setReadOnly(true); 222 if (compareItem.getPropertyExpressions().containsKey("readOnly")) { 223 compareItem.getPropertyExpressions().remove("readOnly"); 224 } 225 } 226 227 // label will be enabled for first comparable only 228 if (suppressLabel && (compareItem instanceof Field)) { 229 ((Field) compareItem).getFieldLabel().setRender(false); 230 } 231 232 // add the renderOnComparableModifier to allow for optional field rendering based on the comparable 233 compareItem.pushObjectToContext("renderOnComparableModifier", comparable.isCompareToForFieldRender()); 234 235 // do value comparison 236 if (performValueChangeComparison && comparable.isHighlightValueChange() && !comparable 237 .isCompareToForValueChange()) { 238 boolean valueChanged = performValueComparison(group, compareItem, model, 239 compareValueObjectBindingPath); 240 241 // add icon to group header if not done so yet 242 if (valueChanged && !changeIconShowedOnHeader && isGenerateCompareHeaders()) { 243 Group groupToSetHeader = null; 244 if (group.getDisclosure() != null && group.getDisclosure().isRender()) { 245 groupToSetHeader = group; 246 } else if (group.getContext().get(UifConstants.ContextVariableNames.PARENT) != null) { 247 // use the parent group to set the notification if available 248 groupToSetHeader = (Group) group.getContext().get(UifConstants.ContextVariableNames.PARENT); 249 } 250 251 if (groupToSetHeader != null) { 252 if (groupToSetHeader.getDisclosure().isRender()) { 253 groupToSetHeader.getDisclosure().setOnDocumentReadyScript( 254 "showChangeIconOnDisclosure('" + groupToSetHeader.getId() + "');"); 255 } else if (groupToSetHeader.getHeader() != null) { 256 groupToSetHeader.getHeader().setOnDocumentReadyScript( 257 "showChangeIconOnHeader('" + groupToSetHeader.getHeader().getId() + "');"); 258 } 259 } 260 261 changeIconShowedOnHeader = true; 262 } 263 264 // if value changed then set row CSS class for later use if using GridLayoutManager 265 if (valueChanged) { 266 rowCssClass = "uif-compared"; 267 } 268 } 269 270 comparisonItems.add(compareItem); 271 272 defaultSuffix++; 273 274 suppressLabel = true; 275 } 276 277 // if group is using grid layout then some extra processing needed 278 if (group.getLayoutManager() instanceof GridLayoutManager) { 279 // add row CSS class 280 ((GridLayoutManager) group.getLayoutManager()).getRowCssClasses().add(rowCssClass); 281 } 282 } 283 284 // update the group's list of components 285 group.setItems(comparisonItems); 286 } 287 288 /** 289 * For each attribute field in the compare item, retrieves the field value and compares against the value for the 290 * main comparable. If the value is different, adds script to the field on ready event to add the change icon to 291 * the field and the containing group header 292 * 293 * @param group group that contains the item and whose header will be highlighted for changes 294 * @param compareItem the compare item being generated and to pull attribute fields from 295 * @param model object containing the data 296 * @param compareValueObjectBindingPath object path for the comparison item 297 * @return true if the value in the field represented by compareItem is equal to the comparison items value, false 298 * otherwise 299 */ 300 protected boolean performValueComparison(Group group, Component compareItem, Object model, 301 String compareValueObjectBindingPath) { 302 // get any attribute fields for the item so we can compare the values 303 List<DataField> itemFields = ViewLifecycleUtils.getElementsOfTypeDeep(compareItem, DataField.class); 304 boolean valueChanged = false; 305 for (DataField field : itemFields) { 306 String fieldBindingPath = field.getBindingInfo().getBindingPath(); 307 if (field.getPropertyName() != null && field.getPropertyName().length() > 0 && !fieldBindingPath.endsWith(field.getPropertyName())) { 308 fieldBindingPath += "." + field.getPropertyName(); 309 } 310 Object fieldValue = ObjectPropertyUtils.getPropertyValue(model, fieldBindingPath); 311 312 String compareBindingPath = StringUtils.replaceOnce(fieldBindingPath, 313 field.getBindingInfo().getBindingObjectPath(), compareValueObjectBindingPath); 314 Object compareValue = ObjectPropertyUtils.getPropertyValue(model, compareBindingPath); 315 316 if (!((fieldValue == null) && (compareValue == null))) { 317 // if one is null then value changed 318 if ((fieldValue == null) || (compareValue == null)) { 319 valueChanged = true; 320 } else { 321 // both not null, compare values 322 valueChanged = !fieldValue.equals(compareValue); 323 } 324 } 325 if (valueChanged) { 326 // add script to show change icon 327 String onReadyScript = "showChangeIcon('" + field.getId() + "');"; 328 field.setRenderMarkerIconSpan(true); 329 field.setOnDocumentReadyScript(onReadyScript); 330 } 331 // TODO: add script for value changed? 332 } 333 return valueChanged; 334 } 335 336 /** 337 * Generates an comparableId suffix for the comparable item 338 * 339 * <p> 340 * If the comparableId to use if configured on the ComparableInfo 341 * it will be used, else the given integer index will be used with an 342 * underscore 343 * </p> 344 * 345 * @param comparable comparable info to check for id suffix 346 * @param index sequence integer 347 * @return id suffix 348 * @see org.kuali.rice.krad.uif.modifier.ComparableInfo#getComparableId() 349 */ 350 protected String getComparableId(ComparableInfo comparable, int index) { 351 String comparableId = comparable.getComparableId(); 352 if (StringUtils.isBlank(comparableId)) { 353 comparableId = "_" + index; 354 } 355 356 return comparableId; 357 } 358 359 /** 360 * {@inheritDoc} 361 */ 362 @Override 363 public Set<Class<? extends Component>> getSupportedComponents() { 364 Set<Class<? extends Component>> components = new HashSet<Class<? extends Component>>(); 365 components.add(Group.class); 366 367 return components; 368 } 369 370 /** 371 * @see org.kuali.rice.krad.uif.modifier.ComponentModifierBase#getComponentPrototypes() 372 */ 373 public List<Component> getComponentPrototypes() { 374 List<Component> components = new ArrayList<Component>(); 375 376 components.add(headerFieldPrototype); 377 378 return components; 379 } 380 381 /** 382 * Indicates the starting integer sequence value to use for 383 * <code>ComparableInfo</code> instances that do not have the order property 384 * set 385 * 386 * @return default sequence starting value 387 */ 388 @BeanTagAttribute 389 public int getDefaultOrderSequence() { 390 return this.defaultOrderSequence; 391 } 392 393 /** 394 * Setter for the default sequence starting value 395 * 396 * @param defaultOrderSequence 397 */ 398 public void setDefaultOrderSequence(int defaultOrderSequence) { 399 this.defaultOrderSequence = defaultOrderSequence; 400 } 401 402 /** 403 * Indicates whether a <code>HeaderField</code> should be created for each 404 * group of comparison fields 405 * 406 * <p> 407 * If set to true, for each group of comparison fields a header field will 408 * be created using the headerFieldPrototype configured on the modifier with 409 * the headerText property of the comparable 410 * </p> 411 * 412 * @return true if the headers should be created, false if no 413 * headers should be created 414 */ 415 @BeanTagAttribute 416 public boolean isGenerateCompareHeaders() { 417 return this.generateCompareHeaders; 418 } 419 420 /** 421 * Setter for the generate comparison headers indicator 422 * 423 * @param generateCompareHeaders 424 */ 425 public void setGenerateCompareHeaders(boolean generateCompareHeaders) { 426 this.generateCompareHeaders = generateCompareHeaders; 427 } 428 429 /** 430 * Prototype instance to use for creating the <code>HeaderField</code> for 431 * each group of comparison fields (if generateCompareHeaders is true) 432 * 433 * @return header field prototype 434 */ 435 @BeanTagAttribute 436 public Header getHeaderFieldPrototype() { 437 return this.headerFieldPrototype; 438 } 439 440 /** 441 * Setter for the header field prototype 442 * 443 * @param headerFieldPrototype 444 */ 445 public void setHeaderFieldPrototype(Header headerFieldPrototype) { 446 this.headerFieldPrototype = headerFieldPrototype; 447 } 448 449 /** 450 * List of <code>ComparableInfo</code> instances the compare fields should 451 * be generated for 452 * 453 * <p> 454 * For each comparable, a copy of the fields configured for the 455 * <code>Group</code> will be created for the comparison view 456 * </p> 457 * 458 * @return comparables to generate fields for 459 */ 460 @BeanTagAttribute 461 public List<ComparableInfo> getComparables() { 462 return this.comparables; 463 } 464 465 /** 466 * Setter for the list of comparable info instances 467 * 468 * @param comparables 469 */ 470 public void setComparables(List<ComparableInfo> comparables) { 471 this.comparables = comparables; 472 } 473 474}