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.layout; 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.component.DataBinding; 026import org.kuali.rice.krad.uif.component.KeepExpression; 027import org.kuali.rice.krad.uif.container.CollectionGroup; 028import org.kuali.rice.krad.uif.container.Container; 029import org.kuali.rice.krad.uif.container.DialogGroup; 030import org.kuali.rice.krad.uif.container.Group; 031import org.kuali.rice.krad.uif.container.collections.LineBuilderContext; 032import org.kuali.rice.krad.uif.element.Action; 033import org.kuali.rice.krad.uif.element.Message; 034import org.kuali.rice.krad.uif.field.Field; 035import org.kuali.rice.krad.uif.layout.collections.CollectionLayoutManagerBase; 036import org.kuali.rice.krad.uif.layout.collections.CollectionPagingHelper; 037import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 038import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleRestriction; 039import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils; 040import org.kuali.rice.krad.uif.util.ComponentFactory; 041import org.kuali.rice.krad.uif.util.ComponentUtils; 042import org.kuali.rice.krad.uif.util.ContextUtils; 043import org.kuali.rice.krad.uif.util.LifecycleElement; 044import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 045import org.kuali.rice.krad.uif.view.ExpressionEvaluator; 046import org.kuali.rice.krad.uif.view.View; 047import org.kuali.rice.krad.uif.view.ViewModel; 048import org.kuali.rice.krad.util.KRADUtils; 049import org.kuali.rice.krad.web.form.UifFormBase; 050 051import java.util.ArrayList; 052import java.util.HashMap; 053import java.util.List; 054import java.util.Map; 055 056/** 057 * Layout manager that works with {@code CollectionGroup} containers and 058 * renders the collection lines in a vertical row 059 * 060 * <p> 061 * For each line of the collection, a {@code Group} instance is created. 062 * The group header contains a label for the line (summary information), the 063 * group fields are the collection line fields, and the group footer contains 064 * the line actions. All the groups are rendered using the 065 * {@code BoxLayoutManager} with vertical orientation. 066 * </p> 067 * 068 * <p> 069 * Modify the lineGroupPrototype to change header/footer styles or any other 070 * customization for the line groups 071 * </p> 072 * 073 * @author Kuali Rice Team (rice.collab@kuali.org) 074 */ 075@BeanTags({@BeanTag(name = "stackedCollectionLayout-bean", parent = "Uif-StackedCollectionLayoutBase"), 076 @BeanTag(name = "stackedCollectionLayout-withGridItems-bean", 077 parent = "Uif-StackedCollectionLayout-WithGridItems"), 078 @BeanTag(name = "stackedCollectionLayout-withBoxItems-bean", 079 parent = "Uif-StackedCollectionLayout-WithBoxItems"), 080 @BeanTag(name = "stackedCollectionLayout-list-bean", parent = "Uif-StackedCollectionLayout-List")}) 081public class StackedLayoutManagerBase extends CollectionLayoutManagerBase implements StackedLayoutManager { 082 private static final long serialVersionUID = 4602368505430238846L; 083 084 @KeepExpression 085 private String summaryTitle; 086 private List<String> summaryFields; 087 088 private Group lineGroupPrototype; 089 private Group wrapperGroup; 090 091 private List<Group> stackedGroups; 092 093 private boolean renderLineActionsInLineGroup; 094 private boolean renderLineActionsInHeader; 095 096 public StackedLayoutManagerBase() { 097 super(); 098 099 summaryFields = new ArrayList<String>(); 100 stackedGroups = new ArrayList<Group>(); 101 } 102 103 /** 104 * {@inheritDoc} 105 */ 106 @Override 107 public void performInitialization(Object model) { 108 super.performInitialization(model); 109 110 stackedGroups = new ArrayList<Group>(); 111 } 112 113 /** 114 * {@inheritDoc} 115 */ 116 @Override 117 public void performApplyModel(Object model, LifecycleElement component) { 118 super.performApplyModel(model, component); 119 120 if (wrapperGroup != null) { 121 wrapperGroup.setItems(stackedGroups); 122 } 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 @Override 129 public void performFinalize(Object model, LifecycleElement element) { 130 super.performFinalize(model, element); 131 132 boolean serverPagingEnabled = 133 (element instanceof CollectionGroup) && ((CollectionGroup) element).isUseServerPaging(); 134 135 // set the appropriate page, total pages, and link script into the Pager 136 if (serverPagingEnabled && this.getPagerWidget() != null) { 137 CollectionLayoutUtils.setupPagerWidget(getPagerWidget(), (CollectionGroup) element, model); 138 } 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override 145 public void buildLine(LineBuilderContext lineBuilderContext) { 146 View view = ViewLifecycle.getView(); 147 148 List<Field> lineFields = lineBuilderContext.getLineFields(); 149 CollectionGroup collectionGroup = lineBuilderContext.getCollectionGroup(); 150 int lineIndex = lineBuilderContext.getLineIndex(); 151 String idSuffix = lineBuilderContext.getIdSuffix(); 152 Object currentLine = lineBuilderContext.getCurrentLine(); 153 154 String bindingPath = lineBuilderContext.getBindingPath(); 155 156 Map<String, Object> lineContext = new HashMap<String, Object>(); 157 lineContext.putAll(this.getContext()); 158 lineContext.put(UifConstants.ContextVariableNames.LINE, currentLine); 159 lineContext.put(UifConstants.ContextVariableNames.MANAGER, this); 160 lineContext.put(UifConstants.ContextVariableNames.VIEW, view); 161 lineContext.put(UifConstants.ContextVariableNames.LINE_SUFFIX, idSuffix); 162 lineContext.put(UifConstants.ContextVariableNames.INDEX, Integer.valueOf(lineIndex)); 163 lineContext.put(UifConstants.ContextVariableNames.COLLECTION_GROUP, collectionGroup); 164 lineContext.put(UifConstants.ContextVariableNames.IS_ADD_LINE, lineBuilderContext.isAddLine()); 165 lineContext.put(UifConstants.ContextVariableNames.READONLY_LINE, Boolean.TRUE.equals(collectionGroup.getReadOnly())); 166 lineContext.put(UifConstants.ContextVariableNames.PARENT_LINE, currentLine); 167 168 ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator(); 169 170 // construct new group 171 Group lineGroup = null; 172 if (lineBuilderContext.isAddLine()) { 173 stackedGroups = new ArrayList<Group>(); 174 175 if (getAddLineGroup() == null) { 176 lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix); 177 } else { 178 lineGroup = ComponentUtils.copy(getAddLineGroup(), idSuffix); 179 lineGroup.addStyleClass(collectionGroup.getAddItemCssClass()); 180 } 181 182 // add line enter key action 183 addEnterKeyDataAttributeToGroup(lineGroup, lineContext, expressionEvaluator, 184 collectionGroup.getAddLineEnterKeyAction()); 185 } else { 186 lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix); 187 188 // existing line enter key action 189 addEnterKeyDataAttributeToGroup(lineGroup, lineContext, expressionEvaluator, 190 collectionGroup.getLineEnterKeyAction()); 191 } 192 193 if (((UifFormBase) lineBuilderContext.getModel()).isAddedCollectionItem(currentLine)) { 194 lineGroup.addStyleClass(collectionGroup.getNewItemsCssClass()); 195 } 196 197 // any actions that are attached to the group prototype (like the header) need to get action parameters 198 // and context set for the collection line 199 List<Action> lineGroupActions = ViewLifecycleUtils.getElementsOfTypeDeep(lineGroup, Action.class); 200 if (lineGroupActions != null) { 201 collectionGroup.getCollectionGroupBuilder().initializeActions(lineGroupActions, collectionGroup, lineIndex); 202 ContextUtils.updateContextsForLine(lineGroupActions, collectionGroup, currentLine, lineIndex, idSuffix); 203 } 204 205 ContextUtils.updateContextForLine(lineGroup, collectionGroup, currentLine, lineIndex, idSuffix); 206 207 // build header for the group 208 if (lineBuilderContext.isAddLine()) { 209 if (lineGroup.getHeader() != null && StringUtils.isNotBlank(lineGroup.getHeaderText())) { 210 Message headerMessage = ComponentUtils.copy(collectionGroup.getAddLineLabel()); 211 headerMessage.setMessageText(lineGroup.getHeaderText()); 212 } 213 } else { 214 // get the collection for this group from the model 215 List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(lineBuilderContext.getModel(), 216 ((DataBinding) collectionGroup).getBindingInfo().getBindingPath()); 217 218 String headerText = buildLineHeaderText(modelCollection.get(lineIndex), lineGroup); 219 220 // don't set header if text is blank (could already be set by other means) 221 if (StringUtils.isNotBlank(headerText) && lineGroup.getHeader() != null) { 222 lineGroup.getHeader().setHeaderText(headerText); 223 } 224 } 225 226 // stack all fields (including sub-collections) for the group 227 List<Component> groupFields = new ArrayList<Component>(); 228 groupFields.addAll(lineFields); 229 230 if (lineBuilderContext.getSubCollectionFields() != null) { 231 groupFields.addAll(lineBuilderContext.getSubCollectionFields()); 232 } 233 234 // Place actions in the appropriate location for the stacked group line 235 determineLineActionPlacement(lineGroup, collectionGroup, lineBuilderContext, groupFields); 236 237 lineGroup.setItems(groupFields); 238 239 // add items to add line group 240 if (lineBuilderContext.isAddLine()) { 241 if (getAddLineGroup() != null) { 242 getAddLineGroup().setItems(lineGroup.getItems()); 243 } 244 } 245 246 // Must evaluate the client-side state on the lineGroup's disclosure for PlaceholderDisclosureGroup processing 247 if (lineBuilderContext.getModel() instanceof ViewModel) { 248 KRADUtils.syncClientSideStateForComponent(lineGroup.getDisclosure(), 249 ((ViewModel) lineBuilderContext.getModel()).getClientStateForSyncing()); 250 } 251 252 // don't add to stackedGroups else will get double set of dialog boxes 253 // see FreeMarkerInlineRenderUtils.renderCollectionGroup near end where renders add line dialog 254 if (lineGroup instanceof DialogGroup == false) { 255 stackedGroups.add(lineGroup); 256 } 257 258 // we need to add the parent line for each of the items in the group 259 ContextUtils.pushObjectToContextDeep(lineGroup.getItems(), UifConstants.ContextVariableNames.PARENT_LINE, 260 lineBuilderContext.getCurrentLine()); 261 } 262 263 /** 264 * Places actions in the appropriate location for the stacked group line based on placement 265 * flags set on this layout manager 266 * 267 * @param lineGroup the current line group 268 * @param collectionGroup the current collection group 269 * @param lineBuilderContext the line's building context 270 * @param groupFields the list of fields which will be added to the line group 271 */ 272 protected void determineLineActionPlacement(Group lineGroup, CollectionGroup collectionGroup, 273 LineBuilderContext lineBuilderContext, List<Component> groupFields) { 274 List<? extends Component> actions = lineBuilderContext.getLineActions(); 275 276 boolean showActions = collectionGroup.isRenderLineActions() && !Boolean.TRUE.equals(collectionGroup.getReadOnly()); 277 if (!showActions) { 278 return; 279 } 280 281 if (renderLineActionsInHeader && lineGroup.getHeader() != null && !lineBuilderContext.isAddLine()) { 282 // add line actions to header when the option is true 283 Group headerGroup = lineGroup.getHeader().getRightGroup(); 284 285 if (headerGroup == null) { 286 headerGroup = ComponentFactory.getHorizontalBoxGroup(); 287 } 288 289 List<Component> items = new ArrayList<Component>(); 290 items.addAll(headerGroup.getItems()); 291 items.addAll(actions); 292 293 headerGroup.setItems(items); 294 lineGroup.getHeader().setRightGroup(headerGroup); 295 } else if (isRenderLineActionsInLineGroup()) { 296 // add the actions to the line group if isRenderLineActionsInLineGroup flag is true 297 groupFields.addAll(actions); 298 lineGroup.setRenderFooter(false); 299 } else if ((lineGroup.getFooter() != null) && ((lineGroup.getFooter().getItems() == null) || lineGroup 300 .getFooter().getItems().isEmpty())) { 301 // add to footer in the default case 302 lineGroup.getFooter().setItems(actions); 303 } 304 } 305 306 /** 307 * Builds the header text for the collection line 308 * 309 * <p> 310 * Header text is built up by first the collection label, either specified 311 * on the collection definition or retrieved from the dictionary. Then for 312 * each summary field defined, the value from the model is retrieved and 313 * added to the header. 314 * </p> 315 * 316 * <p> 317 * Note the {@link #getSummaryTitle()} field may have expressions defined, in which cause it will be copied to the 318 * property expressions map to set the title for the line group (which will have the item context variable set) 319 * </p> 320 * 321 * @param line Collection line containing data 322 * @param lineGroup Group instance for rendering the line and whose title should be built 323 * @return header text for line 324 */ 325 protected String buildLineHeaderText(Object line, Group lineGroup) { 326 // check for expression on summary title 327 if (ViewLifecycle.getExpressionEvaluator().containsElPlaceholder(summaryTitle)) { 328 lineGroup.getPropertyExpressions().put(UifPropertyPaths.HEADER_TEXT, summaryTitle); 329 return null; 330 } 331 332 // build up line summary from declared field values and fixed title 333 String summaryFieldString = ""; 334 for (String summaryField : summaryFields) { 335 Object summaryFieldValue = ObjectPropertyUtils.getPropertyValue(line, summaryField); 336 if (StringUtils.isNotBlank(summaryFieldString)) { 337 summaryFieldString += " - "; 338 } 339 340 if (summaryFieldValue != null) { 341 summaryFieldString += summaryFieldValue; 342 } else { 343 summaryFieldString += "Null"; 344 } 345 } 346 347 String headerText = summaryTitle; 348 if (StringUtils.isNotBlank(summaryFieldString)) { 349 headerText += " ( " + summaryFieldString + " )"; 350 } 351 352 return headerText; 353 } 354 355 /** 356 * Invokes {@link org.kuali.rice.krad.uif.layout.collections.CollectionPagingHelper} to carry out the 357 * paging request. 358 * 359 * {@inheritDoc} 360 */ 361 @Override 362 public void processPagingRequest(Object model, CollectionGroup collectionGroup) { 363 String pageNumber = ViewLifecycle.getRequest().getParameter(UifConstants.PageRequest.PAGE_NUMBER); 364 365 CollectionPagingHelper pagingHelper = new CollectionPagingHelper(); 366 pagingHelper.processPagingRequest(ViewLifecycle.getView(), collectionGroup, (UifFormBase) model, pageNumber); 367 } 368 369 /** 370 * Returns the parent {@link org.kuali.rice.krad.uif.layout.collections.CollectionLayoutManagerBase}'s add line group 371 * 372 * <p> 373 * This method is overridden to restrict the lifecycle of the add line group as a resolution to avoid duplicate 374 * components from being added to the view, for example, quickfinders. 375 * </p> 376 * 377 * {@inheritDoc} 378 */ 379 @Override 380 @BeanTagAttribute 381 @ViewLifecycleRestriction(UifConstants.ViewPhases.INITIALIZE) 382 public Group getAddLineGroup() { 383 return super.getAddLineGroup(); 384 } 385 386 /** 387 * {@inheritDoc} 388 */ 389 @Override 390 public Class<? extends Container> getSupportedContainer() { 391 return CollectionGroup.class; 392 } 393 394 /** 395 * {@inheritDoc} 396 */ 397 @Override 398 @BeanTagAttribute 399 public String getSummaryTitle() { 400 return this.summaryTitle; 401 } 402 403 /** 404 * {@inheritDoc} 405 */ 406 @Override 407 public void setSummaryTitle(String summaryTitle) { 408 this.summaryTitle = summaryTitle; 409 } 410 411 /** 412 * {@inheritDoc} 413 */ 414 @Override 415 @BeanTagAttribute 416 public List<String> getSummaryFields() { 417 return this.summaryFields; 418 } 419 420 /** 421 * {@inheritDoc} 422 */ 423 @Override 424 public void setSummaryFields(List<String> summaryFields) { 425 this.summaryFields = summaryFields; 426 } 427 428 /** 429 * {@inheritDoc} 430 */ 431 @Override 432 @ViewLifecycleRestriction(UifConstants.ViewPhases.INITIALIZE) 433 @BeanTagAttribute 434 public Group getLineGroupPrototype() { 435 return this.lineGroupPrototype; 436 } 437 438 /** 439 * {@inheritDoc} 440 */ 441 @Override 442 public void setLineGroupPrototype(Group lineGroupPrototype) { 443 this.lineGroupPrototype = lineGroupPrototype; 444 } 445 446 /** 447 * {@inheritDoc} 448 */ 449 @Override 450 @BeanTagAttribute 451 public Group getWrapperGroup() { 452 return wrapperGroup; 453 } 454 455 /** 456 * {@inheritDoc} 457 */ 458 @Override 459 public void setWrapperGroup(Group wrapperGroup) { 460 this.wrapperGroup = wrapperGroup; 461 } 462 463 /** 464 * {@inheritDoc} 465 */ 466 @Override 467 @ViewLifecycleRestriction 468 @BeanTagAttribute 469 public List<Group> getStackedGroups() { 470 return this.stackedGroups; 471 } 472 473 /** 474 * {@inheritDoc} 475 */ 476 @Override 477 public List<Group> getStackedGroupsNoWrapper() { 478 return wrapperGroup != null ? null : this.stackedGroups; 479 } 480 481 /** 482 * {@inheritDoc} 483 */ 484 @Override 485 public void setStackedGroups(List<Group> stackedGroups) { 486 this.stackedGroups = stackedGroups; 487 } 488 489 /** 490 * {@inheritDoc} 491 */ 492 @Override 493 @BeanTagAttribute 494 public boolean isRenderLineActionsInLineGroup() { 495 return renderLineActionsInLineGroup; 496 } 497 498 /** 499 * {@inheritDoc} 500 */ 501 @Override 502 public void setRenderLineActionsInLineGroup(boolean renderLineActionsInLineGroup) { 503 this.renderLineActionsInLineGroup = renderLineActionsInLineGroup; 504 } 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override 510 @BeanTagAttribute 511 public boolean isRenderLineActionsInHeader() { 512 return renderLineActionsInHeader; 513 } 514 515 /** 516 * {@inheritDoc} 517 */ 518 @Override 519 public void setRenderLineActionsInHeader(boolean renderLineActionsInHeader) { 520 this.renderLineActionsInHeader = renderLineActionsInHeader; 521 } 522}