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.service.impl; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.core.api.exception.RiceRuntimeException; 020import org.kuali.rice.kim.api.identity.Person; 021import org.kuali.rice.krad.bo.ExternalizableBusinessObject; 022import org.kuali.rice.krad.datadictionary.AttributeDefinition; 023import org.kuali.rice.krad.inquiry.Inquirable; 024import org.kuali.rice.krad.service.DataDictionaryService; 025import org.kuali.rice.krad.service.KRADServiceLocator; 026import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 027import org.kuali.rice.krad.service.ModuleService; 028import org.kuali.rice.krad.uif.UifConstants; 029import org.kuali.rice.krad.uif.component.ComponentSecurity; 030import org.kuali.rice.krad.uif.container.Group; 031import org.kuali.rice.krad.uif.field.ActionField; 032import org.kuali.rice.krad.uif.field.FieldGroup; 033import org.kuali.rice.krad.uif.util.ViewCleaner; 034import org.kuali.rice.krad.uif.view.ViewAuthorizer; 035import org.kuali.rice.krad.uif.view.ViewPresentationController; 036import org.kuali.rice.krad.uif.component.BindingInfo; 037import org.kuali.rice.krad.uif.component.ClientSideState; 038import org.kuali.rice.krad.uif.component.Component; 039import org.kuali.rice.krad.uif.component.DataBinding; 040import org.kuali.rice.krad.uif.component.PropertyReplacer; 041import org.kuali.rice.krad.uif.component.RequestParameter; 042import org.kuali.rice.krad.uif.container.CollectionGroup; 043import org.kuali.rice.krad.uif.container.Container; 044import org.kuali.rice.krad.uif.control.Control; 045import org.kuali.rice.krad.uif.field.DataField; 046import org.kuali.rice.krad.uif.field.InputField; 047import org.kuali.rice.krad.uif.field.Field; 048import org.kuali.rice.krad.uif.field.RemoteFieldsHolder; 049import org.kuali.rice.krad.uif.layout.LayoutManager; 050import org.kuali.rice.krad.uif.modifier.ComponentModifier; 051import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService; 052import org.kuali.rice.krad.uif.service.ViewDictionaryService; 053import org.kuali.rice.krad.uif.service.ViewHelperService; 054import org.kuali.rice.krad.uif.util.BooleanMap; 055import org.kuali.rice.krad.uif.util.CloneUtils; 056import org.kuali.rice.krad.uif.util.ComponentFactory; 057import org.kuali.rice.krad.uif.util.ComponentUtils; 058import org.kuali.rice.krad.uif.util.ExpressionUtils; 059import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 060import org.kuali.rice.krad.uif.util.ScriptUtils; 061import org.kuali.rice.krad.uif.util.ViewModelUtils; 062import org.kuali.rice.krad.uif.view.View; 063import org.kuali.rice.krad.uif.view.ViewModel; 064import org.kuali.rice.krad.uif.widget.Inquiry; 065import org.kuali.rice.krad.uif.widget.Widget; 066import org.kuali.rice.krad.util.GlobalVariables; 067import org.kuali.rice.krad.util.KRADConstants; 068import org.kuali.rice.krad.util.ObjectUtils; 069import org.kuali.rice.krad.valuefinder.ValueFinder; 070import org.kuali.rice.krad.web.form.UifFormBase; 071import org.springframework.util.ClassUtils; 072import org.springframework.util.MethodInvoker; 073 074import java.io.Serializable; 075import java.lang.annotation.Annotation; 076import java.util.ArrayList; 077import java.util.Collection; 078import java.util.Collections; 079import java.util.HashMap; 080import java.util.HashSet; 081import java.util.List; 082import java.util.Map; 083import java.util.Map.Entry; 084import java.util.Set; 085 086/** 087 * Default Implementation of <code>ViewHelperService</code> 088 * 089 * @author Kuali Rice Team (rice.collab@kuali.org) 090 */ 091public class ViewHelperServiceImpl implements ViewHelperService, Serializable { 092 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ViewHelperServiceImpl.class); 093 094 private transient DataDictionaryService dataDictionaryService; 095 private transient ExpressionEvaluatorService expressionEvaluatorService; 096 private transient ViewDictionaryService viewDictionaryService; 097 098 /** 099 * Uses reflection to find all fields defined on the <code>View</code> instance that have 100 * the <code>RequestParameter</code> annotation (which indicates the field may be populated by the request). 101 * 102 * <p> 103 * For each field found, if there is a corresponding key/value pair in the request parameters, 104 * the value is used to populate the field. In addition, any conditional properties of 105 * <code>PropertyReplacers</code> configured for the field are cleared so that the request parameter 106 * value does not get overridden by the dictionary conditional logic 107 * </p> 108 * 109 * @see org.kuali.rice.krad.uif.service.ViewHelperService#populateViewFromRequestParameters(org.kuali.rice.krad.uif.view.View, 110 * java.util.Map) 111 */ 112 @Override 113 public void populateViewFromRequestParameters(View view, Map<String, String> parameters) { 114 // build Map of property replacers by property name so that we can remove them 115 // if the property was set by a request parameter 116 Map<String, Set<PropertyReplacer>> viewPropertyReplacers = new HashMap<String, Set<PropertyReplacer>>(); 117 for (PropertyReplacer replacer : view.getPropertyReplacers()) { 118 Set<PropertyReplacer> propertyReplacers = new HashSet<PropertyReplacer>(); 119 if (viewPropertyReplacers.containsKey(replacer.getPropertyName())) { 120 propertyReplacers = viewPropertyReplacers.get(replacer.getPropertyName()); 121 } 122 propertyReplacers.add(replacer); 123 124 viewPropertyReplacers.put(replacer.getPropertyName(), propertyReplacers); 125 } 126 127 Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(view.getClass(), 128 RequestParameter.class); 129 130 // for each request parameter allowed on the view, if the request contains a value use 131 // to set on View, and clear and conditional expressions or property replacers for that field 132 Map<String, String> viewRequestParameters = new HashMap<String, String>(); 133 for (String fieldToPopulate : annotatedFields.keySet()) { 134 RequestParameter requestParameter = (RequestParameter) annotatedFields.get(fieldToPopulate); 135 136 // use specified parameter name if given, else use field name to retrieve parameter value 137 String requestParameterName = requestParameter.parameterName(); 138 if (StringUtils.isBlank(requestParameterName)) { 139 requestParameterName = fieldToPopulate; 140 } 141 142 if (!parameters.containsKey(requestParameterName)) { 143 continue; 144 } 145 146 String fieldValue = parameters.get(requestParameterName); 147 if (StringUtils.isNotBlank(fieldValue)) { 148 viewRequestParameters.put(requestParameterName, fieldValue); 149 ObjectPropertyUtils.setPropertyValue(view, fieldToPopulate, fieldValue); 150 151 // remove any conditional configuration so value is not 152 // overridden later during the apply model phase 153 if (view.getPropertyExpressions().containsKey(fieldToPopulate)) { 154 view.getPropertyExpressions().remove(fieldToPopulate); 155 } 156 157 if (viewPropertyReplacers.containsKey(fieldToPopulate)) { 158 Set<PropertyReplacer> propertyReplacers = viewPropertyReplacers.get(fieldToPopulate); 159 for (PropertyReplacer replacer : propertyReplacers) { 160 view.getPropertyReplacers().remove(replacer); 161 } 162 } 163 } 164 } 165 166 view.setViewRequestParameters(viewRequestParameters); 167 } 168 169 /** 170 * @see org.kuali.rice.krad.uif.service.ViewHelperService#performInitialization(org.kuali.rice.krad.uif.view.View, 171 * java.lang.Object) 172 */ 173 @Override 174 public void performInitialization(View view, Object model) { 175 view.assignComponentIds(view); 176 performComponentInitialization(view, model, view); 177 } 178 179 /** 180 * Performs the complete component lifecycle on the component passed in, in this order: 181 * performComponentInitialization, performComponentApplyModel, and performComponentFinalize. 182 * 183 * @see {@link org.kuali.rice.krad.uif.service.ViewHelperService#performComponentLifecycle( 184 *org.kuali.rice.krad.uif.view.View, java.lang.Object, org.kuali.rice.krad.uif.component.Component, 185 * java.lang.String) 186 * @see {@link #performComponentInitialization(org.kuali.rice.krad.uif.view.View, Object, 187 * org.kuali.rice.krad.uif.component.Component)} 188 * @see {@link #performComponentApplyModel(View, Component, Object)} 189 * @see {@link #performComponentFinalize(View, Component, Object, Component, Map)} 190 */ 191 public void performComponentLifecycle(View view, Object model, Component component, String origId) { 192 Component origComponent = view.getViewIndex().getComponentById(origId); 193 194 // run through and assign any ids starting with the id for the refreshed component (this might be 195 // necessary if we are getting a new component instance from the bean factory) 196 Integer currentSequenceVal = view.getIdSequence(); 197 Integer startingSequenceVal = view.getViewIndex().getIdSequenceSnapshot().get(component.getId()); 198 view.setIdSequence(startingSequenceVal); 199 200 view.assignComponentIds(component); 201 202 // now set back from the ending view sequence so IDs for any dynamically created (newly) will not stomp 203 // on existing components 204 view.setIdSequence(currentSequenceVal); 205 206 Component parent = (Component) origComponent.getContext().get(UifConstants.ContextVariableNames.PARENT); 207 component.pushAllToContext(origComponent.getContext()); 208 209 // adjust IDs for suffixes that might have been added by a parent component during the full view lifecycle 210 String suffix = StringUtils.replaceOnce(origComponent.getId(), origComponent.getFactoryId(), ""); 211 212 // remove attribute suffix since that gets added in lifecycle 213 if (suffix.endsWith(UifConstants.IdSuffixes.ATTRIBUTE)) { 214 suffix = StringUtils.removeEnd(suffix, UifConstants.IdSuffixes.ATTRIBUTE); 215 } 216 ComponentUtils.updateIdWithSuffix(component, suffix); 217 218 // binding path should stay the same 219 if (component instanceof DataBinding) { 220 ((DataBinding) component).setBindingInfo(((DataBinding) origComponent).getBindingInfo()); 221 ((DataBinding) component).getBindingInfo().setBindingPath( 222 ((DataBinding) origComponent).getBindingInfo().getBindingPath()); 223 } 224 225 // copy properties that are set by parent components in the full view lifecycle 226 if (component instanceof Field) { 227 ((Field) component).setLabelFieldRendered(((Field) origComponent).isLabelFieldRendered()); 228 } else if (component instanceof CollectionGroup) { 229 ((CollectionGroup) component).setSubCollectionSuffix( 230 ((CollectionGroup) origComponent).getSubCollectionSuffix()); 231 } 232 233 if (origComponent.isRefreshedByAction()) { 234 component.setRefreshedByAction(true); 235 } 236 237 // reset data if needed 238 if (component.isResetDataOnRefresh()) { 239 // TODO: this should handle groups as well, going through nested data fields 240 if (component instanceof DataField) { 241 // TODO: should check default value 242 243 // clear value 244 ObjectPropertyUtils.initializeProperty(model, 245 ((DataField) component).getBindingInfo().getBindingPath()); 246 } 247 } 248 249 performComponentInitialization(view, model, component); 250 view.getViewIndex().indexComponent(component); 251 252 performComponentApplyModel(view, component, model); 253 view.getViewIndex().indexComponent(component); 254 255 // make sure id, binding, and label settings stay the same as initial 256 if (component instanceof Group || component instanceof FieldGroup) { 257 List<Component> nestedComponents = ComponentUtils.getAllNestedComponents(component); 258 for (Component nestedComponent : nestedComponents) { 259 Component origNestedComponent = null; 260 if (nestedComponent instanceof DataField) { 261 origNestedComponent = view.getViewIndex().getComponentById( 262 nestedComponent.getId() + suffix + UifConstants.IdSuffixes.ATTRIBUTE); 263 } else { 264 origNestedComponent = view.getViewIndex().getComponentById(nestedComponent.getId() + suffix); 265 } 266 267 if (origNestedComponent != null) { 268 // update binding 269 if (nestedComponent instanceof DataBinding) { 270 ((DataBinding) nestedComponent).setBindingInfo( 271 ((DataBinding) origNestedComponent).getBindingInfo()); 272 ((DataBinding) nestedComponent).getBindingInfo().setBindingPath( 273 ((DataBinding) origNestedComponent).getBindingInfo().getBindingPath()); 274 } 275 276 // update label rendered flag 277 if (nestedComponent instanceof Field) { 278 ((Field) nestedComponent).setLabelFieldRendered( 279 ((Field) origNestedComponent).isLabelFieldRendered()); 280 } 281 282 if (origNestedComponent.isRefreshedByAction()) { 283 nestedComponent.setRefreshedByAction(true); 284 } 285 286 // update id 287 ComponentUtils.updateIdWithSuffix(nestedComponent, suffix); 288 } 289 } 290 } 291 292 // TODO: need to handle updating client state for component refresh 293 Map<String, Object> clientState = new HashMap<String, Object>(); 294 performComponentFinalize(view, component, model, parent, clientState); 295 296 // get client state for component and build update script for on load 297 String clientStateScript = buildClientSideStateScript(view, clientState, true); 298 String onLoadScript = component.getOnLoadScript(); 299 if (StringUtils.isNotBlank(onLoadScript)) { 300 clientStateScript = onLoadScript + clientStateScript; 301 } 302 component.setOnLoadScript(clientStateScript); 303 304 view.getViewIndex().indexComponent(component); 305 } 306 307 /** 308 * Performs initialization of a component by these steps: 309 * 310 * <ul> 311 * <li>For <code>DataField</code> instances, set defaults from the data 312 * dictionary.</li> 313 * <li>Invoke the initialize method on the component. Here the component can 314 * setup defaults and do other initialization that is specific to that 315 * component.</li> 316 * <li>Invoke any configured <code>ComponentModifier</code> instances for 317 * the component.</li> 318 * <li>Call the component to get the List of components that are nested 319 * within and recursively call this method to initialize those components.</li> 320 * <li>Call custom initialize hook for service overrides</li> 321 * </ul> 322 * 323 * <p> 324 * Note the order various initialize points are called, this can sometimes 325 * be an important factor to consider when initializing a component 326 * </p> 327 * 328 * @throws RiceRuntimeException if the component id or factoryId is not specified 329 * 330 * @see org.kuali.rice.krad.uif.service.ViewHelperService#performComponentInitialization(org.kuali.rice.krad.uif.view.View, 331 * java.lang.Object, org.kuali.rice.krad.uif.component.Component) 332 */ 333 public void performComponentInitialization(View view, Object model, Component component) { 334 if (component == null) { 335 return; 336 } 337 338 if (StringUtils.isBlank(component.getId())) { 339 throw new RiceRuntimeException("Id is not set, this should not happen unless a component is misconfigured"); 340 } 341 342 // TODO: duplicate ID check 343 344 LOG.debug("Initializing component: " + component.getId() + " with type: " + component.getClass()); 345 346 // add initial state to the view index for component refreshes 347 if (!(component instanceof View)) { 348 view.getViewIndex().addInitialComponentStateIfNeeded(component); 349 } 350 351 // invoke component to initialize itself after properties have been set 352 component.performInitialization(view, model); 353 354 // for attribute fields, set defaults from dictionary entry 355 if (component instanceof DataField) { 356 initializeDataFieldFromDataDictionary(view, (DataField) component); 357 } 358 359 if (component instanceof Container) { 360 LayoutManager layoutManager = ((Container) component).getLayoutManager(); 361 362 // invoke hook point for adding components through code 363 addCustomContainerComponents(view, model, (Container) component); 364 365 // process any remote fields holder that might be in the containers items, collection items will get 366 // processed as the lines are being built 367 if (!(component instanceof CollectionGroup)) { 368 processAnyRemoteFieldsHolder(view, model, (Container) component); 369 } 370 } 371 372 // for collection groups set defaults from dictionary entry 373 if (component instanceof CollectionGroup) { 374 // TODO: initialize from dictionary 375 } 376 377 // invoke component modifiers setup to run in the initialize phase 378 runComponentModifiers(view, component, null, UifConstants.ViewPhases.INITIALIZE); 379 380 // initialize nested components 381 for (Component nestedComponent : component.getComponentsForLifecycle()) { 382 performComponentInitialization(view, model, nestedComponent); 383 } 384 385 // initialize nested components in property replacements 386 for (PropertyReplacer replacer : component.getPropertyReplacers()) { 387 for (Component replacerComponent : replacer.getNestedComponents()) { 388 performComponentInitialization(view, model, replacerComponent); 389 } 390 } 391 392 // invoke initialize service hook 393 performCustomInitialization(view, component); 394 } 395 396 /** 397 * Iterates through the containers configured items checking for <code>RemotableFieldsHolder</code>, if found 398 * the holder is invoked to retrieved the remotable fields and translate to attribute fields. The translated list 399 * is then inserted into the container item list at the position of the holder 400 * 401 * @param view - view instance containing the container 402 * @param model - object instance containing the view data 403 * @param container - container instance to check for any remotable fields holder 404 */ 405 protected void processAnyRemoteFieldsHolder(View view, Object model, Container container) { 406 List<Component> processedItems = new ArrayList<Component>(); 407 408 // check for holders and invoke to retrieve the remotable fields and translate 409 // translated fields are placed into the container item list at the position of the holder 410 for (Component item : container.getItems()) { 411 if (item instanceof RemoteFieldsHolder) { 412 List<InputField> translatedFields = ((RemoteFieldsHolder) item).fetchAndTranslateRemoteFields(view, 413 model, container); 414 processedItems.addAll(translatedFields); 415 } else { 416 processedItems.add(item); 417 } 418 } 419 420 // updated container items 421 container.setItems(processedItems); 422 } 423 424 /** 425 * Sets properties of the <code>InputField</code> (if blank) to the 426 * corresponding attribute entry in the data dictionary 427 * 428 * @param view - view instance containing the field 429 * @param field - data field instance to initialize 430 */ 431 protected void initializeDataFieldFromDataDictionary(View view, DataField field) { 432 AttributeDefinition attributeDefinition = null; 433 434 String dictionaryAttributeName = field.getDictionaryAttributeName(); 435 String dictionaryObjectEntry = field.getDictionaryObjectEntry(); 436 437 // if entry given but not attribute name, use field name as attribute 438 // name 439 if (StringUtils.isNotBlank(dictionaryObjectEntry) && StringUtils.isBlank(dictionaryAttributeName)) { 440 dictionaryAttributeName = field.getPropertyName(); 441 } 442 443 // if dictionary entry and attribute set, attempt to find definition 444 if (StringUtils.isNotBlank(dictionaryAttributeName) && StringUtils.isNotBlank(dictionaryObjectEntry)) { 445 attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry, 446 dictionaryAttributeName); 447 } 448 449 // if definition not found, recurse through path 450 if (attributeDefinition == null) { 451 String propertyPath = field.getBindingInfo().getBindingPath(); 452 if (StringUtils.isNotBlank(field.getBindingInfo().getCollectionPath())) { 453 propertyPath = field.getBindingInfo().getCollectionPath(); 454 if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) { 455 propertyPath += "." + field.getBindingInfo().getBindByNamePrefix(); 456 } 457 propertyPath += "." + field.getBindingInfo().getBindingName(); 458 } 459 460 attributeDefinition = findNestedDictionaryAttribute(view, field, null, propertyPath); 461 } 462 463 // if a definition was found, initialize field from definition 464 if (attributeDefinition != null) { 465 field.copyFromAttributeDefinition(view, attributeDefinition); 466 } 467 468 // if control still null, assign default 469 if (field instanceof InputField) { 470 InputField inputField = (InputField) field; 471 if (inputField.getControl() == null) { 472 Control control = ComponentFactory.getTextControl(); 473 control.setId(view.getNextId()); 474 control.setFactoryId(control.getId()); 475 476 inputField.setControl(control); 477 } 478 } 479 } 480 481 /** 482 * Recursively drills down the property path (if nested) to find an 483 * AttributeDefinition, the first attribute definition found will be 484 * returned 485 * 486 * <p> 487 * e.g. suppose parentPath is 'document' and propertyPath is 488 * 'account.subAccount.name', first the property type for document will be 489 * retrieved using the view metadata and used as the dictionary entry, with 490 * the propertyPath as the dictionary attribute, if an attribute definition 491 * exists it will be returned. Else, the first part of the property path is 492 * added to the parent, making the parentPath 'document.account' and the 493 * propertyPath 'subAccount.name', the method is then called again to 494 * perform the process with those parameters. The recursion continues until 495 * an attribute field is found, or the propertyPath is no longer nested 496 * </p> 497 * 498 * @param view - view instance containing the field 499 * @param field - field we are attempting to find a supporting attribute 500 * definition for 501 * @param parentPath - parent path to use for getting the dictionary entry 502 * @param propertyPath - path of the property relative to the parent, to use as 503 * dictionary attribute and to drill down on 504 * @return AttributeDefinition if found, or Null 505 */ 506 protected AttributeDefinition findNestedDictionaryAttribute(View view, DataField field, String parentPath, 507 String propertyPath) { 508 AttributeDefinition attributeDefinition = null; 509 510 // attempt to find definition for parent and property 511 String dictionaryAttributeName = propertyPath; 512 String dictionaryObjectEntry = null; 513 514 if (field.getBindingInfo().isBindToMap()) { 515 parentPath = ""; 516 if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank( 517 field.getBindingInfo().getBindingObjectPath())) { 518 parentPath = field.getBindingInfo().getBindingObjectPath(); 519 } 520 if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) { 521 if (StringUtils.isNotBlank(parentPath)) { 522 parentPath += "." + field.getBindingInfo().getBindByNamePrefix(); 523 } else { 524 parentPath = field.getBindingInfo().getBindByNamePrefix(); 525 } 526 } 527 528 dictionaryAttributeName = field.getBindingInfo().getBindingName(); 529 } 530 531 if (StringUtils.isNotBlank(parentPath)) { 532 Class<?> dictionaryModelClass = ViewModelUtils.getPropertyTypeByClassAndView(view, parentPath); 533 if (dictionaryModelClass != null) { 534 dictionaryObjectEntry = dictionaryModelClass.getName(); 535 536 attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry, 537 dictionaryAttributeName); 538 } 539 } 540 541 // if definition not found and property is still nested, recurse down 542 // one level 543 if ((attributeDefinition == null) && StringUtils.contains(propertyPath, ".")) { 544 String nextParentPath = StringUtils.substringBefore(propertyPath, "."); 545 if (StringUtils.isNotBlank(parentPath)) { 546 nextParentPath = parentPath + "." + nextParentPath; 547 } 548 String nextPropertyPath = StringUtils.substringAfter(propertyPath, "."); 549 550 return findNestedDictionaryAttribute(view, field, nextParentPath, nextPropertyPath); 551 } 552 553 // if a definition was found, update the fields dictionary properties 554 if (attributeDefinition != null) { 555 field.setDictionaryAttributeName(dictionaryAttributeName); 556 field.setDictionaryObjectEntry(dictionaryObjectEntry); 557 } 558 559 return attributeDefinition; 560 } 561 562 /** 563 * @see org.kuali.rice.krad.uif.service.ViewHelperService#performApplyModel(org.kuali.rice.krad.uif.view.View, 564 * java.lang.Object) 565 */ 566 @Override 567 public void performApplyModel(View view, Object model) { 568 // get action flag and edit modes from authorizer/presentation controller 569 retrieveEditModesAndActionFlags(view, (UifFormBase) model); 570 571 // set view context for conditional expressions 572 setViewContext(view, model); 573 574 performComponentApplyModel(view, view, model); 575 } 576 577 /** 578 * Invokes the configured <code>PresentationController</code> and 579 * </code>Authorizer</code> for the view to get the exported action flags 580 * and edit modes that can be used in conditional logic 581 * 582 * @param view - view instance that is being built and presentation/authorizer pulled for 583 * @param model - Object that contains the model data 584 */ 585 protected void retrieveEditModesAndActionFlags(View view, UifFormBase model) { 586 ViewPresentationController presentationController = view.getPresentationController(); 587 ViewAuthorizer authorizer = view.getAuthorizer(); 588 589 Person user = GlobalVariables.getUserSession().getPerson(); 590 591 Set<String> actionFlags = presentationController.getActionFlags(view, model); 592 actionFlags = authorizer.getActionFlags(view, model, user, actionFlags); 593 594 view.setActionFlags(new BooleanMap(actionFlags)); 595 596 Set<String> editModes = presentationController.getEditModes(view, model); 597 editModes = authorizer.getEditModes(view, model, user, editModes); 598 599 view.setEditModes(new BooleanMap(editModes)); 600 } 601 602 /** 603 * Sets up the view context which will be available to other components 604 * through their context for conditional logic evaluation 605 * 606 * @param view - view instance to set context for 607 * @param model - object containing the view data 608 */ 609 protected void setViewContext(View view, Object model) { 610 view.pushAllToContext(getPreModelContext(view)); 611 612 // evaluate view expressions for further context 613 for (Entry<String, String> variableExpression : view.getExpressionVariables().entrySet()) { 614 String variableName = variableExpression.getKey(); 615 Object value = getExpressionEvaluatorService().evaluateExpression(model, view.getContext(), 616 variableExpression.getValue()); 617 view.pushObjectToContext(variableName, value); 618 } 619 } 620 621 /** 622 * Returns the general context that is available before the apply model 623 * phase (during the initialize phase) 624 * 625 * @param view - view instance for context 626 * @return Map<String, Object> context map 627 */ 628 protected Map<String, Object> getPreModelContext(View view) { 629 Map<String, Object> context = new HashMap<String, Object>(); 630 631 context.put(UifConstants.ContextVariableNames.VIEW, view); 632 context.put(UifConstants.ContextVariableNames.VIEW_HELPER, this); 633 634 Map<String, String> properties = KRADServiceLocator.getKualiConfigurationService().getAllProperties(); 635 context.put(UifConstants.ContextVariableNames.CONFIG_PROPERTIES, properties); 636 context.put(UifConstants.ContextVariableNames.CONSTANTS, KRADConstants.class); 637 context.put(UifConstants.ContextVariableNames.UIF_CONSTANTS, UifConstants.class); 638 639 return context; 640 } 641 642 /** 643 * Applies the model data to a component of the View instance 644 * 645 * <p> 646 * The component is invoked to to apply the model data. Here the component 647 * can generate any additional fields needed or alter the configured fields. 648 * After the component is invoked a hook for custom helper service 649 * processing is invoked. Finally the method is recursively called for all 650 * the component children 651 * </p> 652 * 653 * @param view - view instance the component belongs to 654 * @param component - the component instance the model should be applied to 655 * @param model - top level object containing the data 656 */ 657 protected void performComponentApplyModel(View view, Component component, Object model) { 658 if (component == null) { 659 return; 660 } 661 662 // evaluate expressions on component properties 663 component.pushAllToContext(getCommonContext(view, component)); 664 ExpressionUtils.adjustPropertyExpressions(view, component); 665 getExpressionEvaluatorService().evaluateObjectExpressions(component, model, component.getContext()); 666 667 // evaluate expressions on component security 668 ComponentSecurity componentSecurity = component.getComponentSecurity(); 669 ExpressionUtils.adjustPropertyExpressions(view, componentSecurity); 670 getExpressionEvaluatorService().evaluateObjectExpressions(componentSecurity, model, component.getContext()); 671 672 // evaluate expressions on the binding info object 673 if (component instanceof DataBinding) { 674 BindingInfo bindingInfo = ((DataBinding) component).getBindingInfo(); 675 ExpressionUtils.adjustPropertyExpressions(view, bindingInfo); 676 getExpressionEvaluatorService().evaluateObjectExpressions(bindingInfo, model, component.getContext()); 677 } 678 679 // evaluate expressions on the layout manager 680 if (component instanceof Container) { 681 LayoutManager layoutManager = ((Container) component).getLayoutManager(); 682 683 if (layoutManager != null) { 684 layoutManager.getContext().putAll(getCommonContext(view, component)); 685 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component); 686 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.MANAGER, layoutManager); 687 688 ExpressionUtils.adjustPropertyExpressions(view, layoutManager); 689 getExpressionEvaluatorService().evaluateObjectExpressions(layoutManager, model, 690 layoutManager.getContext()); 691 } 692 } 693 694 // sync the component with previous client side state 695 syncClientSideStateForComponent(component, ((ViewModel) model).getClientStateForSyncing()); 696 697 // invoke authorizer and presentation controller to set component state 698 applyAuthorizationAndPresentationLogic(view, component, (ViewModel) model); 699 700 // invoke component to perform its conditional logic 701 Component parent = (Component) component.getContext().get(UifConstants.ContextVariableNames.PARENT); 702 component.performApplyModel(view, model, parent); 703 704 // invoke service override hook 705 performCustomApplyModel(view, component, model); 706 707 // invoke component modifiers configured to run in the apply model phase 708 runComponentModifiers(view, component, model, UifConstants.ViewPhases.APPLY_MODEL); 709 710 // get children and recursively perform conditional logic 711 for (Component nestedComponent : component.getComponentsForLifecycle()) { 712 if (nestedComponent != null) { 713 nestedComponent.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component); 714 } 715 716 performComponentApplyModel(view, nestedComponent, model); 717 } 718 } 719 720 /** 721 * Invokes the view's configured {@link ViewAuthorizer} and {@link ViewPresentationController} to set state of 722 * the component 723 * 724 * <p> 725 * The following authorization is done here: 726 * Fields: edit, view, required, mask, and partial mask 727 * Groups: edit and view 728 * Actions: take action 729 * </p> 730 * 731 * <p> 732 * Note additional checks are also done for fields that are part of a collection group. This authorization is 733 * found in {@link org.kuali.rice.krad.uif.container.CollectionGroupBuilder} 734 * </p> 735 * 736 * @param view - view instance the component belongs to and from which the authorizer and presentation controller 737 * will be pulled 738 * @param component - component instance to authorize 739 * @param model - model object containing the data for the view 740 */ 741 protected void applyAuthorizationAndPresentationLogic(View view, Component component, ViewModel model) { 742 ViewPresentationController presentationController = view.getPresentationController(); 743 ViewAuthorizer authorizer = view.getAuthorizer(); 744 745 Person user = GlobalVariables.getUserSession().getPerson(); 746 747 // if component not flagged for render no need to check auth and controller logic 748 if (!component.isRender()) { 749 return; 750 } 751 752 // check top level view edit authorization 753 if (component instanceof View) { 754 if (!view.isReadOnly()) { 755 boolean canEditView = authorizer.canEditView(view, model, user); 756 if (canEditView) { 757 canEditView = presentationController.canEditView(view, model); 758 } 759 view.setReadOnly(!canEditView); 760 } 761 } 762 763 // perform group authorization and presentation logic 764 else if (component instanceof Group) { 765 Group group = (Group) component; 766 767 // if group is not hidden, do authorization for viewing the group 768 if (!group.isHidden()) { 769 boolean canViewGroup = authorizer.canViewGroup(view, model, group, group.getId(), user); 770 if (canViewGroup) { 771 canViewGroup = presentationController.canViewGroup(view, model, group, group.getId()); 772 } 773 group.setHidden(!canViewGroup); 774 group.setRender(canViewGroup); 775 } 776 777 // if group is editable, do authorization for editing the group 778 if (!group.isReadOnly()) { 779 boolean canEditGroup = authorizer.canEditGroup(view, model, group, group.getId(), user); 780 if (canEditGroup) { 781 canEditGroup = presentationController.canEditGroup(view, model, group, group.getId()); 782 } 783 group.setReadOnly(!canEditGroup); 784 } 785 } 786 787 // perform field authorization and presentation logic 788 else if (component instanceof Field) { 789 Field field = (Field) component; 790 791 String propertyName = null; 792 if (field instanceof DataBinding) { 793 propertyName = ((DataBinding) field).getPropertyName(); 794 } 795 796 // if field is not hidden, do authorization for viewing the field 797 if (!field.isHidden()) { 798 boolean canViewField = authorizer.canViewField(view, model, field, propertyName, user); 799 if (canViewField) { 800 canViewField = presentationController.canViewField(view, model, field, propertyName); 801 } 802 field.setHidden(!canViewField); 803 field.setRender(canViewField); 804 } 805 806 // if field is not readOnly, check edit authorization 807 if (!field.isReadOnly()) { 808 // check field edit authorization 809 boolean canEditField = authorizer.canEditField(view, model, field, propertyName, user); 810 if (canEditField) { 811 canEditField = presentationController.canEditField(view, model, field, propertyName); 812 } 813 field.setReadOnly(!canEditField); 814 } 815 816 // if field is not already required, invoke presentation logic to determine if it should be 817 if ((field.getRequired() == null) || !field.getRequired().booleanValue()) { 818 boolean fieldIsRequired = presentationController.fieldIsRequired(view, model, field, propertyName); 819 } 820 821 if (field instanceof DataField) { 822 DataField dataField = (DataField) field; 823 824 // check mask authorization 825 boolean canUnmaskValue = authorizer.canUnmaskField(view, model, dataField, dataField.getPropertyName(), 826 user); 827 if (!canUnmaskValue) { 828 dataField.setApplyValueMask(true); 829 dataField.setMaskFormatter(dataField.getDataFieldSecurity().getAttributeSecurity(). 830 getMaskFormatter()); 831 } else { 832 // check partial mask authorization 833 boolean canPartiallyUnmaskValue = authorizer.canPartialUnmaskField(view, model, dataField, 834 dataField.getPropertyName(), user); 835 if (!canPartiallyUnmaskValue) { 836 dataField.setApplyValueMask(true); 837 dataField.setMaskFormatter( 838 dataField.getDataFieldSecurity().getAttributeSecurity().getPartialMaskFormatter()); 839 } 840 } 841 } 842 843 // check authorization for actions 844 if (field instanceof ActionField) { 845 ActionField actionField = (ActionField) field; 846 847 boolean canTakeAction = authorizer.canPerformAction(view, model, actionField, 848 actionField.getActionEvent(), actionField.getId(), user); 849 if (canTakeAction) { 850 canTakeAction = presentationController.canPerformAction(view, model, actionField, 851 actionField.getActionEvent(), actionField.getId()); 852 } 853 actionField.setRender(canTakeAction); 854 } 855 } 856 857 // perform widget authorization and presentation logic 858 else if (component instanceof Widget) { 859 Widget widget = (Widget) component; 860 861 // if widget is not hidden, do authorization for viewing the widget 862 if (!widget.isHidden()) { 863 boolean canViewWidget = authorizer.canViewWidget(view, model, widget, widget.getId(), user); 864 if (canViewWidget) { 865 canViewWidget = presentationController.canViewWidget(view, model, widget, widget.getId()); 866 } 867 widget.setHidden(!canViewWidget); 868 widget.setRender(canViewWidget); 869 } 870 871 // if widget is not readOnly, check edit authorization 872 if (!widget.isReadOnly()) { 873 boolean canEditWidget = authorizer.canEditWidget(view, model, widget, widget.getId(), user); 874 if (canEditWidget) { 875 canEditWidget = presentationController.canEditWidget(view, model, widget, widget.getId()); 876 } 877 widget.setReadOnly(!canEditWidget); 878 } 879 } 880 } 881 882 /** 883 * Runs any configured <code>ComponentModifiers</code> for the given 884 * component that match the given run phase and who run condition evaluation 885 * succeeds 886 * 887 * <p> 888 * If called during the initialize phase, the performInitialization method will be invoked on 889 * the <code>ComponentModifier</code> before running 890 * </p> 891 * 892 * @param view - view instance for context 893 * @param component - component instance whose modifiers should be run 894 * @param model - model object for context 895 * @param runPhase - current phase to match on 896 */ 897 protected void runComponentModifiers(View view, Component component, Object model, String runPhase) { 898 for (ComponentModifier modifier : component.getComponentModifiers()) { 899 // if run phase is initialize, invoke initialize method on modifier first 900 if (StringUtils.equals(runPhase, UifConstants.ViewPhases.INITIALIZE)) { 901 modifier.performInitialization(view, model, component); 902 } 903 904 // check run phase matches 905 if (StringUtils.equals(modifier.getRunPhase(), runPhase)) { 906 // check condition (if set) evaluates to true 907 boolean runModifier = true; 908 if (StringUtils.isNotBlank(modifier.getRunCondition())) { 909 Map<String, Object> context = new HashMap<String, Object>(); 910 context.put(UifConstants.ContextVariableNames.COMPONENT, component); 911 context.put(UifConstants.ContextVariableNames.VIEW, view); 912 913 String conditionEvaluation = getExpressionEvaluatorService().evaluateExpressionTemplate(model, 914 context, modifier.getRunCondition()); 915 runModifier = Boolean.parseBoolean(conditionEvaluation); 916 } 917 918 if (runModifier) { 919 modifier.performModification(view, model, component); 920 } 921 } 922 } 923 } 924 925 /** 926 * Gets global objects for the context map and pushes them to the context 927 * for the component 928 * 929 * @param view - view instance for component 930 * @param component - component instance to push context to 931 */ 932 protected Map<String, Object> getCommonContext(View view, Component component) { 933 Map<String, Object> context = new HashMap<String, Object>(); 934 935 context.putAll(view.getContext()); 936 context.put(UifConstants.ContextVariableNames.COMPONENT, component); 937 938 return context; 939 } 940 941 /** 942 * @see org.kuali.rice.krad.uif.service.ViewHelperService#performFinalize(org.kuali.rice.krad.uif.view.View, 943 * java.lang.Object) 944 */ 945 @Override 946 public void performFinalize(View view, Object model) { 947 Map<String, Object> clientState = new HashMap<String, Object>(); 948 performComponentFinalize(view, view, model, null, clientState); 949 950 String clientStateScript = buildClientSideStateScript(view, clientState, false); 951 String viewPreLoadScript = view.getPreLoadScript(); 952 if (StringUtils.isNotBlank(viewPreLoadScript)) { 953 clientStateScript = viewPreLoadScript + clientStateScript; 954 } 955 view.setPreLoadScript(clientStateScript); 956 957 // apply default values if they have not been applied yet 958 if (!((ViewModel) model).isDefaultsApplied()) { 959 applyDefaultValues(view, view, model); 960 ((ViewModel) model).setDefaultsApplied(true); 961 } 962 } 963 964 /** 965 * Builds script that will initialize configuration parameters and component state on the client 966 * 967 * <p> 968 * Here client side state is initialized along with configuration variables that need exposed to script 969 * </p> 970 * 971 * @param view - view instance that is being built 972 * @param clientSideState - map of key/value pairs that should be exposed as client side state 973 * @param updateOnly - boolean that indicates whether we are just updating a component (true), or the full view 974 */ 975 protected String buildClientSideStateScript(View view, Map<String, Object> clientSideState, boolean updateOnly) { 976 // merge any additional client side state added to the view during processing 977 // state from view will override in all cases except when both values are maps, in which the maps 978 // be combined for the new value 979 for (Entry<String, Object> additionalState : view.getClientSideState().entrySet()) { 980 if (!clientSideState.containsKey(additionalState.getKey())) { 981 clientSideState.put(additionalState.getKey(), additionalState.getValue()); 982 } else { 983 Object state = clientSideState.get(additionalState.getKey()); 984 Object mergeState = additionalState.getValue(); 985 if ((state instanceof Map) && (mergeState instanceof Map)) { 986 ((Map) state).putAll((Map) mergeState); 987 } else { 988 clientSideState.put(additionalState.getKey(), additionalState.getValue()); 989 } 990 } 991 } 992 993 // script for initializing client side state on load 994 String clientStateScript = ""; 995 if (!clientSideState.isEmpty()) { 996 if (updateOnly) { 997 clientStateScript = "updateViewState({"; 998 } else { 999 clientStateScript = "initializeViewState({"; 1000 } 1001 1002 for (Entry<String, Object> stateEntry : clientSideState.entrySet()) { 1003 clientStateScript += "'" + stateEntry.getKey() + "':"; 1004 clientStateScript += ScriptUtils.translateValue(stateEntry.getValue()); 1005 clientStateScript += ","; 1006 } 1007 clientStateScript = StringUtils.removeEnd(clientStateScript, ","); 1008 clientStateScript += "});"; 1009 } 1010 1011 // add necessary configuration parameters 1012 if (!updateOnly) { 1013 String kradImageLocation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 1014 "krad.externalizable.images.url"); 1015 clientStateScript += "setConfigParam('" 1016 + UifConstants.ClientSideVariables.KRAD_IMAGE_LOCATION 1017 + "','" 1018 + kradImageLocation 1019 + "');"; 1020 1021 String kradURL = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString("krad.url"); 1022 clientStateScript += "setConfigParam('" 1023 + UifConstants.ClientSideVariables.KRAD_URL 1024 + "','" 1025 + kradURL 1026 + "');"; 1027 } 1028 1029 return clientStateScript; 1030 } 1031 1032 /** 1033 * Update state of the given component and does final preparation for 1034 * rendering 1035 * 1036 * @param view - view instance the component belongs to 1037 * @param component - the component instance that should be updated 1038 * @param model - top level object containing the data 1039 * @param parent - Parent component for the component being finalized 1040 * @param clientSideState - map to add client state to 1041 */ 1042 protected void performComponentFinalize(View view, Component component, Object model, Component parent, 1043 Map<String, Object> clientSideState) { 1044 if (component == null) { 1045 return; 1046 } 1047 1048 // implement readonly request overrides 1049 ViewModel viewModel = (ViewModel) model; 1050 if ((component instanceof DataBinding) && view.isSupportsReadOnlyFieldsOverride() && !viewModel 1051 .getReadOnlyFieldsList().isEmpty()) { 1052 String propertyName = ((DataBinding) component).getPropertyName(); 1053 if (viewModel.getReadOnlyFieldsList().contains(propertyName)) { 1054 component.setReadOnly(true); 1055 } 1056 } 1057 1058 // invoke configured method finalizers 1059 invokeMethodFinalizer(view, component, model); 1060 1061 // invoke component to update its state 1062 component.performFinalize(view, model, parent); 1063 1064 // add client side state for annotated component properties 1065 addClientSideStateForComponent(component, clientSideState); 1066 1067 // invoke service override hook 1068 performCustomFinalize(view, component, model, parent); 1069 1070 // invoke component modifiers setup to run in the finalize phase 1071 runComponentModifiers(view, component, model, UifConstants.ViewPhases.FINALIZE); 1072 1073 // get components children and recursively update state 1074 for (Component nestedComponent : component.getComponentsForLifecycle()) { 1075 performComponentFinalize(view, nestedComponent, model, component, clientSideState); 1076 } 1077 } 1078 1079 /** 1080 * Reflects the class for the given component to find any fields that are annotated with 1081 * <code>ClientSideState</code> and adds the corresponding property name/value pair to the client side state 1082 * map 1083 * 1084 * <p> 1085 * Note if the component is the <code>View</code, state is added directly to the client side state map, while 1086 * for other components a nested Map is created to hold the state, which is then placed into the client side 1087 * state map with the component id as the key 1088 * </p> 1089 * 1090 * @param component - component instance to get client state for 1091 * @param clientSideState - map to add client side variable name/values to 1092 */ 1093 protected void addClientSideStateForComponent(Component component, Map<String, Object> clientSideState) { 1094 Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(), 1095 ClientSideState.class); 1096 1097 if (!annotatedFields.isEmpty()) { 1098 Map<String, Object> componentClientState = null; 1099 if (component instanceof View) { 1100 componentClientState = clientSideState; 1101 } else { 1102 if (clientSideState.containsKey(component.getId())) { 1103 componentClientState = (Map<String, Object>) clientSideState.get(component.getId()); 1104 } else { 1105 componentClientState = new HashMap<String, Object>(); 1106 clientSideState.put(component.getId(), componentClientState); 1107 } 1108 } 1109 1110 for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) { 1111 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue(); 1112 1113 String variableName = clientSideStateAnnot.variableName(); 1114 if (StringUtils.isBlank(variableName)) { 1115 variableName = annotatedField.getKey(); 1116 } 1117 1118 Object value = ObjectPropertyUtils.getPropertyValue(component, annotatedField.getKey()); 1119 componentClientState.put(variableName, value); 1120 } 1121 } 1122 } 1123 1124 /** 1125 * Updates the properties of the given component instance with the value found from the corresponding map of 1126 * client state (if found) 1127 * 1128 * @param component - component instance to update 1129 * @param clientSideState - map of state to sync with 1130 */ 1131 protected void syncClientSideStateForComponent(Component component, Map<String, Object> clientSideState) { 1132 // find the map of state that was sent for component (if any) 1133 Map<String, Object> componentState = null; 1134 if (component instanceof View) { 1135 componentState = clientSideState; 1136 } else { 1137 if (clientSideState.containsKey(component.getId())) { 1138 componentState = (Map<String, Object>) clientSideState.get(component.getId()); 1139 } 1140 } 1141 1142 // if state was sent, match with fields on the component that are annotated to have client state 1143 if ((componentState != null) && (!componentState.isEmpty())) { 1144 Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(), 1145 ClientSideState.class); 1146 1147 for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) { 1148 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue(); 1149 1150 String variableName = clientSideStateAnnot.variableName(); 1151 if (StringUtils.isBlank(variableName)) { 1152 variableName = annotatedField.getKey(); 1153 } 1154 1155 if (componentState.containsKey(variableName)) { 1156 Object value = componentState.get(variableName); 1157 ObjectPropertyUtils.setPropertyValue(component, annotatedField.getKey(), value); 1158 } 1159 } 1160 } 1161 } 1162 1163 /** 1164 * Invokes the finalize method for the component (if configured) and sets 1165 * the render output for the component to the returned method string (if 1166 * method is not a void type) 1167 * 1168 * @param view - view instance that contains the component 1169 * @param component - component to run finalize method for 1170 * @param model - top level object containing the data 1171 */ 1172 protected void invokeMethodFinalizer(View view, Component component, Object model) { 1173 String finalizeMethodToCall = component.getFinalizeMethodToCall(); 1174 MethodInvoker finalizeMethodInvoker = component.getFinalizeMethodInvoker(); 1175 1176 if (StringUtils.isBlank(finalizeMethodToCall) && (finalizeMethodInvoker == null)) { 1177 return; 1178 } 1179 1180 if (finalizeMethodInvoker == null) { 1181 finalizeMethodInvoker = new MethodInvoker(); 1182 } 1183 1184 // if method not set on invoker, use renderingMethodToCall, note staticMethod could be set(don't know since 1185 // there is not a getter), if so it will override the target method in prepare 1186 if (StringUtils.isBlank(finalizeMethodInvoker.getTargetMethod())) { 1187 finalizeMethodInvoker.setTargetMethod(finalizeMethodToCall); 1188 } 1189 1190 // if target class or object not set, use view helper service 1191 if ((finalizeMethodInvoker.getTargetClass() == null) && (finalizeMethodInvoker.getTargetObject() == null)) { 1192 finalizeMethodInvoker.setTargetObject(view.getViewHelperService()); 1193 } 1194 1195 // setup arguments for method 1196 List<Object> additionalArguments = component.getFinalizeMethodAdditionalArguments(); 1197 if (additionalArguments == null) { 1198 additionalArguments = new ArrayList<Object>(); 1199 } 1200 1201 Object[] arguments = new Object[2 + additionalArguments.size()]; 1202 arguments[0] = component; 1203 arguments[1] = model; 1204 1205 int argumentIndex = 1; 1206 for (Object argument : additionalArguments) { 1207 argumentIndex++; 1208 arguments[argumentIndex] = argument; 1209 } 1210 finalizeMethodInvoker.setArguments(arguments); 1211 1212 // invoke method and get render output 1213 try { 1214 LOG.debug("Invoking render method: " 1215 + finalizeMethodInvoker.getTargetMethod() 1216 + " for component: " 1217 + component.getId()); 1218 finalizeMethodInvoker.prepare(); 1219 1220 Class<?> methodReturnType = finalizeMethodInvoker.getPreparedMethod().getReturnType(); 1221 if (StringUtils.equals("void", methodReturnType.getName())) { 1222 finalizeMethodInvoker.invoke(); 1223 } else { 1224 String renderOutput = (String) finalizeMethodInvoker.invoke(); 1225 1226 component.setSelfRendered(true); 1227 component.setRenderOutput(renderOutput); 1228 } 1229 } catch (Exception e) { 1230 LOG.error("Error invoking rendering method for component: " + component.getId(), e); 1231 throw new RuntimeException("Error invoking rendering method for component: " + component.getId(), e); 1232 } 1233 } 1234 1235 /** 1236 * @see org.kuali.rice.krad.uif.service.ViewHelperService#cleanViewAfterRender(org.kuali.rice.krad.uif.view.View) 1237 */ 1238 @Override 1239 public void cleanViewAfterRender(View view) { 1240 ViewCleaner.cleanView(view); 1241 } 1242 1243 /** 1244 * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionAddLine(org.kuali.rice.krad.uif.view.View, 1245 * java.lang.Object, java.lang.String) 1246 */ 1247 @Override 1248 public void processCollectionAddLine(View view, Object model, String collectionPath) { 1249 // get the collection group from the view 1250 CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath); 1251 if (collectionGroup == null) { 1252 logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath); 1253 } 1254 1255 // get the collection instance for adding the new line 1256 Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath); 1257 if (collection == null) { 1258 logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath); 1259 } 1260 1261 // now get the new line we need to add 1262 String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 1263 Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath); 1264 if (addLine == null) { 1265 logAndThrowRuntime("Add line instance not found for path: " + addLinePath); 1266 } 1267 1268 processBeforeAddLine(view, collectionGroup, model, addLine); 1269 1270 // validate the line to make sure it is ok to add 1271 boolean isValidLine = performAddLineValidation(view, collectionGroup, model, addLine); 1272 if (isValidLine) { 1273 // TODO: should check to see if there is an add line method on the 1274 // collection parent and if so call that instead of just adding to 1275 // the collection (so that sequence can be set) 1276 addLine(collection, addLine); 1277 1278 // make a new instance for the add line 1279 collectionGroup.initializeNewCollectionLine(view, model, collectionGroup, true); 1280 } 1281 1282 processAfterAddLine(view, collectionGroup, model, addLine); 1283 } 1284 1285 /** 1286 * Add addLine to collection while giving derived classes an opportunity to override 1287 * for things like sorting. 1288 * 1289 * @param collection - the Collection to add the given addLine to 1290 * @param addLine - the line to add to the given collection 1291 */ 1292 protected void addLine(Collection<Object> collection, Object addLine) { 1293 if (collection instanceof List) { 1294 ((List) collection).add(0, addLine); 1295 } else { 1296 collection.add(addLine); 1297 } 1298 } 1299 1300 1301 /** 1302 * Performs validation on the new collection line before it is added to the 1303 * corresponding collection 1304 * 1305 * @param view - view instance that the action was taken on 1306 * @param collectionGroup - collection group component for the collection 1307 * @param addLine - new line instance to validate 1308 * @param model - object instance that contain's the views data 1309 * @return boolean true if the line is valid and it should be added to the 1310 * collection, false if it was not valid and should not be added to 1311 * the collection 1312 */ 1313 protected boolean performAddLineValidation(View view, CollectionGroup collectionGroup, Object model, 1314 Object addLine) { 1315 boolean isValid = true; 1316 1317 // TODO: this should invoke rules, subclasses like the document view 1318 // should create the document add line event 1319 1320 return isValid; 1321 } 1322 1323 /** 1324 * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionDeleteLine(org.kuali.rice.krad.uif.view.View, 1325 * java.lang.Object, java.lang.String, int) 1326 */ 1327 public void processCollectionDeleteLine(View view, Object model, String collectionPath, int lineIndex) { 1328 // get the collection group from the view 1329 CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath); 1330 if (collectionGroup == null) { 1331 logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath); 1332 } 1333 1334 // get the collection instance for adding the new line 1335 Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath); 1336 if (collection == null) { 1337 logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath); 1338 } 1339 1340 // TODO: look into other ways of identifying a line so we can deal with 1341 // unordered collections 1342 if (collection instanceof List) { 1343 Object deleteLine = ((List<Object>) collection).get(lineIndex); 1344 1345 // validate the delete action is allowed for this line 1346 boolean isValid = performDeleteLineValidation(view, collectionGroup, deleteLine); 1347 if (isValid) { 1348 ((List<Object>) collection).remove(lineIndex); 1349 processAfterDeleteLine(view, collectionGroup, model, lineIndex); 1350 } 1351 } else { 1352 logAndThrowRuntime("Only List collection implementations are supported for the delete by index method"); 1353 } 1354 } 1355 1356 /** 1357 * Performs validation on the collection line before it is removed from the 1358 * corresponding collection 1359 * 1360 * @param view - view instance that the action was taken on 1361 * @param collectionGroup - collection group component for the collection 1362 * @param deleteLine - line that will be removed 1363 * @return boolean true if the action is allowed and the line should be 1364 * removed, false if the line should not be removed 1365 */ 1366 protected boolean performDeleteLineValidation(View view, CollectionGroup collectionGroup, Object deleteLine) { 1367 boolean isValid = true; 1368 1369 // TODO: this should invoke rules, sublclasses like the document view 1370 // should create the document delete line event 1371 1372 return isValid; 1373 } 1374 1375 /** 1376 * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#processMultipleValueLookupResults 1377 */ 1378 public void processMultipleValueLookupResults(View view, Object model, String collectionPath, 1379 String lookupResultValues) { 1380 // if no line values returned, no population is needed 1381 if (StringUtils.isBlank(lookupResultValues)) { 1382 return; 1383 } 1384 1385 // retrieve the collection group so we can get the collection class and collection lookup 1386 CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath); 1387 if (collectionGroup == null) { 1388 throw new RuntimeException("Unable to find collection group for path: " + collectionPath); 1389 } 1390 1391 Class<?> collectionObjectClass = collectionGroup.getCollectionObjectClass(); 1392 Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, 1393 collectionGroup.getBindingInfo().getBindingPath()); 1394 if (collection == null) { 1395 Class<?> collectionClass = ObjectPropertyUtils.getPropertyType(model, 1396 collectionGroup.getBindingInfo().getBindingPath()); 1397 collection = (Collection<Object>) ObjectUtils.newInstance(collectionClass); 1398 ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getBindingInfo().getBindingPath(), collection); 1399 } 1400 1401 Map<String, String> fieldConversions = collectionGroup.getCollectionLookup().getFieldConversions(); 1402 List<String> toFieldNamesColl = new ArrayList(fieldConversions.values()); 1403 Collections.sort(toFieldNamesColl); 1404 String[] toFieldNames = new String[toFieldNamesColl.size()]; 1405 toFieldNamesColl.toArray(toFieldNames); 1406 1407 // first split to get the line value sets 1408 String[] lineValues = StringUtils.split(lookupResultValues, ","); 1409 1410 // for each returned set create a new instance of collection class and populate with returned line values 1411 for (String lineValue : lineValues) { 1412 Object lineDataObject = null; 1413 1414 // TODO: need to put this in data object service so logic can be reused 1415 ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( 1416 collectionObjectClass); 1417 if (moduleService != null && moduleService.isExternalizable(collectionObjectClass)) { 1418 lineDataObject = moduleService.createNewObjectFromExternalizableClass(collectionObjectClass.asSubclass( 1419 ExternalizableBusinessObject.class)); 1420 } else { 1421 lineDataObject = ObjectUtils.newInstance(collectionObjectClass); 1422 } 1423 1424 // apply default values to new line 1425 applyDefaultValuesForCollectionLine(view, model, collectionGroup, lineDataObject); 1426 1427 String[] fieldValues = StringUtils.split(lineValue, ":"); 1428 if (fieldValues.length != toFieldNames.length) { 1429 throw new RuntimeException( 1430 "Value count passed back from multi-value lookup does not match field conversion count"); 1431 } 1432 1433 // set each field value on the line 1434 for (int i = 0; i < fieldValues.length; i++) { 1435 String fieldName = toFieldNames[i]; 1436 ObjectPropertyUtils.setPropertyValue(lineDataObject, fieldName, fieldValues[i]); 1437 } 1438 1439 // TODO: duplicate identifier check 1440 1441 collection.add(lineDataObject); 1442 } 1443 } 1444 1445 /** 1446 * Finds the <code>Inquirable</code> configured for the given data object 1447 * class and delegates to it for building the inquiry URL 1448 * 1449 * @see org.kuali.rice.krad.uif.service.ViewHelperService#buildInquiryLink(java.lang.Object, 1450 * java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry) 1451 */ 1452 public void buildInquiryLink(Object dataObject, String propertyName, Inquiry inquiry) { 1453 Inquirable inquirable = getViewDictionaryService().getInquirable(dataObject.getClass(), inquiry.getViewName()); 1454 if (inquirable != null) { 1455 inquirable.buildInquirableLink(dataObject, propertyName, inquiry); 1456 } else { 1457 // inquirable not found, no inquiry link can be set 1458 inquiry.setRender(false); 1459 } 1460 } 1461 1462 /** 1463 * @see org.kuali.rice.krad.uif.service.ViewHelperService#applyDefaultValuesForCollectionLine(org.kuali.rice.krad.uif.view.View, 1464 * java.lang.Object, org.kuali.rice.krad.uif.container.CollectionGroup, 1465 * java.lang.Object) 1466 */ 1467 public void applyDefaultValuesForCollectionLine(View view, Object model, CollectionGroup collectionGroup, 1468 Object line) { 1469 // retrieve all data fields for the collection line 1470 List<DataField> dataFields = ComponentUtils.getComponentsOfTypeDeep(collectionGroup.getAddLineFields(), 1471 DataField.class); 1472 for (DataField dataField : dataFields) { 1473 String bindingPath = ""; 1474 if (StringUtils.isNotBlank(dataField.getBindingInfo().getBindByNamePrefix())) { 1475 bindingPath = dataField.getBindingInfo().getBindByNamePrefix() + "."; 1476 } 1477 bindingPath += dataField.getBindingInfo().getBindingName(); 1478 1479 populateDefaultValueForField(view, line, dataField, bindingPath); 1480 } 1481 } 1482 1483 /** 1484 * Iterates through the view components picking up data fields and applying an default value configured 1485 * 1486 * @param view - view instance we are applying default values for 1487 * @param component - component that should be checked for default values 1488 * @param model - model object that values should be set on 1489 */ 1490 protected void applyDefaultValues(View view, Component component, Object model) { 1491 if (component == null) { 1492 return; 1493 } 1494 1495 // if component is a data field apply default value 1496 if (component instanceof DataField) { 1497 DataField dataField = ((DataField) component); 1498 1499 // need to make sure binding is initialized since this could be on a page we have not initialized yet 1500 dataField.getBindingInfo().setDefaults(view, dataField.getPropertyName()); 1501 1502 populateDefaultValueForField(view, model, dataField, dataField.getBindingInfo().getBindingPath()); 1503 } 1504 1505 List<Component> nestedComponents = component.getComponentsForLifecycle(); 1506 1507 // if view, need to add all pages since only one will be on the lifecycle 1508 if (component instanceof View) { 1509 nestedComponents.addAll(((View) component).getItems()); 1510 } 1511 1512 for (Component nested : nestedComponents) { 1513 applyDefaultValues(view, nested, model); 1514 } 1515 } 1516 1517 /** 1518 * Applies the default value configured for the given field (if any) to the 1519 * line given object property that is determined by the given binding path 1520 * 1521 * <p> 1522 * Checks for a configured default value or default value class for the 1523 * field. If both are given, the configured static default value will win. 1524 * In addition, if the default value contains an el expression it is 1525 * evaluated against the initial context 1526 * </p> 1527 * 1528 * @param view - view instance the field belongs to 1529 * @param object - object that should be populated 1530 * @param dataField - field to check for configured default value 1531 * @param bindingPath - path to the property on the object that should be populated 1532 */ 1533 protected void populateDefaultValueForField(View view, Object object, DataField dataField, String bindingPath) { 1534 // check for configured default value 1535 String defaultValue = dataField.getDefaultValue(); 1536 if (StringUtils.isBlank(defaultValue) && (dataField.getDefaultValueFinderClass() != null)) { 1537 ValueFinder defaultValueFinder = ObjectUtils.newInstance(dataField.getDefaultValueFinderClass()); 1538 defaultValue = defaultValueFinder.getValue(); 1539 } 1540 1541 // populate default value if given and path is valid 1542 if (StringUtils.isNotBlank(defaultValue) && ObjectPropertyUtils.isWritableProperty(object, bindingPath)) { 1543 if (getExpressionEvaluatorService().containsElPlaceholder(defaultValue)) { 1544 Map<String, Object> context = getPreModelContext(view); 1545 defaultValue = getExpressionEvaluatorService().evaluateExpressionTemplate(null, context, defaultValue); 1546 } 1547 1548 // TODO: this should go through our formatters 1549 // Skip nullable non-null non-empty objects when setting default 1550 Object currentValue = ObjectPropertyUtils.getPropertyValue(object, bindingPath); 1551 Class currentClazz = ObjectPropertyUtils.getPropertyType(object, bindingPath); 1552 if(currentValue == null || StringUtils.isBlank(currentValue.toString()) || ClassUtils.isPrimitiveOrWrapper(currentClazz)) { 1553 ObjectPropertyUtils.setPropertyValue(object, bindingPath, defaultValue); 1554 } 1555 } 1556 } 1557 1558 /** 1559 * Hook for creating new components with code and adding them to a container 1560 * 1561 * <p> 1562 * Subclasses can override this method to check for one or more containers by id and then adding components 1563 * created in code. This is invoked before the initialize method on the container component, so the full 1564 * lifecycle will be run on the components returned. 1565 * </p> 1566 * 1567 * <p> 1568 * New components instances can be retrieved using {@link ComponentFactory} 1569 * </p> 1570 * 1571 * @param view - view instance the container belongs to 1572 * @param model - object containing the view data 1573 * @param container - container instance to add components to 1574 */ 1575 protected void addCustomContainerComponents(View view, Object model, Container container) { 1576 1577 } 1578 1579 /** 1580 * Hook for service overrides to perform custom initialization on the 1581 * component 1582 * 1583 * @param view - view instance containing the component 1584 * @param component - component instance to initialize 1585 */ 1586 protected void performCustomInitialization(View view, Component component) { 1587 1588 } 1589 1590 /** 1591 * Hook for service overrides to perform custom apply model logic on the 1592 * component 1593 * 1594 * @param view - view instance containing the component 1595 * @param component - component instance to apply model to 1596 * @param model - Top level object containing the data (could be the form or a 1597 * top level business object, dto) 1598 */ 1599 protected void performCustomApplyModel(View view, Component component, Object model) { 1600 1601 } 1602 1603 /** 1604 * Hook for service overrides to perform custom component finalization 1605 * 1606 * @param view - view instance containing the component 1607 * @param component - component instance to update 1608 * @param model - Top level object containing the data 1609 * @param parent - Parent component for the component being finalized 1610 */ 1611 protected void performCustomFinalize(View view, Component component, Object model, Component parent) { 1612 1613 } 1614 1615 /** 1616 * Hook for service overrides to process the new collection line before it 1617 * is added to the collection 1618 * 1619 * @param view - view instance that is being presented (the action was taken 1620 * on) 1621 * @param collectionGroup - collection group component for the collection the line will 1622 * be added to 1623 * @param model - object instance that contain's the views data 1624 * @param addLine - the new line instance to be processed 1625 */ 1626 protected void processBeforeAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) { 1627 1628 } 1629 1630 /** 1631 * Hook for service overrides to process the new collection line after it 1632 * has been added to the collection 1633 * 1634 * @param view - view instance that is being presented (the action was taken 1635 * on) 1636 * @param collectionGroup - collection group component for the collection the line that 1637 * was added 1638 * @param model - object instance that contain's the views data 1639 * @param addLine - the new line that was added 1640 */ 1641 protected void processAfterAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) { 1642 1643 } 1644 1645 /** 1646 * Hook for service overrides to process the collection line after it has been deleted 1647 * 1648 * @param view - view instance that is being presented (the action was taken on) 1649 * @param collectionGroup - collection group component for the collection the line that 1650 * was added 1651 * @param model - object instance that contains the views data 1652 * @param lineIndex - index of the line that was deleted 1653 */ 1654 protected void processAfterDeleteLine(View view, CollectionGroup collectionGroup, Object model, int lineIndex) { 1655 1656 } 1657 1658 protected void logAndThrowRuntime(String message) { 1659 LOG.error(message); 1660 throw new RuntimeException(message); 1661 } 1662 1663 protected DataDictionaryService getDataDictionaryService() { 1664 if (this.dataDictionaryService == null) { 1665 this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 1666 } 1667 1668 return this.dataDictionaryService; 1669 } 1670 1671 public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { 1672 this.dataDictionaryService = dataDictionaryService; 1673 } 1674 1675 protected ExpressionEvaluatorService getExpressionEvaluatorService() { 1676 if (this.expressionEvaluatorService == null) { 1677 this.expressionEvaluatorService = KRADServiceLocatorWeb.getExpressionEvaluatorService(); 1678 } 1679 1680 return this.expressionEvaluatorService; 1681 } 1682 1683 public void setExpressionEvaluatorService(ExpressionEvaluatorService expressionEvaluatorService) { 1684 this.expressionEvaluatorService = expressionEvaluatorService; 1685 } 1686 1687 public ViewDictionaryService getViewDictionaryService() { 1688 if (this.viewDictionaryService == null) { 1689 this.viewDictionaryService = KRADServiceLocatorWeb.getViewDictionaryService(); 1690 } 1691 return this.viewDictionaryService; 1692 } 1693 1694 public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) { 1695 this.viewDictionaryService = viewDictionaryService; 1696 } 1697}