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}