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.service.impl; 017 018import org.apache.commons.collections.CollectionUtils; 019import org.apache.commons.lang.StringUtils; 020import org.kuali.rice.core.api.data.DataType; 021import org.kuali.rice.kim.api.KimConstants; 022import org.kuali.rice.krad.data.DataObjectService; 023import org.kuali.rice.krad.data.metadata.DataObjectAttribute; 024import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship; 025import org.kuali.rice.krad.data.metadata.DataObjectMetadata; 026import org.kuali.rice.krad.data.metadata.DataObjectRelationship; 027import org.kuali.rice.krad.data.provider.annotation.UifDisplayHint; 028import org.kuali.rice.krad.data.provider.annotation.UifDisplayHintType; 029import org.kuali.rice.krad.datadictionary.AttributeDefinition; 030import org.kuali.rice.krad.datadictionary.CollectionDefinition; 031import org.kuali.rice.krad.datadictionary.DataObjectEntry; 032import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint; 033import org.kuali.rice.krad.lookup.LookupInputField; 034import org.kuali.rice.krad.lookup.LookupView; 035import org.kuali.rice.krad.service.DataDictionaryService; 036import org.kuali.rice.krad.uif.component.Component; 037import org.kuali.rice.krad.uif.container.CollectionGroup; 038import org.kuali.rice.krad.uif.container.Group; 039import org.kuali.rice.krad.uif.control.Control; 040import org.kuali.rice.krad.uif.control.HiddenControl; 041import org.kuali.rice.krad.uif.control.TextAreaControl; 042import org.kuali.rice.krad.uif.control.TextControl; 043import org.kuali.rice.krad.uif.control.UserControl; 044import org.kuali.rice.krad.uif.field.DataField; 045import org.kuali.rice.krad.uif.layout.TableLayoutManager; 046import org.kuali.rice.krad.uif.service.UifDefaultingService; 047import org.kuali.rice.krad.uif.util.ComponentFactory; 048import org.kuali.rice.krad.uif.view.InquiryView; 049import org.kuali.rice.krad.util.KRADPropertyConstants; 050 051import java.util.ArrayList; 052import java.util.Collection; 053import java.util.HashMap; 054import java.util.HashSet; 055import java.util.List; 056import java.util.Map; 057 058public class UifDefaultingServiceImpl implements UifDefaultingService { 059 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifDefaultingServiceImpl.class); 060 061 protected DataDictionaryService dataDictionaryService; 062 protected DataObjectService dataObjectService; 063 064 protected static final String ANY_CHARACTER_PATTERN_CONSTRAINT = "UTF8AnyCharacterPatternConstraint"; 065 protected static final String DATE_PATTERN_CONSTRAINT = "BasicDatePatternConstraint"; 066 protected static final String FLOATING_POINT_PATTERN_CONSTRAINT = "FloatingPointPatternConstraintTemplate"; 067 protected static final String BIG_DECIMAL_PATTERN_CONSTRAINT = "BigDecimalPatternConstraintTemplate"; 068 protected static final String TIMESTAMP_PATTERN_CONSTRAINT = "TimestampPatternConstraint"; 069 protected static final String CURRENCY_PATTERN_CONSTRAINT = "CurrencyPatternConstraint"; 070 071 @Override 072 public String deriveHumanFriendlyNameFromPropertyName(String camelCasedName) { 073 // quick check to make sure there is a property name to modify 074 if(StringUtils.isBlank(camelCasedName)) { 075 return camelCasedName; 076 } 077 078 // We only want to include the component after the last property separator 079 if (camelCasedName.contains(".")) { 080 camelCasedName = StringUtils.substringAfterLast(camelCasedName, "."); 081 } 082 083 StringBuilder label = new StringBuilder(camelCasedName); 084 085 // upper case the 1st letter 086 label.replace(0, 1, label.substring(0, 1).toUpperCase()); 087 088 // loop through, inserting spaces when cap 089 for (int i = 0; i < label.length(); i++) { 090 if (Character.isUpperCase(label.charAt(i)) || Character.isDigit(label.charAt(i)) ) { 091 label.insert(i, ' '); 092 i++; 093 } 094 } 095 096 return label.toString().trim(); 097 } 098 099 100 protected UifDisplayHint getHintOfType( DataObjectAttribute attr, UifDisplayHintType hintType ) { 101 if ( attr != null && attr.getDisplayHints() != null ) { 102 for ( UifDisplayHint hint : attr.getDisplayHints() ) { 103 if ( hint.value().equals(hintType) ) { 104 return hint; 105 } 106 } 107 } 108 109 return null; 110 } 111 112 /** 113 * Check the {@link UifDisplayHint}s on an attribute, return true if any of them have the 114 * given type. 115 * @param attr data object attribute 116 * @param hintType hint type 117 * @return true if the hint type is present on the attribute 118 */ 119 protected boolean hasHintOfType( DataObjectAttribute attr, UifDisplayHintType hintType ) { 120 return getHintOfType(attr, hintType) != null; 121 } 122 123 protected Control getControlInstance( AttributeDefinition attrDef, DataObjectAttribute dataObjectAttribute ) { 124 Control c = null; 125 // Check for the hidden hint - if present - then use that control type 126 if ( dataObjectAttribute != null && hasHintOfType(dataObjectAttribute, UifDisplayHintType.HIDDEN) ) { 127 c = ComponentFactory.getHiddenControl(); 128 } else if ( attrDef.getOptionsFinder() != null ) { 129 // if a values finder has been established, use a radio button group or drop-down list 130 if ( dataObjectAttribute != null && hasHintOfType(dataObjectAttribute, UifDisplayHintType.RADIO) ) { 131 c = ComponentFactory.getRadioGroupControl(); 132 } else { 133 c = ComponentFactory.getSelectControl(); 134 } 135 } else if ( attrDef.getName().endsWith( ".principalName" ) && dataObjectAttribute != null ) { 136 // FIXME: JHK: Yes, I know this is a *HORRIBLE* hack - but the alternative 137 // would look even more "hacky" and error-prone 138 c = ComponentFactory.getUserControl(); 139 // Need to find the relationship information 140 // get the relationship ID by removing .principalName from the attribute name 141 String relationshipName = StringUtils.removeEnd(attrDef.getName(), ".principalName"); 142 DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata( 143 dataObjectAttribute.getOwningType()); 144 if ( metadata != null ) { 145 DataObjectRelationship relationship = metadata.getRelationship(relationshipName); 146 if ( relationship != null && CollectionUtils.isNotEmpty(relationship.getAttributeRelationships())) { 147 ((UserControl)c).setPrincipalIdPropertyName(relationship.getAttributeRelationships().get(0).getParentAttributeName()); 148 ((UserControl)c).setPersonNamePropertyName(relationshipName + "." + KimConstants.AttributeConstants.NAME); 149 ((UserControl)c).setPersonObjectPropertyName(relationshipName); 150 } 151 } else { 152 LOG.warn( "Attempt to pull relationship name: " + relationshipName + " resulted in missing metadata when looking for: " + dataObjectAttribute.getOwningType() ); 153 } 154 } else { 155 switch ( attrDef.getDataType() ) { 156 case STRING : 157 // TODO: Determine better way to store the "200" metric below 158 if ( attrDef.getMaxLength() != null && attrDef.getMaxLength().intValue() > 200 ) { 159 c = ComponentFactory.getTextAreaControl(); 160 } else { 161 c = ComponentFactory.getTextControl(); 162 } 163 break; 164 case BOOLEAN: 165 c = ComponentFactory.getCheckboxControl(); 166 break; 167 case DATE: 168 case DATETIME: 169 case TRUNCATED_DATE: 170 c = ComponentFactory.getDateControl(); 171 break; 172 case CURRENCY: 173 case DOUBLE: 174 case FLOAT: 175 case INTEGER: 176 case LARGE_INTEGER: 177 case LONG: 178 case PRECISE_DECIMAL: 179 c = ComponentFactory.getTextControl(); 180 break; 181 case MARKUP: 182 c = ComponentFactory.getTextAreaControl(); 183 break; 184 default: 185 c = ComponentFactory.getTextControl(); 186 break; 187 } 188 } 189 return c; 190 } 191 192 protected void customizeControlInstance( Control c, AttributeDefinition attrDef, DataObjectAttribute dataObjectAttribute ) { 193 c.setRequired(attrDef.isRequired()); 194 if ( c instanceof TextControl ) { 195 if ( attrDef.getMaxLength() != null ) { 196 ((TextControl) c).setMaxLength( attrDef.getMaxLength() ); 197 ((TextControl) c).setSize( attrDef.getMaxLength() ); 198 // If it's a larger field, add the expand icon by default 199 if ( attrDef.getMaxLength() > 80 ) { // JHK : yes, this was a mostly arbitrary choice 200 ((TextControl) c).setTextExpand(true); 201 } 202 } 203 if ( attrDef.getMinLength() != null ) { 204 ((TextControl) c).setMinLength( attrDef.getMinLength() ); 205 } 206 } 207 if ( c instanceof TextAreaControl ) { 208 if ( attrDef.getMaxLength() != null ) { 209 ((TextAreaControl) c).setMaxLength( attrDef.getMaxLength() ); 210 ((TextAreaControl) c).setRows(attrDef.getMaxLength()/((TextAreaControl) c).getCols()); 211 } 212 if ( attrDef.getMinLength() != null ) { 213 ((TextAreaControl) c).setMinLength( attrDef.getMinLength() ); 214 } 215 } 216 } 217 218 @Override 219 public Control deriveControlAttributeFromMetadata( AttributeDefinition attrDef ) { 220 DataObjectAttribute dataObjectAttribute = attrDef.getDataObjectAttribute(); 221 Control c = getControlInstance(attrDef, dataObjectAttribute); 222 // If we a have a control...we should - but just in case - don't want to be too dependent on assumptions of the above code 223 if (c != null) { 224 customizeControlInstance(c, attrDef, dataObjectAttribute); 225 } 226 return c; 227 } 228 229 @Override 230 public ValidCharactersConstraint deriveValidCharactersConstraint(AttributeDefinition attrDef) { 231 ValidCharactersConstraint validCharactersConstraint = null; 232 233 // First - see if one was defined in the metadata (provided by krad-data module annotations) 234 if (attrDef.getDataObjectAttribute() != null) { 235 if (StringUtils.isNotBlank(attrDef.getDataObjectAttribute().getValidCharactersConstraintBeanName())) { 236 Object consObj = dataDictionaryService.getDictionaryBean(attrDef.getDataObjectAttribute() 237 .getValidCharactersConstraintBeanName()); 238 if (consObj != null && consObj instanceof ValidCharactersConstraint) { 239 validCharactersConstraint = (ValidCharactersConstraint) consObj; 240 } 241 } 242 } 243 244 // if not, make an intelligent guess from the data type 245 if (validCharactersConstraint == null) { 246 if (attrDef.getDataType() != null) { 247 if (attrDef.getDataType() == DataType.CURRENCY) { 248 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 249 .getDictionaryBean(CURRENCY_PATTERN_CONSTRAINT); 250 }else if (attrDef.getDataType() == DataType.PRECISE_DECIMAL ) { 251 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 252 .getDictionaryBean(BIG_DECIMAL_PATTERN_CONSTRAINT); 253 } else if (attrDef.getDataType().isNumeric()) { 254 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 255 .getDictionaryBean(FLOATING_POINT_PATTERN_CONSTRAINT); 256 } else if (attrDef.getDataType().isTemporal()) { 257 if (attrDef.getDataType() == DataType.DATE) { 258 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 259 .getDictionaryBean(DATE_PATTERN_CONSTRAINT); 260 } else if (attrDef.getDataType() == DataType.TIMESTAMP) { 261 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 262 .getDictionaryBean(TIMESTAMP_PATTERN_CONSTRAINT); 263 } 264 } 265 } 266 } 267 268 // default to UTF8 269 if (validCharactersConstraint == null) { 270 validCharactersConstraint = (ValidCharactersConstraint) dataDictionaryService 271 .getDictionaryBean(ANY_CHARACTER_PATTERN_CONSTRAINT); 272 } 273 274 return validCharactersConstraint; 275 } 276 277 protected Group createInquirySection( String groupId, String headerText ) { 278 Group group = ComponentFactory.getGroupWithDisclosureGridLayout(); 279 group.setId(groupId); 280 group.setHeaderText(headerText); 281 group.setItems(new ArrayList<Component>()); 282 return group; 283 } 284 285 protected CollectionGroup createCollectionInquirySection( String groupId, String headerText ) { 286 CollectionGroup group = ComponentFactory.getCollectionWithDisclosureGroupTableLayout(); 287 group.setId(groupId); 288 group.setHeaderText(headerText); 289 group.setItems(new ArrayList<Component>()); 290 ((TableLayoutManager)group.getLayoutManager()).setRenderSequenceField(false); 291 return group; 292 } 293 294 @SuppressWarnings("unchecked") 295 protected void addAttributeSectionsToInquiryView( InquiryView view, DataObjectEntry dataObjectEntry ) { 296 // Set up data structures to manage the creation of sections 297 Map<String,Group> inquirySectionsById = new HashMap<String,Group>(); 298 Group currentGroup = createInquirySection("default",dataObjectEntry.getObjectLabel()); 299 inquirySectionsById.put(currentGroup.getId(), currentGroup); 300 ((List<Group>)view.getItems()).add(currentGroup); 301 302 // Loop over the attributes on the data object, adding them into the inquiry 303 // If we have an @Section notation, switch to the section, creating if the ID is unknown 304 List<Component> items = (List<Component>) currentGroup.getItems(); // needed to deal with generics issue 305 for ( AttributeDefinition attr : dataObjectEntry.getAttributes() ) { 306 boolean dontDisplay = hasHintOfType(attr.getDataObjectAttribute(), UifDisplayHintType.NO_INQUIRY); 307 dontDisplay |= (attr.getControlField() instanceof HiddenControl); 308 // Check for a section hint 309 // Create or retrieve existing section as determined by the ID on the annotation 310 UifDisplayHint sectionHint = getHintOfType(attr.getDataObjectAttribute(), UifDisplayHintType.SECTION); 311 if ( sectionHint != null ) { 312 if ( StringUtils.isNotBlank( sectionHint.id() ) ) { 313 currentGroup = inquirySectionsById.get( sectionHint.id() ); 314 if ( currentGroup == null ) { 315 String sectionLabel = sectionHint.label(); 316 if ( StringUtils.isBlank(sectionLabel) ) { 317 sectionLabel = deriveHumanFriendlyNameFromPropertyName(sectionHint.id() ); 318 } 319 320 currentGroup = createInquirySection(sectionHint.id(), sectionHint.label()); 321 inquirySectionsById.put(currentGroup.getId(), currentGroup); 322 ((List<Group>)view.getItems()).add(currentGroup); 323 } 324 } else { 325 LOG.warn( "SECTION UifDisplayHint given without an ID - assuming 'default'" ); 326 currentGroup = inquirySectionsById.get("default"); 327 } 328 items = (List<Component>) currentGroup.getItems(); 329 } 330 331 // This is checked after the section test, since the @Section annotation 332 // would be on the FK field 333 if ( dontDisplay ) { 334 continue; 335 } 336 337 DataField dataField = ComponentFactory.getDataField(); 338 dataField.setPropertyName(attr.getName()); 339 dataField.setLabel(attr.getLabel()); 340 items.add(dataField); 341 } 342 } 343 344 @SuppressWarnings("unchecked") 345 protected void addCollectionSectionsToInquiryView( InquiryView view, DataObjectEntry dataObjectEntry ) { 346 for ( CollectionDefinition coll : dataObjectEntry.getCollections() ) { 347 // Create a new section 348 DataObjectEntry collectionEntry = dataDictionaryService.getDataDictionary().getDataObjectEntry(coll.getDataObjectClass()); 349 // Extract the key fields on the collection which are linked to the parent. 350 // When auto-generating the Inquiry Collection table, we want to exclude those. 351 Collection<String> collectionFieldsLinkedToParent = new HashSet<String>(); 352 353 if ( coll.getDataObjectCollection() != null ) { 354 for ( DataObjectAttributeRelationship rel : coll.getDataObjectCollection().getAttributeRelationships() ) { 355 collectionFieldsLinkedToParent.add(rel.getChildAttributeName()); 356 } 357 } 358 359 if ( collectionEntry == null ) { 360 LOG.warn( "Unable to find DataObjectEntry for collection class: " + coll.getDataObjectClass()); 361 continue; 362 } 363 364 CollectionGroup section = createCollectionInquirySection(coll.getName(), coll.getLabel()); 365 try { 366 section.setCollectionObjectClass(Class.forName(coll.getDataObjectClass())); 367 } catch (ClassNotFoundException e) { 368 LOG.warn( "Unable to set class on collection section - class not found: " + coll.getDataObjectClass()); 369 } 370 371 section.setPropertyName(coll.getName()); 372 // summary title : collection object label 373 // Summary fields : PK fields? 374 // add the attributes to the section 375 for ( AttributeDefinition attr : collectionEntry.getAttributes() ) { 376 boolean dontDisplay = hasHintOfType(attr.getDataObjectAttribute(), UifDisplayHintType.NO_INQUIRY); 377 dontDisplay |= (attr.getControlField() instanceof HiddenControl); 378 // Auto-exclude fields linked to the parent object 379 dontDisplay |= collectionFieldsLinkedToParent.contains( attr.getName() ); 380 381 if ( dontDisplay ) { 382 continue; 383 } 384 385 DataField dataField = ComponentFactory.getDataField(); 386 dataField.setPropertyName(attr.getName()); 387 ((List<Component>)section.getItems()).add(dataField); 388 } 389 ((List<Group>)view.getItems()).add(section); 390 } 391 } 392 /** 393 * @see org.kuali.rice.krad.uif.service.UifDefaultingService#deriveInquiryViewFromMetadata(org.kuali.rice.krad.datadictionary.DataObjectEntry) 394 */ 395 @Override 396 public InquiryView deriveInquiryViewFromMetadata(DataObjectEntry dataObjectEntry) { 397 // Create the main view object and set the title and BO class 398 InquiryView view = ComponentFactory.getInquiryView(); 399 view.setHeaderText(dataObjectEntry.getObjectLabel()); 400 view.setDataObjectClassName(dataObjectEntry.getDataObjectClass()); 401 402 addAttributeSectionsToInquiryView(view, dataObjectEntry); 403 404 // TODO: if there are updatable reference objects, include sections for them 405 406 // If there are collections on the object, include sections for them 407 addCollectionSectionsToInquiryView(view, dataObjectEntry); 408 409 return view; 410 } 411 412 protected void addAttributesToLookupCriteria( LookupView view, DataObjectEntry dataObjectEntry ) { 413 AttributeDefinition activeAttribute = null; 414 415 for ( AttributeDefinition attr : dataObjectEntry.getAttributes() ) { 416 // Check if we have been told not to display this attribute here 417 boolean dontDisplay = hasHintOfType(attr.getDataObjectAttribute(), UifDisplayHintType.NO_LOOKUP_CRITERIA); 418 dontDisplay |= (attr.getControlField() instanceof HiddenControl); 419 420 if ( dontDisplay ) { 421 continue; 422 } 423 424 if ( attr.getName().equals( KRADPropertyConstants.ACTIVE ) ) { 425 activeAttribute = attr; 426 continue; // leave until the end of the lookup criteria 427 } 428 429 LookupInputField field = ComponentFactory.getLookupCriteriaInputField(); 430 field.setPropertyName(attr.getName()); 431 field.setLabel(attr.getLabel()); 432 view.getCriteriaFields().add(field); 433 } 434 435 // If there was one, add the active attribute at the end 436 if ( activeAttribute != null ) { 437 LookupInputField field = ComponentFactory.getLookupCriteriaInputField(); 438 field.setPropertyName(activeAttribute.getName()); 439 field.setLabel(activeAttribute.getLabel()); 440 view.getCriteriaFields().add(field); 441 } 442 } 443 444 protected void addAttributesToLookupResults( LookupView view, DataObjectEntry dataObjectEntry ) { 445 AttributeDefinition activeAttribute = null; 446 447 for ( AttributeDefinition attr : dataObjectEntry.getAttributes() ) { 448 // Check if we have been told not to display this attribute here 449 boolean dontDisplay = hasHintOfType(attr.getDataObjectAttribute(), UifDisplayHintType.NO_LOOKUP_RESULT); 450 dontDisplay |= (attr.getControlField() instanceof HiddenControl); 451 452 if ( dontDisplay ) { 453 continue; 454 } 455 456 if ( attr.getName().equals( KRADPropertyConstants.ACTIVE ) ) { 457 activeAttribute = attr; 458 continue; // leave until the end of the lookup results 459 } 460 461 DataField field = ComponentFactory.getDataField(); 462 field.setPropertyName(attr.getName()); 463 view.getResultFields().add(field); 464 } 465 466 // If there was one, add the active attribute at the end 467 if ( activeAttribute != null ) { 468 DataField field = ComponentFactory.getDataField(); 469 field.setPropertyName(activeAttribute.getName()); 470 view.getResultFields().add(field); 471 } 472 } 473 474 /** 475 * @see org.kuali.rice.krad.uif.service.UifDefaultingService#deriveLookupViewFromMetadata(org.kuali.rice.krad.datadictionary.DataObjectEntry) 476 */ 477 @Override 478 public LookupView deriveLookupViewFromMetadata(DataObjectEntry dataObjectEntry) { 479 LookupView view = ComponentFactory.getLookupView(); 480 view.setHeaderText(dataObjectEntry.getObjectLabel() + " Lookup"); 481 view.setDataObjectClass(dataObjectEntry.getDataObjectClass()); 482 view.setCriteriaFields(new ArrayList<Component>()); 483 view.setResultFields(new ArrayList<Component>()); 484 view.setDefaultSortAttributeNames(dataObjectEntry.getPrimaryKeys()); 485 486 addAttributesToLookupCriteria(view, dataObjectEntry); 487 addAttributesToLookupResults(view, dataObjectEntry); 488 489 return view; 490 } 491 492 public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { 493 this.dataDictionaryService = dataDictionaryService; 494 } 495 496 public void setDataObjectService(DataObjectService dataObjectService) { 497 this.dataObjectService = dataObjectService; 498 } 499}