View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.service.impl;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.data.DataType;
21  import org.kuali.rice.kim.api.KimConstants;
22  import org.kuali.rice.krad.data.DataObjectService;
23  import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
24  import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
25  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
26  import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
27  import org.kuali.rice.krad.data.provider.annotation.UifDisplayHint;
28  import org.kuali.rice.krad.data.provider.annotation.UifDisplayHintType;
29  import org.kuali.rice.krad.datadictionary.AttributeDefinition;
30  import org.kuali.rice.krad.datadictionary.CollectionDefinition;
31  import org.kuali.rice.krad.datadictionary.DataObjectEntry;
32  import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
33  import org.kuali.rice.krad.lookup.LookupInputField;
34  import org.kuali.rice.krad.lookup.LookupView;
35  import org.kuali.rice.krad.service.DataDictionaryService;
36  import org.kuali.rice.krad.uif.component.Component;
37  import org.kuali.rice.krad.uif.container.CollectionGroup;
38  import org.kuali.rice.krad.uif.container.Group;
39  import org.kuali.rice.krad.uif.control.Control;
40  import org.kuali.rice.krad.uif.control.HiddenControl;
41  import org.kuali.rice.krad.uif.control.TextAreaControl;
42  import org.kuali.rice.krad.uif.control.TextControl;
43  import org.kuali.rice.krad.uif.control.UserControl;
44  import org.kuali.rice.krad.uif.field.DataField;
45  import org.kuali.rice.krad.uif.layout.TableLayoutManager;
46  import org.kuali.rice.krad.uif.service.UifDefaultingService;
47  import org.kuali.rice.krad.uif.util.ComponentFactory;
48  import org.kuali.rice.krad.uif.view.InquiryView;
49  import org.kuali.rice.krad.util.KRADPropertyConstants;
50  
51  import java.util.ArrayList;
52  import java.util.Collection;
53  import java.util.HashMap;
54  import java.util.HashSet;
55  import java.util.List;
56  import java.util.Map;
57  
58  public class UifDefaultingServiceImpl implements UifDefaultingService {
59      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifDefaultingServiceImpl.class);
60  
61      protected DataDictionaryService dataDictionaryService;
62      protected DataObjectService dataObjectService;
63  
64      protected static final String ANY_CHARACTER_PATTERN_CONSTRAINT = "UTF8AnyCharacterPatternConstraint";
65      protected static final String DATE_PATTERN_CONSTRAINT = "BasicDatePatternConstraint";
66      protected static final String FLOATING_POINT_PATTERN_CONSTRAINT = "FloatingPointPatternConstraintTemplate";
67      protected static final String BIG_DECIMAL_PATTERN_CONSTRAINT = "BigDecimalPatternConstraintTemplate";
68      protected static final String TIMESTAMP_PATTERN_CONSTRAINT = "TimestampPatternConstraint";
69      protected static final String CURRENCY_PATTERN_CONSTRAINT = "CurrencyPatternConstraint";
70  
71      @Override
72      public String deriveHumanFriendlyNameFromPropertyName(String camelCasedName) {
73          // quick check to make sure there is a property name to modify
74          if(StringUtils.isBlank(camelCasedName)) {
75              return camelCasedName;
76          }
77  
78          // We only want to include the component after the last property separator
79          if (camelCasedName.contains(".")) {
80              camelCasedName = StringUtils.substringAfterLast(camelCasedName, ".");
81          }
82          
83          StringBuilder label = new StringBuilder(camelCasedName);
84          
85          // upper case the 1st letter
86          label.replace(0, 1, label.substring(0, 1).toUpperCase());
87          
88          // loop through, inserting spaces when cap
89          for (int i = 0; i < label.length(); i++) {
90              if (Character.isUpperCase(label.charAt(i)) || Character.isDigit(label.charAt(i)) ) {
91                  label.insert(i, ' ');
92                  i++;
93              }
94          }
95  
96          return label.toString().trim();
97      }
98  
99  
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 }