001/** 002 * Copyright 2005-2016 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.layout; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 020import org.kuali.rice.krad.uif.component.KeepExpression; 021import org.kuali.rice.krad.uif.container.CollectionGroup; 022import org.kuali.rice.krad.uif.container.Container; 023import org.kuali.rice.krad.uif.container.Group; 024import org.kuali.rice.krad.uif.field.FieldGroup; 025import org.kuali.rice.krad.uif.view.View; 026import org.kuali.rice.krad.uif.component.Component; 027import org.kuali.rice.krad.uif.component.DataBinding; 028import org.kuali.rice.krad.uif.field.ActionField; 029import org.kuali.rice.krad.uif.field.Field; 030import org.kuali.rice.krad.uif.util.ComponentUtils; 031import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 032 033import java.util.ArrayList; 034import java.util.List; 035 036/** 037 * Layout manager that works with <code>CollectionGroup</code> containers and 038 * renders the collection lines in a vertical row 039 * 040 * <p> 041 * For each line of the collection, a <code>Group</code> instance is created. 042 * The group header contains a label for the line (summary information), the 043 * group fields are the collection line fields, and the group footer contains 044 * the line actions. All the groups are rendered using the 045 * <code>BoxLayoutManager</code> with vertical orientation. 046 * </p> 047 * 048 * <p> 049 * Modify the lineGroupPrototype to change header/footer styles or any other 050 * customization for the line groups 051 * </p> 052 * 053 * @author Kuali Rice Team (rice.collab@kuali.org) 054 */ 055public class StackedLayoutManager extends LayoutManagerBase implements CollectionLayoutManager { 056 private static final long serialVersionUID = 4602368505430238846L; 057 058 @KeepExpression 059 private String summaryTitle; 060 private List<String> summaryFields; 061 062 private Group addLineGroup; 063 private Group lineGroupPrototype; 064 private FieldGroup subCollectionFieldGroupPrototype; 065 private Field selectFieldPrototype; 066 private Group wrapperGroup; 067 068 private List<Group> stackedGroups; 069 070 public StackedLayoutManager() { 071 super(); 072 073 summaryFields = new ArrayList<String>(); 074 stackedGroups = new ArrayList<Group>(); 075 } 076 077 /** 078 * The following actions are performed: 079 * 080 * <ul> 081 * <li>Initializes the prototypes</li> 082 * </ul> 083 * 084 * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.krad.uif.view.View, 085 * java.lang.Object, org.kuali.rice.krad.uif.container.Container) 086 */ 087 @Override 088 public void performInitialization(View view, Object model, Container container) { 089 super.performInitialization(view, model, container); 090 091 stackedGroups = new ArrayList<Group>(); 092 093 if (addLineGroup != null) { 094 view.getViewHelperService().performComponentInitialization(view, model, addLineGroup); 095 } 096 view.getViewHelperService().performComponentInitialization(view, model, lineGroupPrototype); 097 view.getViewHelperService().performComponentInitialization(view, model, subCollectionFieldGroupPrototype); 098 view.getViewHelperService().performComponentInitialization(view, model, selectFieldPrototype); 099 } 100 101 /** 102 * The following actions are performed: 103 * 104 * <ul> 105 * <li>If wrapper group is specified, places the stacked groups into the wrapper</li> 106 * </ul> 107 * 108 * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performApplyModel(org.kuali.rice.krad.uif.view.View, 109 * java.lang.Object, org.kuali.rice.krad.uif.container.Container) 110 */ 111 @Override 112 public void performApplyModel(View view, Object model, Container container) { 113 super.performApplyModel(view, model, container); 114 115 if (wrapperGroup != null) { 116 wrapperGroup.setItems(stackedGroups); 117 } 118 } 119 120 /** 121 * Builds a <code>Group</code> instance for a collection line. The group is 122 * built by first creating a copy of the configured prototype. Then the 123 * header for the group is created using the configured summary fields on 124 * the <code>CollectionGroup</code>. The line fields passed in are set as 125 * the items for the group, and finally the actions are placed into the 126 * group footer 127 * 128 * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#buildLine(org.kuali.rice.krad.uif.view.View, 129 * java.lang.Object, org.kuali.rice.krad.uif.container.CollectionGroup, 130 * java.util.List, java.util.List, java.lang.String, java.util.List, 131 * java.lang.String, java.lang.Object, int) 132 */ 133 public void buildLine(View view, Object model, CollectionGroup collectionGroup, List<Field> lineFields, 134 List<FieldGroup> subCollectionFields, String bindingPath, List<ActionField> actions, String idSuffix, 135 Object currentLine, int lineIndex) { 136 boolean isAddLine = lineIndex == -1; 137 138 // construct new group 139 Group lineGroup = null; 140 if (isAddLine) { 141 stackedGroups = new ArrayList<Group>(); 142 143 if (addLineGroup == null) { 144 lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix); 145 } else { 146 lineGroup = ComponentUtils.copy(getAddLineGroup(), idSuffix); 147 } 148 } else { 149 lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix); 150 } 151 152 ComponentUtils.updateContextForLine(lineGroup, currentLine, lineIndex); 153 154 // build header text for group 155 String headerText = ""; 156 if (isAddLine) { 157 headerText = collectionGroup.getAddLineLabel(); 158 } else { 159 // get the collection for this group from the model 160 List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model, 161 ((DataBinding) collectionGroup).getBindingInfo().getBindingPath()); 162 163 headerText = buildLineHeaderText(modelCollection.get(lineIndex), lineGroup); 164 } 165 166 // don't set header if text is blank (could already be set by other means) 167 if (StringUtils.isNotBlank(headerText) && lineGroup.getHeader() != null) { 168 lineGroup.getHeader().setHeaderText(headerText); 169 } 170 171 // stack all fields (including sub-collections) for the group 172 List<Field> groupFields = new ArrayList<Field>(); 173 groupFields.addAll(lineFields); 174 groupFields.addAll(subCollectionFields); 175 176 lineGroup.setItems(groupFields); 177 178 // set line actions on group footer 179 if (collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly() && (lineGroup.getFooter() != null)) { 180 lineGroup.getFooter().setItems(actions); 181 } 182 183 stackedGroups.add(lineGroup); 184 } 185 186 /** 187 * Builds the header text for the collection line 188 * 189 * <p> 190 * Header text is built up by first the collection label, either specified 191 * on the collection definition or retrieved from the dictionary. Then for 192 * each summary field defined, the value from the model is retrieved and 193 * added to the header. 194 * </p> 195 * 196 * <p> 197 * Note the {@link #getSummaryTitle()} field may have expressions defined, in which cause it will be copied to the 198 * property expressions map to set the title for the line group (which will have the item context variable set) 199 * </p> 200 * 201 * @param line - Collection line containing data 202 * @param lineGroup - Group instance for rendering the line and whose title should be built 203 * @return String header text for line 204 */ 205 protected String buildLineHeaderText(Object line, Group lineGroup) { 206 // check for expression on summary title 207 if (KRADServiceLocatorWeb.getExpressionEvaluatorService().containsElPlaceholder(summaryTitle)) { 208 lineGroup.getPropertyExpressions().put("title", summaryTitle); 209 return null; 210 } 211 212 // build up line summary from declared field values and fixed title 213 String summaryFieldString = ""; 214 for (String summaryField : summaryFields) { 215 Object summaryFieldValue = ObjectPropertyUtils.getPropertyValue(line, summaryField); 216 if (StringUtils.isNotBlank(summaryFieldString)) { 217 summaryFieldString += " - "; 218 } 219 220 if (summaryFieldValue != null) { 221 summaryFieldString += summaryFieldValue; 222 } else { 223 summaryFieldString += "Null"; 224 } 225 } 226 227 String headerText = summaryTitle; 228 if (StringUtils.isNotBlank(summaryFieldString)) { 229 headerText += " ( " + summaryFieldString + " )"; 230 } 231 232 return headerText; 233 } 234 235 /** 236 * @see org.kuali.rice.krad.uif.layout.ContainerAware#getSupportedContainer() 237 */ 238 @Override 239 public Class<? extends Container> getSupportedContainer() { 240 return CollectionGroup.class; 241 } 242 243 /** 244 * @see org.kuali.rice.krad.uif.layout.LayoutManagerBase#getComponentsForLifecycle() 245 */ 246 @Override 247 public List<Component> getComponentsForLifecycle() { 248 List<Component> components = super.getComponentsForLifecycle(); 249 250 if (wrapperGroup != null) { 251 components.add(wrapperGroup); 252 } else { 253 components.addAll(stackedGroups); 254 } 255 256 return components; 257 } 258 259 /** 260 * @see org.kuali.rice.krad.uif.layout.LayoutManager#getComponentPrototypes() 261 */ 262 @Override 263 public List<Component> getComponentPrototypes() { 264 List<Component> components = super.getComponentPrototypes(); 265 266 components.add(addLineGroup); 267 components.add(lineGroupPrototype); 268 components.add(subCollectionFieldGroupPrototype); 269 components.add(selectFieldPrototype); 270 271 return components; 272 } 273 274 /** 275 * Text to appears in the header for each collection lines Group. Used in 276 * conjunction with {@link #getSummaryFields()} to build up the final header 277 * text 278 * 279 * @return String summary title text 280 */ 281 public String getSummaryTitle() { 282 return this.summaryTitle; 283 } 284 285 /** 286 * Setter for the summary title text 287 * 288 * @param summaryTitle 289 */ 290 public void setSummaryTitle(String summaryTitle) { 291 this.summaryTitle = summaryTitle; 292 } 293 294 /** 295 * List of attribute names from the collection line class that should be 296 * used to build the line summary. To build the summary the value for each 297 * attribute is retrieved from the line instance. All the values are then 298 * placed together with a separator. 299 * 300 * @return List<String> summary field names 301 * @see #buildLineHeaderText(java.lang.Object) 302 */ 303 public List<String> getSummaryFields() { 304 return this.summaryFields; 305 } 306 307 /** 308 * Setter for the summary field name list 309 * 310 * @param summaryFields 311 */ 312 public void setSummaryFields(List<String> summaryFields) { 313 this.summaryFields = summaryFields; 314 } 315 316 /** 317 * Group instance that will be used for the add line 318 * 319 * <p> 320 * Add line fields and actions configured on the 321 * <code>CollectionGroup</code> will be set onto the add line group (if add 322 * line is enabled). If the add line group is not configured, a new instance 323 * of the line group prototype will be used for the add line. 324 * </p> 325 * 326 * @return Group add line group instance 327 * @see #getAddLineGroup() 328 */ 329 public Group getAddLineGroup() { 330 return this.addLineGroup; 331 } 332 333 /** 334 * Setter for the add line group 335 * 336 * @param addLineGroup 337 */ 338 public void setAddLineGroup(Group addLineGroup) { 339 this.addLineGroup = addLineGroup; 340 } 341 342 /** 343 * Group instance that is used as a prototype for creating the collection 344 * line groups. For each line a copy of the prototype is made and then 345 * adjusted as necessary 346 * 347 * @return Group instance to use as prototype 348 */ 349 public Group getLineGroupPrototype() { 350 return this.lineGroupPrototype; 351 } 352 353 /** 354 * Setter for the line group prototype 355 * 356 * @param lineGroupPrototype 357 */ 358 public void setLineGroupPrototype(Group lineGroupPrototype) { 359 this.lineGroupPrototype = lineGroupPrototype; 360 } 361 362 /** 363 * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#getSubCollectionFieldGroupPrototype() 364 */ 365 public FieldGroup getSubCollectionFieldGroupPrototype() { 366 return this.subCollectionFieldGroupPrototype; 367 } 368 369 /** 370 * Setter for the sub-collection field group prototype 371 * 372 * @param subCollectionFieldGroupPrototype 373 */ 374 public void setSubCollectionFieldGroupPrototype(FieldGroup subCollectionFieldGroupPrototype) { 375 this.subCollectionFieldGroupPrototype = subCollectionFieldGroupPrototype; 376 } 377 378 /** 379 * Field instance that serves as a prototype for creating the select field on each line when 380 * {@link org.kuali.rice.krad.uif.container.CollectionGroup#isRenderSelectField()} is true 381 * 382 * <p> 383 * This prototype can be used to set the control used for the select field (generally will be a checkbox control) 384 * in addition to styling and other setting. The binding path will be formed with using the 385 * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getSelectPropertyName()} or if not set the framework 386 * will use {@link org.kuali.rice.krad.web.form.UifFormBase#getSelectedCollectionLines()} 387 * </p> 388 * 389 * @return Field select field prototype instance 390 */ 391 public Field getSelectFieldPrototype() { 392 return selectFieldPrototype; 393 } 394 395 /** 396 * Setter for the prototype instance for select fields 397 * 398 * @param selectFieldPrototype 399 */ 400 public void setSelectFieldPrototype(Field selectFieldPrototype) { 401 this.selectFieldPrototype = selectFieldPrototype; 402 } 403 404 /** 405 * Group that will 'wrap' the generated collection lines so that they have a different layout from the general 406 * stacked layout 407 * 408 * <p> 409 * By default (when the wrapper group is null), each collection line will become a group and the groups are 410 * rendered one after another. If the wrapper group is configured, the generated groups will be inserted as the 411 * items for the wrapper group, and the layout manager configured for the wrapper group will determine how they 412 * are rendered. For example, the layout manager could be a grid layout configured for three columns, which would 413 * layout the first three lines horizontally then break to a new row. 414 * </p> 415 * 416 * @return Group instance whose items list should be populated with the generated groups, or null to use the 417 * default layout 418 */ 419 public Group getWrapperGroup() { 420 return wrapperGroup; 421 } 422 423 /** 424 * Setter for the wrapper group that will receive the generated line groups 425 * 426 * @param wrapperGroup 427 */ 428 public void setWrapperGroup(Group wrapperGroup) { 429 this.wrapperGroup = wrapperGroup; 430 } 431 432 /** 433 * Final <code>List</code> of Groups to render for the collection 434 * 435 * @return List<Group> collection groups 436 */ 437 public List<Group> getStackedGroups() { 438 return this.stackedGroups; 439 } 440 441 /** 442 * Setter for the collection groups 443 * 444 * @param stackedGroups 445 */ 446 public void setStackedGroups(List<Group> stackedGroups) { 447 this.stackedGroups = stackedGroups; 448 } 449 450}