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.kns.util;
017
018import org.apache.commons.beanutils.NestedNullException;
019import org.apache.commons.beanutils.PropertyUtils;
020import org.apache.commons.lang.StringUtils;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.encryption.EncryptionService;
023import org.kuali.rice.core.api.mo.common.active.MutableInactivatable;
024import org.kuali.rice.core.api.uif.AttributeLookupSettings;
025import org.kuali.rice.core.api.uif.DataType;
026import org.kuali.rice.core.api.uif.RemotableAbstractControl;
027import org.kuali.rice.core.api.uif.RemotableAbstractWidget;
028import org.kuali.rice.core.api.uif.RemotableAttributeField;
029import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
030import org.kuali.rice.core.api.uif.RemotableCheckbox;
031import org.kuali.rice.core.api.uif.RemotableCheckboxGroup;
032import org.kuali.rice.core.api.uif.RemotableControlContract;
033import org.kuali.rice.core.api.uif.RemotableDatepicker;
034import org.kuali.rice.core.api.uif.RemotableHiddenInput;
035import org.kuali.rice.core.api.uif.RemotablePasswordInput;
036import org.kuali.rice.core.api.uif.RemotableQuickFinder;
037import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
038import org.kuali.rice.core.api.uif.RemotableSelect;
039import org.kuali.rice.core.api.uif.RemotableTextExpand;
040import org.kuali.rice.core.api.uif.RemotableTextInput;
041import org.kuali.rice.core.api.uif.RemotableTextarea;
042import org.kuali.rice.core.api.util.ClassLoaderUtils;
043import org.kuali.rice.core.api.util.ConcreteKeyValue;
044import org.kuali.rice.core.api.util.KeyValue;
045import org.kuali.rice.core.web.format.FormatException;
046import org.kuali.rice.core.web.format.Formatter;
047import org.kuali.rice.kew.api.KewApiConstants;
048import org.kuali.rice.kim.api.identity.Person;
049import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
050import org.kuali.rice.kns.datadictionary.FieldDefinition;
051import org.kuali.rice.kns.datadictionary.MaintainableCollectionDefinition;
052import org.kuali.rice.kns.datadictionary.control.ButtonControlDefinition;
053import org.kuali.rice.kns.datadictionary.control.CurrencyControlDefinition;
054import org.kuali.rice.kns.datadictionary.control.KualiUserControlDefinition;
055import org.kuali.rice.kns.datadictionary.control.LinkControlDefinition;
056import org.kuali.rice.kns.document.authorization.FieldRestriction;
057import org.kuali.rice.kns.document.authorization.MaintenanceDocumentRestrictions;
058import org.kuali.rice.kns.inquiry.Inquirable;
059import org.kuali.rice.kns.lookup.HtmlData;
060import org.kuali.rice.kns.lookup.HtmlData.AnchorHtmlData;
061import org.kuali.rice.kns.lookup.LookupUtils;
062import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
063import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
064import org.kuali.rice.kns.service.KNSServiceLocator;
065import org.kuali.rice.kns.web.comparator.CellComparatorHelper;
066import org.kuali.rice.kns.web.ui.Column;
067import org.kuali.rice.kns.web.ui.Field;
068import org.kuali.rice.kns.web.ui.PropertyRenderingConfigElement;
069import org.kuali.rice.kns.web.ui.Row;
070import org.kuali.rice.kns.web.ui.Section;
071import org.kuali.rice.krad.bo.BusinessObject;
072import org.kuali.rice.krad.bo.DataObjectRelationship;
073import org.kuali.rice.krad.bo.KualiCode;
074import org.kuali.rice.krad.bo.PersistableBusinessObject;
075import org.kuali.rice.krad.datadictionary.control.ControlDefinition;
076import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
077import org.kuali.rice.krad.datadictionary.mask.MaskFormatter;
078import org.kuali.rice.krad.keyvalues.IndicatorValuesFinder;
079import org.kuali.rice.krad.keyvalues.KeyValuesFinder;
080import org.kuali.rice.krad.keyvalues.PersistableBusinessObjectValuesFinder;
081import org.kuali.rice.krad.service.DataDictionaryService;
082import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
083import org.kuali.rice.krad.service.KualiModuleService;
084import org.kuali.rice.krad.service.ModuleService;
085import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
086import org.kuali.rice.krad.util.GlobalVariables;
087import org.kuali.rice.krad.util.KRADConstants;
088import org.kuali.rice.krad.util.KRADPropertyConstants;
089import org.kuali.rice.krad.util.MessageMap;
090import org.kuali.rice.krad.util.ObjectUtils;
091import org.kuali.rice.krad.valuefinder.ValueFinder;
092
093import java.lang.reflect.InvocationTargetException;
094import java.security.GeneralSecurityException;
095import java.util.ArrayList;
096import java.util.Collection;
097import java.util.Collections;
098import java.util.HashMap;
099import java.util.Iterator;
100import java.util.LinkedHashMap;
101import java.util.List;
102import java.util.Map;
103
104
105/**
106 * This class is used to build Field objects from underlying data dictionary and general utility methods for handling fields.
107 */
108public final class FieldUtils {
109    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FieldUtils.class);
110    private static DataDictionaryService dataDictionaryService = null;
111    private static BusinessObjectMetaDataService businessObjectMetaDataService = null;
112    private static BusinessObjectDictionaryService businessObjectDictionaryService = null;
113    private static KualiModuleService kualiModuleService = null;
114    
115        private FieldUtils() {
116                throw new UnsupportedOperationException("do not call");
117        }
118
119    public static void setInquiryURL(Field field, BusinessObject bo, String propertyName) {
120        HtmlData inquiryHref = new AnchorHtmlData(KRADConstants.EMPTY_STRING, KRADConstants.EMPTY_STRING);
121
122        Boolean b = getBusinessObjectDictionaryService().noInquiryFieldInquiry(bo.getClass(), propertyName);
123        if (b == null || !b.booleanValue()) {
124            Class<Inquirable> inquirableClass = getBusinessObjectDictionaryService().getInquirableClass(bo.getClass());
125            Boolean b2 = getBusinessObjectDictionaryService().forceLookupResultFieldInquiry(bo.getClass(), propertyName);
126            Inquirable inq = null;
127            try {
128                if ( inquirableClass != null ) {
129                    inq = inquirableClass.newInstance();
130                } else {
131                    inq = KNSServiceLocator.getKualiInquirable();
132                    if ( LOG.isDebugEnabled() ) {
133                        LOG.debug( "Default Inquirable Class: " + inq.getClass() );
134        }
135                }
136
137                inquiryHref = inq.getInquiryUrl(bo, propertyName, null == b2 ? false : b2.booleanValue() );
138
139            } catch ( Exception ex ) {
140                LOG.error("unable to create inquirable to get inquiry URL", ex );
141            }
142        }
143
144        field.setInquiryURL(inquiryHref);
145    }
146
147        /**
148         * Sets the control on the field based on the data dictionary definition
149         *
150         * @param businessObjectClass
151         *            - business object class for the field attribute
152         * @param attributeName
153         *            - name of the attribute whose {@link Field} is being set
154         * @param convertForLookup
155         *            - whether the field is being build for lookup search which impacts the control chosen
156         * @param field
157         *            - {@link Field} to set control on
158         */
159        public static void setFieldControl(Class businessObjectClass, String attributeName, boolean convertForLookup,
160                        Field field) {
161                ControlDefinition control = getDataDictionaryService().getAttributeControlDefinition(businessObjectClass,
162                                attributeName);
163                String fieldType = Field.TEXT;
164
165                if (control != null) {
166                        if (control.isSelect()) {
167                                if (control.getScript() != null && control.getScript().length() > 0) {
168                                        fieldType = Field.DROPDOWN_SCRIPT;
169                                        field.setScript(control.getScript());
170                                } else {
171                                        fieldType = Field.DROPDOWN;
172                                }
173                        }
174
175                        if (control.isMultiselect()) {
176                                fieldType = Field.MULTISELECT;
177                        }
178
179                        if (control.isCheckbox()) {
180                                fieldType = Field.CHECKBOX;
181                        }
182
183                        if (control.isRadio()) {
184                                fieldType = Field.RADIO;
185                        }
186
187                        if (control.isHidden()) {
188                                fieldType = Field.HIDDEN;
189                        }
190
191                        if (control.isKualiUser()) {
192                                fieldType = Field.KUALIUSER;
193                                KualiUserControlDefinition kualiUserControl = (KualiUserControlDefinition) control;
194                                field.setUniversalIdAttributeName(kualiUserControl.getUniversalIdAttributeName());
195                                field.setUserIdAttributeName(kualiUserControl.getUserIdAttributeName());
196                                field.setPersonNameAttributeName(kualiUserControl.getPersonNameAttributeName());
197                        }
198
199                        if (control.isWorkflowWorkgroup()) {
200                                fieldType = Field.WORKFLOW_WORKGROUP;
201                        }
202
203                        if (control.isFile()) {
204                                fieldType = Field.FILE;
205                        }
206
207                        if (control.isTextarea() && !convertForLookup) {
208                                fieldType = Field.TEXT_AREA;
209                        }
210
211                        if (control.isLookupHidden()) {
212                                fieldType = Field.LOOKUP_HIDDEN;
213                        }
214
215                        if (control.isLookupReadonly()) {
216                                fieldType = Field.LOOKUP_READONLY;
217                        }
218
219                        if (control.isCurrency()) {
220                                fieldType = Field.CURRENCY;
221                        }
222
223                        if (control.isButton()) {
224                                fieldType = Field.BUTTON;
225                        }
226
227                        if (control.isLink()) {
228                                fieldType = Field.LINK;
229                        }
230
231                        if (Field.CURRENCY.equals(fieldType) && control instanceof CurrencyControlDefinition) {
232                                CurrencyControlDefinition currencyControl = (CurrencyControlDefinition) control;
233                                field.setStyleClass("amount");
234                                field.setSize(currencyControl.getSize());
235                                field.setFormattedMaxLength(currencyControl.getFormattedMaxLength());
236                        }
237
238                        // for text controls, set size attribute
239                        if (Field.TEXT.equals(fieldType)) {
240                                Integer size = control.getSize();
241                                if (size != null) {
242                                        field.setSize(size.intValue());
243                                } else {
244                                        field.setSize(30);
245                                }
246                                field.setDatePicker(control.isDatePicker());
247                                field.setRanged(control.isRanged());
248                        }
249
250                        if (Field.WORKFLOW_WORKGROUP.equals(fieldType)) {
251                                Integer size = control.getSize();
252                                if (size != null) {
253                                        field.setSize(size.intValue());
254                                } else {
255                                        field.setSize(30);
256                                }
257                        }
258
259                        // for text area controls, set rows and cols attributes
260                        if (Field.TEXT_AREA.equals(fieldType)) {
261                                Integer rows = control.getRows();
262                                if (rows != null) {
263                                        field.setRows(rows.intValue());
264                                } else {
265                                        field.setRows(3);
266                                }
267
268                                Integer cols = control.getCols();
269                                if (cols != null) {
270                                        field.setCols(cols.intValue());
271                                } else {
272                                        field.setCols(40);
273                                }
274                                field.setExpandedTextArea(control.isExpandedTextArea());
275                        }
276
277            if (Field.MULTISELECT.equals(fieldType)) {
278                Integer size = control.getSize();
279                                if (size != null) {
280                                        field.setSize(size.intValue());
281                                }
282            }
283
284                        // for dropdown and radio, get instance of specified KeyValuesFinder and set field values
285                        if (Field.DROPDOWN.equals(fieldType) || Field.RADIO.equals(fieldType)
286                                        || Field.DROPDOWN_SCRIPT.equals(fieldType)
287                                        || Field.MULTISELECT.equals(fieldType)) {
288                                String keyFinderClassName = control.getValuesFinderClass();
289
290                                if (StringUtils.isNotBlank(keyFinderClassName)) {
291                                        try {
292                                                Class keyFinderClass = ClassLoaderUtils.getClass(keyFinderClassName);
293                                                KeyValuesFinder finder = (KeyValuesFinder) keyFinderClass.newInstance();
294
295                                                if (finder != null) {
296                                                        if (finder instanceof PersistableBusinessObjectValuesFinder) {
297                                                                ((PersistableBusinessObjectValuesFinder) finder)
298                                                                                .setBusinessObjectClass(ClassLoaderUtils.getClass(control
299                                                .getBusinessObjectClass()));
300                                                                ((PersistableBusinessObjectValuesFinder) finder).setKeyAttributeName(control
301                                        .getKeyAttribute());
302                                                                ((PersistableBusinessObjectValuesFinder) finder).setLabelAttributeName(control
303                                                                                .getLabelAttribute());
304                                                                if (control.getIncludeBlankRow() != null) {
305                                                                        ((PersistableBusinessObjectValuesFinder) finder).setIncludeBlankRow(control
306                                                                                        .getIncludeBlankRow());
307                                                                }
308                                                                ((PersistableBusinessObjectValuesFinder) finder).setIncludeKeyInDescription(control
309                                        .getIncludeKeyInLabel());
310                                                        }
311                                                        field.setFieldValidValues(finder.getKeyValues());
312                                                        field.setFieldInactiveValidValues(finder.getKeyValues(false));
313                                                }
314                                        } catch (InstantiationException e) {
315                                                LOG.error("Unable to get new instance of finder class: " + keyFinderClassName);
316                                                throw new RuntimeException("Unable to get new instance of finder class: " + keyFinderClassName);
317                                        } catch (IllegalAccessException e) {
318                                                LOG.error("Unable to get new instance of finder class: " + keyFinderClassName);
319                                                throw new RuntimeException("Unable to get new instance of finder class: " + keyFinderClassName);
320                                        }
321                                }
322                        }
323
324                        if (Field.CHECKBOX.equals(fieldType) && convertForLookup) {
325                                fieldType = Field.RADIO;
326                                field.setFieldValidValues(IndicatorValuesFinder.INSTANCE.getKeyValues());
327                        }
328
329                        // for button control
330                        if (Field.BUTTON.equals(fieldType)) {
331                                ButtonControlDefinition buttonControl = (ButtonControlDefinition) control;
332                                field.setImageSrc(buttonControl.getImageSrc());
333                                field.setStyleClass(buttonControl.getStyleClass());
334                        }
335
336                        // for link control
337                        if (Field.LINK.equals(fieldType)) {
338                                LinkControlDefinition linkControl = (LinkControlDefinition) control;
339                                field.setStyleClass(linkControl.getStyleClass());
340                                field.setTarget(linkControl.getTarget());
341                                field.setHrefText(linkControl.getHrefText());
342                        }
343
344                }
345
346                field.setFieldType(fieldType);
347        }
348
349
350    /**
351     * Builds up a Field object based on the propertyName and business object class.
352     *
353     * See KULRICE-2480 for info on convertForLookup flag
354     *
355     */
356    public static Field getPropertyField(Class businessObjectClass, String attributeName, boolean convertForLookup) {
357        Field field = new Field();
358        field.setPropertyName(attributeName);
359        
360        //hack to get correct BO impl in case of ebos....
361        if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObject(businessObjectClass)) {
362            ModuleService moduleService = getKualiModuleService().getResponsibleModuleService(businessObjectClass);
363            businessObjectClass = moduleService.getExternalizableBusinessObjectDictionaryEntry(businessObjectClass).getDataObjectClass();
364        }
365        
366        field.setFieldLabel(getDataDictionaryService().getAttributeLabel(businessObjectClass, attributeName));
367
368        setFieldControl(businessObjectClass, attributeName, convertForLookup, field);
369
370        Boolean fieldRequired = getBusinessObjectDictionaryService().getLookupAttributeRequired(businessObjectClass, attributeName);
371        if (fieldRequired != null) {
372            field.setFieldRequired(fieldRequired.booleanValue());
373        }
374
375        Integer maxLength = getDataDictionaryService().getAttributeMaxLength(businessObjectClass, attributeName);
376        if (maxLength != null) {
377            field.setMaxLength(maxLength.intValue());
378        }
379
380        Boolean upperCase = null;
381        try {
382            upperCase = getDataDictionaryService().getAttributeForceUppercase(businessObjectClass, attributeName);
383        }
384        catch (UnknownBusinessClassAttributeException t) {
385                // do nothing
386                LOG.warn( "UnknownBusinessClassAttributeException in fieldUtils.getPropertyField() : " + t.getMessage() );
387        }
388        if (upperCase != null) {
389            field.setUpperCase(upperCase.booleanValue());
390        }
391        
392                if (!businessObjectClass.isInterface()) {
393                        try {
394                                field.setFormatter(
395                        ObjectUtils.getFormatterWithDataDictionary(businessObjectClass.newInstance(), attributeName));
396                        } catch (InstantiationException e) {
397                                LOG.info("Unable to get new instance of business object class: " + businessObjectClass.getName(), e);
398                                // just swallow exception and leave formatter blank
399                        } catch (IllegalAccessException e) {
400                                LOG.info("Unable to get new instance of business object class: " + businessObjectClass.getName(), e);
401                                // just swallow exception and leave formatter blank
402                        }
403                }
404
405        // set Field help properties
406        field.setBusinessObjectClassName(businessObjectClass.getName());
407        field.setFieldHelpName(attributeName);
408        field.setFieldHelpSummary(getDataDictionaryService().getAttributeSummary(businessObjectClass, attributeName));
409
410        return field;
411    }
412
413        /**
414         * For attributes that are codes (determined by whether they have a
415         * reference to a KualiCode bo and similar naming) sets the name as an
416         * additional display property
417         * 
418         * @param businessObjectClass -
419         *            class containing attribute
420         * @param attributeName - 
421         *            name of attribute in the business object
422         * @param field - 
423         *            property display element
424         */
425        public static void setAdditionalDisplayPropertyForCodes(Class businessObjectClass, String attributeName, PropertyRenderingConfigElement field) {
426                try {
427                        DataObjectRelationship relationship = getBusinessObjectMetaDataService().getBusinessObjectRelationship(
428                                        (BusinessObject) businessObjectClass.newInstance(), attributeName);
429
430                        if (relationship != null && attributeName.startsWith(relationship.getParentAttributeName())
431                                        && KualiCode.class.isAssignableFrom(relationship.getRelatedClass())) {
432                                field.setAdditionalDisplayPropertyName(relationship.getParentAttributeName() + "."
433                                                + KRADPropertyConstants.NAME);
434                        }
435                } catch (Exception e) {
436                        throw new RuntimeException("Cannot get new instance of class to check for KualiCode references: "
437                                        + e.getMessage());
438                }
439        }
440
441
442    /**
443     * Wraps each Field in the list into a Row.
444     *
445     * @param fields
446     * @return List of Row objects
447     */
448    public static List wrapFields(List fields) {
449        return wrapFields(fields, KRADConstants.DEFAULT_NUM_OF_COLUMNS);
450    }
451
452    /**
453     * This method is to implement multiple columns where the numberOfColumns is obtained from data dictionary.
454     *
455     * @param fields
456     * @param numberOfColumns
457     * @return
458     */
459    public static List<Row> wrapFields(List<Field> fields, int numberOfColumns) {
460
461        List<Row> rows = new ArrayList();
462        List<Field> fieldOnlyList = new ArrayList();
463
464        List<Field> visableFields = getVisibleFields(fields);
465        List<Field> nonVisableFields = getNonVisibleFields(fields);
466
467        int fieldsPosition = 0;
468        for (Field element : visableFields) {
469            if (Field.SUB_SECTION_SEPARATOR.equals(element.getFieldType()) || Field.CONTAINER.equals(element.getFieldType())) {
470                fieldsPosition = createBlankSpace(fieldOnlyList, rows, numberOfColumns, fieldsPosition);
471                List fieldList = new ArrayList();
472                fieldList.add(element);
473                rows.add(new Row(fieldList));
474            }
475            else {
476                if (fieldsPosition < numberOfColumns) {
477                    fieldOnlyList.add(element);
478                    fieldsPosition++;
479                }
480                else {
481                    rows.add(new Row(new ArrayList(fieldOnlyList)));
482                    fieldOnlyList.clear();
483                    fieldOnlyList.add(element);
484                    fieldsPosition = 1;
485                }
486            }
487        }
488        createBlankSpace(fieldOnlyList, rows, numberOfColumns, fieldsPosition);
489
490     // Add back the non Visible Rows
491        if(nonVisableFields != null && !nonVisableFields.isEmpty()){
492                Row nonVisRow = new Row();
493                nonVisRow.setFields(nonVisableFields);
494                rows.add(nonVisRow);
495        }
496
497
498        return rows;
499    }
500
501    private static List<Field> getVisibleFields(List<Field> fields){
502        List<Field> rList = new ArrayList<Field>();
503
504                for(Field f: fields){
505                        if(!Field.HIDDEN.equals(f.getFieldType()) &&  !Field.BLANK_SPACE.equals(f.getFieldType())){
506                                rList.add(f);
507                        }
508                }
509
510        return rList;
511    }
512
513    private static List<Field> getNonVisibleFields(List<Field> fields){
514        List<Field> rList = new ArrayList<Field>();
515
516                for(Field f: fields){
517                        if(Field.HIDDEN.equals(f.getFieldType()) || Field.BLANK_SPACE.equals(f.getFieldType())){
518                                rList.add(f);
519                        }
520                }
521
522        return rList;
523    }
524
525    /**
526     * This is a helper method to create and add a blank space to the fieldOnly List.
527     *
528     * @param fieldOnlyList
529     * @param rows
530     * @param numberOfColumns
531     * @return fieldsPosition
532     */
533    private static int createBlankSpace(List<Field> fieldOnlyList, List<Row> rows, int numberOfColumns, int fieldsPosition) {
534        int fieldOnlySize = fieldOnlyList.size();
535        if (fieldOnlySize > 0) {
536            for (int i = 0; i < (numberOfColumns - fieldOnlySize); i++) {
537                Field empty = new Field();
538                empty.setFieldType(Field.BLANK_SPACE);
539                // Must be set or AbstractLookupableHelperServiceImpl::preprocessDateFields dies
540                empty.setPropertyName(Field.BLANK_SPACE);
541                fieldOnlyList.add(empty);
542            }
543            rows.add(new Row(new ArrayList(fieldOnlyList)));
544            fieldOnlyList.clear();
545            fieldsPosition = 0;
546        }
547        return fieldsPosition;
548    }
549
550    /**
551     * Wraps list of fields into a Field of type CONTAINER
552     *
553     * @param name name for the field
554     * @param label label for the field
555     * @param fields list of fields that should be contained in the container
556     * @return Field of type CONTAINER
557     */
558    public static Field constructContainerField(String name, String label, List fields) {
559        return constructContainerField(name, label, fields, KRADConstants.DEFAULT_NUM_OF_COLUMNS);
560    }
561
562    /**
563     * Wraps list of fields into a Field of type CONTAINER and arrange them into multiple columns.
564     *
565     * @param name name for the field
566     * @param label label for the field
567     * @param fields list of fields that should be contained in the container
568     * @param numberOfColumns the number of columns for each row that the fields should be arranged into
569     * @return Field of type CONTAINER
570     */
571    public static Field constructContainerField(String name, String label, List fields, int numberOfColumns) {
572        Field containerField = new Field();
573        containerField.setPropertyName(name);
574        containerField.setFieldLabel(label);
575        containerField.setFieldType(Field.CONTAINER);
576        containerField.setNumberOfColumnsForCollection(numberOfColumns);
577
578        List rows = wrapFields(fields, numberOfColumns);
579        containerField.setContainerRows(rows);
580
581        return containerField;
582    }
583
584    /**
585     * Uses reflection to get the property names of the business object, then checks for a matching field property name. If found,
586     * takes the value of the business object property and populates the field value. Iterates through for all fields in the list.
587     *
588     * @param fields list of Field object to populate
589     * @param bo business object to get field values from
590     * @return List of fields with values populated from business object.
591     */
592    public static List<Field> populateFieldsFromBusinessObject(List<Field> fields, BusinessObject bo) {
593        List<Field> populatedFields = new ArrayList<Field>();
594
595        if (bo instanceof PersistableBusinessObject) {
596                ((PersistableBusinessObject) bo).refreshNonUpdateableReferences();
597        }
598        
599        for (Iterator<Field> iter = fields.iterator(); iter.hasNext();) {
600            Field element = iter.next();
601            if (element.containsBOData()) {
602                String propertyName = element.getPropertyName();
603
604                // See: https://test.kuali.org/jira/browse/KULCOA-1185
605                // Properties that could not possibly be set by the BusinessObject should be ignored.
606                // (https://test.kuali.org/jira/browse/KULRNE-4354; this code was killing the src attribute of IMAGE_SUBMITs).
607                if (isPropertyNested(propertyName) && !isObjectTreeNonNullAllTheWayDown(bo, propertyName) && ((!element.getFieldType().equals(Field.IMAGE_SUBMIT)) && !(element.getFieldType().equals(Field.CONTAINER)) && (!element.getFieldType().equals(Field.QUICKFINDER)))) {
608                    element.setPropertyValue(null);
609                }
610                else if (isPropertyReadable(bo, propertyName)) {
611                        populateReadableField(element, bo);
612                }
613                
614                        if (StringUtils.isNotBlank(element.getAlternateDisplayPropertyName())) {
615                                String alternatePropertyValue = ObjectUtils.getFormattedPropertyValueUsingDataDictionary(bo, element
616                                                .getAlternateDisplayPropertyName());
617                                element.setAlternateDisplayPropertyValue(alternatePropertyValue);
618                        }
619
620                        if (StringUtils.isNotBlank(element.getAdditionalDisplayPropertyName())) {
621                                String additionalPropertyValue = ObjectUtils.getFormattedPropertyValueUsingDataDictionary(bo, element
622                                                .getAdditionalDisplayPropertyName());
623                                element.setAdditionalDisplayPropertyValue(additionalPropertyValue);
624                        }
625            }
626            populatedFields.add(element);
627        }
628
629        return populatedFields;
630    }
631    
632    private static boolean isPropertyReadable(Object bean, String name) {
633        try {
634            return PropertyUtils.isReadable(bean, name);
635        } catch (NestedNullException e) {
636            return false;
637        }
638    }
639
640    private static boolean isPropertyWritable(Object bean, String name) {
641        try {
642            return PropertyUtils.isWriteable(bean, name);
643        } catch (NestedNullException e) {
644            return false;
645        }
646    }
647
648    public static void populateReadableField(Field field, BusinessObject businessObject){
649                Object obj = ObjectUtils.getNestedValue(businessObject, field.getPropertyName());
650
651        // For files the FormFile is not being persisted instead the file data is stored in
652                // individual fields as defined by PersistableAttachment.
653            if (Field.FILE.equals(field.getFieldType())) {
654            Object fileName = ObjectUtils.getNestedValue(businessObject, KRADConstants.BO_ATTACHMENT_FILE_NAME);
655            Object fileType = ObjectUtils.getNestedValue(businessObject, KRADConstants.BO_ATTACHMENT_FILE_CONTENT_TYPE);
656            field.setImageSrc(WebUtils.getAttachmentImageForUrl((String)fileType));
657            field.setPropertyValue(fileName);
658        }
659
660        if (obj != null) {
661                        String formattedValue = ObjectUtils.getFormattedPropertyValueUsingDataDictionary(businessObject, field.getPropertyName());
662                        field.setPropertyValue(formattedValue);
663                
664            // for user fields, attempt to pull the principal ID and person's name from the source object
665            if ( field.getFieldType().equals(Field.KUALIUSER) ) {
666                // this is supplemental, so catch and log any errors
667                try {
668                        if ( StringUtils.isNotBlank(field.getUniversalIdAttributeName()) ) {
669                                Object principalId = ObjectUtils.getNestedValue(businessObject, field.getUniversalIdAttributeName());
670                                if ( principalId != null ) {
671                                        field.setUniversalIdValue(principalId.toString());
672                                }
673                        }
674                        if ( StringUtils.isNotBlank(field.getPersonNameAttributeName()) ) {
675                                Object personName = ObjectUtils.getNestedValue(businessObject, field.getPersonNameAttributeName());
676                                if ( personName != null ) {
677                                        field.setPersonNameValue( personName.toString() );
678                                }
679                        }
680                } catch ( Exception ex ) {
681                        LOG.warn( "Unable to get principal ID or person name property in FieldBridge.", ex );
682                }
683            }
684        }
685        
686        populateSecureField(field, obj);
687    }
688
689    public static void populateSecureField(Field field, Object fieldValue){
690        // set encrypted & masked value if user does not have permission to see real value in UI
691        // element.isSecure() => a non-null AttributeSecurity object is set in the field
692        if (field.isSecure()) {
693            try {
694                if (fieldValue != null && fieldValue.toString().endsWith(EncryptionService.HASH_POST_PREFIX)) {
695                        field.setEncryptedValue(fieldValue.toString());
696                }
697                else {
698                    if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
699                            field.setEncryptedValue(CoreApiServiceLocator.getEncryptionService().encrypt(fieldValue) + EncryptionService.ENCRYPTION_POST_PREFIX);
700                    }
701                }
702            }
703            catch (GeneralSecurityException e) {
704                throw new RuntimeException("Unable to encrypt secure field " + e.getMessage());
705            }
706            //field.setDisplayMaskValue(field.getAttributeSecurity().getDisplayMaskValue(fieldValue));
707        }
708    }
709
710    /**
711     * This method indicates whether or not propertyName refers to a nested attribute.
712     *
713     * @param propertyName
714     * @return true if propertyName refers to a nested property (e.g. "x.y")
715     */
716    static private boolean isPropertyNested(String propertyName) {
717        return -1 != propertyName.indexOf('.');
718    }
719
720    /**
721     * This method verifies that all of the parent objects of propertyName are non-null.
722     *
723     * @param bo
724     * @param propertyName
725     * @return true if all parents are non-null, otherwise false
726     */
727
728    static private boolean isObjectTreeNonNullAllTheWayDown(BusinessObject bo, String propertyName) {
729        String[] propertyParts = propertyName.split("\\.");
730
731        StringBuffer property = new StringBuffer();
732        for (int i = 0; i < propertyParts.length - 1; i++) {
733
734            property.append((0 == property.length()) ? "" : ".").append(propertyParts[i]);
735            try {
736                if (null == PropertyUtils.getNestedProperty(bo, property.toString())) {
737                    return false;
738                }
739            }
740            catch (Throwable t) {
741                LOG.debug("Either getter or setter not specified for property \"" + property.toString() + "\"", t);
742                return false;
743            }
744        }
745
746        return true;
747
748    }
749
750    /**
751     * @param bo
752     * @param propertyName
753     * @return true if one (or more) of the intermediate objects in the given propertyName is null
754     */
755    private static boolean containsIntermediateNull(Object bo, String propertyName) {
756        boolean containsNull = false;
757
758        if (StringUtils.contains(propertyName, ".")) {
759            String prefix = StringUtils.substringBefore(propertyName, ".");
760            Object propertyValue = ObjectUtils.getPropertyValue(bo, prefix);
761
762            if (propertyValue == null) {
763                containsNull = true;
764            }
765            else {
766                String suffix = StringUtils.substringAfter(propertyName, ".");
767                containsNull = containsIntermediateNull(propertyValue, suffix);
768            }
769        }
770
771        return containsNull;
772    }
773
774    /**
775     * Uses reflection to get the property names of the business object, then checks for the property name as a key in the passed
776     * map. If found, takes the value from the map and sets the business object property.
777     *
778     * @param bo
779     * @param fieldValues
780     * @return Cached Values from any formatting failures
781     */
782    public static Map populateBusinessObjectFromMap(BusinessObject bo, Map fieldValues) {
783        return populateBusinessObjectFromMap(bo, fieldValues, "");
784    }
785
786    /**
787     * Uses reflection to get the property names of the business object, then checks for the property name as a key in the passed
788     * map. If found, takes the value from the map and sets the business object property.
789     *
790     * @param bo
791     * @param fieldValues
792     * @param propertyNamePrefix this value will be prepended to all property names in the returned unformattable values map
793     * @return Cached Values from any formatting failures
794     */
795    public static Map populateBusinessObjectFromMap(BusinessObject bo, Map<String, ?> fieldValues, String propertyNamePrefix) {
796        Map cachedValues = new HashMap();
797        MessageMap errorMap = GlobalVariables.getMessageMap();
798
799        try {
800            for (Iterator<String> iter = fieldValues.keySet().iterator(); iter.hasNext();) {
801                String propertyName = iter.next();
802
803                if (propertyName.endsWith(KRADConstants.CHECKBOX_PRESENT_ON_FORM_ANNOTATION)) {
804                    // since checkboxes do not post values when unchecked, this code detects whether a checkbox was unchecked, and
805                    // sets the value to false.
806                    if (StringUtils.isNotBlank((String) fieldValues.get(propertyName))) {
807                        String checkboxName = StringUtils.removeEnd(propertyName, KRADConstants.CHECKBOX_PRESENT_ON_FORM_ANNOTATION);
808                        String checkboxValue = (String) fieldValues.get(checkboxName);
809                        if (checkboxValue == null) {
810                            // didn't find a checkbox value, assume that it is unchecked
811                            if (isPropertyWritable(bo, checkboxName)) {
812                                Class type = ObjectUtils.easyGetPropertyType(bo, checkboxName);
813                                if (type == Boolean.TYPE || type == Boolean.class) {
814                                    // ASSUMPTION: unchecked means false
815                                    ObjectUtils.setObjectProperty(bo, checkboxName, type, "false");
816                                }
817                            }
818                        }
819                    }
820                    // else, if not null, then it has a value, and we'll let the rest of the code handle it when the param is processed on
821                    // another iteration (may be before or after this iteration).
822                }
823                else if (isPropertyWritable(bo, propertyName) && fieldValues.get(propertyName) != null ) {
824                    // if the field propertyName is a valid property on the bo class
825                    Class type = ObjectUtils.easyGetPropertyType(bo, propertyName);
826                    try {
827                        Object fieldValue = fieldValues.get(propertyName);
828                        ObjectUtils.setObjectProperty(bo, propertyName, type, fieldValue);
829                    }
830                    catch (FormatException e) {
831                        cachedValues.put(propertyNamePrefix + propertyName, fieldValues.get(propertyName));
832                        errorMap.putError(propertyNamePrefix + propertyName, e.getErrorKey(), e.getErrorArgs());
833                    }
834                }
835            }
836        }
837        catch (IllegalAccessException e) {
838            LOG.error("unable to populate business object" + e.getMessage());
839            throw new RuntimeException(e.getMessage(), e);
840        }
841        catch (InvocationTargetException e) {
842            LOG.error("unable to populate business object" + e.getMessage());
843            throw new RuntimeException(e.getMessage(), e);
844        }
845        catch (NoSuchMethodException e) {
846            LOG.error("unable to populate business object" + e.getMessage());
847            throw new RuntimeException(e.getMessage(), e);
848        }
849
850        return cachedValues;
851    }
852
853    /**
854     * Does prefixing and read only settings of a Field UI for display in a maintenance document.
855     *
856     * @param field - the Field object to be displayed
857     * @param keyFieldNames - Primary key property names for the business object being maintained.
858     * @param namePrefix - String to prefix Field names with.
859     * @param maintenanceAction - The maintenance action requested.
860     * @param readOnly - Indicates whether all fields should be read only.
861     * @return Field
862     */
863    public static Field fixFieldForForm(Field field, List keyFieldNames, String namePrefix, String maintenanceAction, boolean readOnly, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
864        String propertyName = field.getPropertyName();
865        // We only need to do the following processing if the field is not a sub section header
866        if (field.containsBOData()) {
867
868            // don't prefix submit fields, must start with dispatch parameter name
869            if (!propertyName.startsWith(KRADConstants.DISPATCH_REQUEST_PARAMETER)) {
870                // if the developer hasn't set a specific prefix use the one supplied
871                if (field.getPropertyPrefix() == null || field.getPropertyPrefix().equals("")) {
872                    field.setPropertyName(namePrefix + propertyName);
873                }
874                else {
875                    field.setPropertyName(field.getPropertyPrefix() + "." + propertyName);
876                }
877            }
878
879            if (readOnly) {
880                field.setReadOnly(true);
881            }
882
883            // set keys read only for edit
884            if ( KRADConstants.MAINTENANCE_EDIT_ACTION.equals(maintenanceAction) ) {
885                if (keyFieldNames.contains(propertyName) ) {
886                        field.setReadOnly(true);
887                        field.setKeyField(true);
888                    } else if ( StringUtils.isNotBlank( field.getUniversalIdAttributeName() )
889                                && keyFieldNames.contains(field.getUniversalIdAttributeName() ) ) {
890                        // special handling for when the principal ID is the PK field for a record
891                        // this causes locking down of the user ID field
892                        field.setReadOnly(true);
893                        field.setKeyField(true);
894                    }
895            }
896
897            // apply any authorization restrictions to field availability on the UI
898            applyAuthorization(field, maintenanceAction, auths, documentStatus, documentInitiatorPrincipalId);
899
900            // if fieldConversions specified, prefix with new constant
901            if (StringUtils.isNotBlank(field.getFieldConversions())) {
902                String fieldConversions = field.getFieldConversions();
903                String newFieldConversions = KRADConstants.EMPTY_STRING;
904                String[] conversions = StringUtils.split(fieldConversions, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
905
906                for (int l = 0; l < conversions.length; l++) {
907                    String conversion = conversions[l];
908                    //String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
909                    String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
910                    String conversionFrom = conversionPair[0];
911                    String conversionTo = conversionPair[1];
912                    conversionTo = KRADConstants.MAINTENANCE_NEW_MAINTAINABLE + conversionTo;
913                    newFieldConversions += (conversionFrom + KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR + conversionTo);
914
915                    if (l < conversions.length) {
916                        newFieldConversions += KRADConstants.FIELD_CONVERSIONS_SEPARATOR;
917                    }
918                }
919
920                field.setFieldConversions(newFieldConversions);
921            }
922
923            // if inquiryParameters specified, prefix with new constant
924            if (StringUtils.isNotBlank(field.getInquiryParameters())) {
925                String inquiryParameters = field.getInquiryParameters();
926                StringBuilder newInquiryParameters = new StringBuilder();
927                String[] parameters = StringUtils.split(inquiryParameters, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
928
929                for (int l = 0; l < parameters.length; l++) {
930                    String parameter = parameters[l];
931                    //String[] parameterPair = StringUtils.split(parameter, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
932                    String[] parameterPair = StringUtils.split(parameter, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
933                    String conversionFrom = parameterPair[0];
934                    String conversionTo = parameterPair[1];
935
936                    // append the conversionFrom string, prefixed by document.newMaintainable
937                    newInquiryParameters.append(KRADConstants.MAINTENANCE_NEW_MAINTAINABLE).append(conversionFrom);
938
939                    newInquiryParameters.append(KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR).append(conversionTo);
940
941                    if (l < parameters.length - 1) {
942                        newInquiryParameters.append(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
943                    }
944                }
945
946                field.setInquiryParameters(newInquiryParameters.toString());
947            }
948
949            if (Field.KUALIUSER.equals(field.getFieldType())) {
950                // prefix the personNameAttributeName
951                int suffixIndex = field.getPropertyName().indexOf( field.getUserIdAttributeName() );
952                if ( suffixIndex != -1 ) {
953                        field.setPersonNameAttributeName( field.getPropertyName().substring( 0, suffixIndex ) + field.getPersonNameAttributeName() );
954                        field.setUniversalIdAttributeName( field.getPropertyName().substring( 0, suffixIndex ) + field.getUniversalIdAttributeName() );
955                } else {
956                        field.setPersonNameAttributeName(namePrefix + field.getPersonNameAttributeName());
957                        field.setUniversalIdAttributeName(namePrefix + field.getUniversalIdAttributeName());
958                }
959
960                // TODO: do we need to prefix the universalIdAttributeName in Field as well?
961            }
962
963            // if lookupParameters specified, prefix with new constant
964            if (StringUtils.isNotBlank(field.getLookupParameters())) {
965                String lookupParameters = field.getLookupParameters();
966                String newLookupParameters = KRADConstants.EMPTY_STRING;
967                String[] conversions = StringUtils.split(lookupParameters, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
968
969                for (int m = 0; m < conversions.length; m++) {
970                    String conversion = conversions[m];
971                    //String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
972                    String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
973                    String conversionFrom = conversionPair[0];
974                    String conversionTo = conversionPair[1];
975                    conversionFrom = KRADConstants.MAINTENANCE_NEW_MAINTAINABLE + conversionFrom;
976                    newLookupParameters += (conversionFrom + KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR + conversionTo);
977
978                    if (m < conversions.length) {
979                        newLookupParameters += KRADConstants.FIELD_CONVERSIONS_SEPARATOR;
980                    }
981                }
982
983                field.setLookupParameters(newLookupParameters);
984            }
985
986            // CONTAINER field types have nested rows and fields that need setup for the form
987            if (Field.CONTAINER.equals(field.getFieldType())) {
988                List containerRows = field.getContainerRows();
989                List fixedRows = new ArrayList();
990
991                for (Iterator iter = containerRows.iterator(); iter.hasNext();) {
992                    Row containerRow = (Row) iter.next();
993                    List containerFields = containerRow.getFields();
994                    List fixedFields = new ArrayList();
995
996                    for (Iterator iterator = containerFields.iterator(); iterator.hasNext();) {
997                        Field containerField = (Field) iterator.next();
998                        containerField = fixFieldForForm(containerField, keyFieldNames, namePrefix, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
999                        fixedFields.add(containerField);
1000                    }
1001
1002                    fixedRows.add(new Row(fixedFields));
1003                }
1004
1005                field.setContainerRows(fixedRows);
1006            }
1007        }
1008        return field;
1009    }
1010
1011    public static void applyAuthorization(Field field, String maintenanceAction, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
1012        String fieldName = "";
1013        FieldRestriction fieldAuth = null;
1014        Person user = GlobalVariables.getUserSession().getPerson();
1015        // only apply this on the newMaintainable
1016        if (field.getPropertyName().startsWith(KRADConstants.MAINTENANCE_NEW_MAINTAINABLE)) {
1017            // get just the actual fieldName, with the document.newMaintainableObject, etc etc removed
1018            fieldName = field.getPropertyName().substring(KRADConstants.MAINTENANCE_NEW_MAINTAINABLE.length());
1019
1020            // if the field is restricted somehow
1021            if (auths.hasRestriction(fieldName)) {
1022                fieldAuth = auths.getFieldRestriction(fieldName);
1023                if(KRADConstants.MAINTENANCE_NEW_ACTION.equals(maintenanceAction) || KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction)){
1024                        if((KewApiConstants.ROUTE_HEADER_SAVED_CD.equals(documentStatus) || KewApiConstants.ROUTE_HEADER_INITIATED_CD.equals(documentStatus))
1025                                && user.getPrincipalId().equals(documentInitiatorPrincipalId)){
1026
1027                                //user should be able to see the unmark value
1028                        }else{
1029                                if(fieldAuth.isPartiallyMasked()){
1030                                field.setSecure(true);
1031                                fieldAuth.setShouldBeEncrypted(true);
1032                                MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1033                                String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1034                                field.setDisplayMaskValue(displayMaskValue);
1035                                populateSecureField(field, field.getPropertyValue());
1036                        }
1037                        else if(fieldAuth.isMasked()){
1038                                field.setSecure(true);
1039                                fieldAuth.setShouldBeEncrypted(true);
1040                                MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1041                                String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1042                                field.setDisplayMaskValue(displayMaskValue);
1043                                populateSecureField(field, field.getPropertyValue());
1044                        }
1045                        }
1046                }
1047
1048                if (KRADConstants.MAINTENANCE_EDIT_ACTION.equals(maintenanceAction) || KRADConstants.MAINTENANCE_NEWWITHEXISTING_ACTION.equals(maintenanceAction)) {
1049                        // if there's existing data on the page that we're not going to clear out, then we will mask it out
1050                        if(fieldAuth.isPartiallyMasked()){
1051                                field.setSecure(true);
1052                                fieldAuth.setShouldBeEncrypted(true);
1053                                MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1054                                String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1055                                field.setDisplayMaskValue(displayMaskValue);
1056                                populateSecureField(field, field.getPropertyValue());
1057                        }
1058                        else if(fieldAuth.isMasked()){
1059                                field.setSecure(true);
1060                                fieldAuth.setShouldBeEncrypted(true);
1061                                MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1062                                String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1063                                field.setDisplayMaskValue(displayMaskValue);
1064                                populateSecureField(field, field.getPropertyValue());
1065                        }
1066                }
1067
1068                if (Field.isInputField(field.getFieldType()) || field.getFieldType().equalsIgnoreCase(Field.CHECKBOX)) {
1069                        // if its an editable field, allow decreasing availability to readonly or hidden
1070                    // only touch the field if the restricted type is hidden or readonly
1071                    if (fieldAuth.isReadOnly()) {
1072                        if (!field.isReadOnly() && !fieldAuth.isMasked() && !fieldAuth.isPartiallyMasked()) {
1073                            field.setReadOnly(true);
1074                        }
1075                    }
1076                    else if (fieldAuth.isHidden()) {
1077                        if (field.getFieldType() != Field.HIDDEN) {
1078                            field.setFieldType(Field.HIDDEN);
1079                        }
1080                    }
1081                }
1082
1083                if(Field.BUTTON.equalsIgnoreCase(field.getFieldType()) && fieldAuth.isHidden()){
1084                        field.setFieldType(Field.HIDDEN);
1085                }
1086
1087                // if the field is readOnly, and the authorization says it should be hidden,
1088                // then restrict it
1089                if (field.isReadOnly() && fieldAuth.isHidden()) {
1090                    field.setFieldType(Field.HIDDEN);
1091                }
1092
1093            }
1094            // special check for old maintainable - need to ensure that fields hidden on the
1095            // "new" side are also hidden on the old side
1096        }
1097        else if (field.getPropertyName().startsWith(KRADConstants.MAINTENANCE_OLD_MAINTAINABLE)) {
1098            // get just the actual fieldName, with the document.oldMaintainableObject, etc etc removed
1099            fieldName = field.getPropertyName().substring(KRADConstants.MAINTENANCE_OLD_MAINTAINABLE.length());
1100            // if the field is restricted somehow
1101            if (auths.hasRestriction(fieldName)) {
1102                fieldAuth = auths.getFieldRestriction(fieldName);
1103                if(fieldAuth.isPartiallyMasked()){
1104                    field.setSecure(true);
1105                    MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1106                    String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1107                    field.setDisplayMaskValue(displayMaskValue);
1108                    field.setPropertyValue(displayMaskValue);
1109                    populateSecureField(field, field.getPropertyValue());
1110
1111               }
1112
1113               if(fieldAuth.isMasked()){
1114                    field.setSecure(true);
1115                    MaskFormatter maskFormatter = fieldAuth.getMaskFormatter();
1116                    String displayMaskValue = maskFormatter.maskValue(field.getPropertyValue());
1117                    field.setDisplayMaskValue(displayMaskValue);
1118                    field.setPropertyValue(displayMaskValue);
1119                    populateSecureField(field, field.getPropertyValue());
1120                }
1121
1122                if (fieldAuth.isHidden()) {
1123                    field.setFieldType(Field.HIDDEN);
1124                }
1125            }
1126        }
1127    }
1128
1129    /**
1130     * Merges together sections of the old maintainable and new maintainable.
1131     *
1132     * @param oldSections
1133     * @param newSections
1134     * @param keyFieldNames
1135     * @param maintenanceAction
1136     * @param readOnly
1137     * @return List of Section objects
1138     */
1139    public static List meshSections(List oldSections, List newSections, List keyFieldNames, String maintenanceAction, boolean readOnly, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
1140        List meshedSections = new ArrayList();
1141
1142        for (int i = 0; i < newSections.size(); i++) {
1143            Section maintSection = (Section) newSections.get(i);
1144            List sectionRows = maintSection.getRows();
1145            Section oldMaintSection = (Section) oldSections.get(i);
1146            List oldSectionRows = oldMaintSection.getRows();
1147            List<Row> meshedRows = new ArrayList();
1148            meshedRows = meshRows(oldSectionRows, sectionRows, keyFieldNames, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
1149            maintSection.setRows(meshedRows);
1150            if (StringUtils.isBlank(maintSection.getErrorKey())) {
1151                maintSection.setErrorKey(MaintenanceUtils.generateErrorKeyForSection(maintSection));
1152            }
1153            meshedSections.add(maintSection);
1154        }
1155
1156        return meshedSections;
1157    }
1158
1159    /**
1160     * Merges together rows of an old maintainable section and new maintainable section.
1161     *
1162     * @param oldRows
1163     * @param newRows
1164     * @param keyFieldNames
1165     * @param maintenanceAction
1166     * @param readOnly
1167     * @return List of Row objects
1168     */
1169    public static List meshRows(List oldRows, List newRows, List keyFieldNames, String maintenanceAction, boolean readOnly, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
1170        List<Row> meshedRows = new ArrayList<Row>();
1171
1172        for (int j = 0; j < newRows.size(); j++) {
1173            Row sectionRow = (Row) newRows.get(j);
1174            List rowFields = sectionRow.getFields();
1175            Row oldSectionRow = null;
1176            List oldRowFields = new ArrayList();
1177
1178            if (null != oldRows && oldRows.size() > j) {
1179                oldSectionRow = (Row) oldRows.get(j);
1180                oldRowFields = oldSectionRow.getFields();
1181            }
1182
1183            List meshedFields = meshFields(oldRowFields, rowFields, keyFieldNames, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
1184            if (meshedFields.size() > 0) {
1185                Row meshedRow = new Row(meshedFields);
1186                if (sectionRow.isHidden()) {
1187                    meshedRow.setHidden(true);
1188                }
1189
1190                meshedRows.add(meshedRow);
1191            }
1192        }
1193
1194        return meshedRows;
1195    }
1196
1197
1198    /**
1199     * Merges together fields and an old maintainble row and new maintainable row, for each field call fixFieldForForm.
1200     *
1201     * @param oldFields
1202     * @param newFields
1203     * @param keyFieldNames
1204     * @param maintenanceAction
1205     * @param readOnly
1206     * @return List of Field objects
1207     */
1208    public static List meshFields(List oldFields, List newFields, List keyFieldNames, String maintenanceAction, boolean readOnly, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
1209        List meshedFields = new ArrayList();
1210
1211        List newFieldsToMerge = new ArrayList();
1212        List oldFieldsToMerge = new ArrayList();
1213
1214        for (int k = 0; k < newFields.size(); k++) {
1215            Field newMaintField = (Field) newFields.get(k);
1216            String propertyName = newMaintField.getPropertyName();
1217            // If this is an add button, then we have to have only this field for the entire row.
1218            if (Field.IMAGE_SUBMIT.equals(newMaintField.getFieldType())) {
1219                meshedFields.add(newMaintField);
1220            }
1221            else if (Field.CONTAINER.equals(newMaintField.getFieldType())) {
1222                if (oldFields.size() > k) {
1223                    Field oldMaintField = (Field) oldFields.get(k);
1224                    newMaintField = meshContainerFields(oldMaintField, newMaintField, keyFieldNames, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
1225                }
1226                else {
1227                    newMaintField = meshContainerFields(newMaintField, newMaintField, keyFieldNames, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
1228                }
1229                meshedFields.add(newMaintField);
1230            }
1231            else {
1232                newMaintField = FieldUtils.fixFieldForForm(newMaintField, keyFieldNames, KRADConstants.MAINTENANCE_NEW_MAINTAINABLE, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId);
1233                // add old fields for edit
1234                if (KRADConstants.MAINTENANCE_EDIT_ACTION.equals(maintenanceAction) || KRADConstants.MAINTENANCE_COPY_ACTION.equals(maintenanceAction)) {
1235                    Field oldMaintField = (Field) oldFields.get(k);
1236
1237                    // compare values for change, and set new maintainable fields for highlighting
1238                    // no point in highlighting the hidden fields, since they won't be rendered anyways
1239                    if (!StringUtils.equalsIgnoreCase(newMaintField.getPropertyValue(), oldMaintField.getPropertyValue())
1240                            && !Field.HIDDEN.equals(newMaintField.getFieldType())) {
1241                        newMaintField.setHighlightField(true);
1242                    }
1243
1244                    oldMaintField = FieldUtils.fixFieldForForm(oldMaintField, keyFieldNames, KRADConstants.MAINTENANCE_OLD_MAINTAINABLE, maintenanceAction, true, auths, documentStatus, documentInitiatorPrincipalId);
1245                    oldFieldsToMerge.add(oldMaintField);
1246                }
1247
1248                newFieldsToMerge.add(newMaintField);
1249
1250                for (Iterator iter = oldFieldsToMerge.iterator(); iter.hasNext();) {
1251                    Field element = (Field) iter.next();
1252                    meshedFields.add(element);
1253                }
1254
1255                for (Iterator iter = newFieldsToMerge.iterator(); iter.hasNext();) {
1256                    Field element = (Field) iter.next();
1257                    meshedFields.add(element);
1258                }
1259            }
1260        }
1261        return meshedFields;
1262    }
1263
1264    /**
1265     * Determines whether field level help is enabled for the field corresponding to the dataObjectClass and attribute name
1266     *
1267     * If this value is true, then the field level help will be enabled.
1268     * If false, then whether a field is enabled is determined by the value returned by {@link #isLookupFieldLevelHelpDisabled(Class, String)} and the system-wide
1269     * parameter setting.  Note that if a field is read-only, that may cause field-level help to not be rendered.
1270     *
1271     * @param businessObjectClass the looked up class
1272     * @param attributeName the attribute for the field
1273     * @return true if field level help is enabled, false if the value of this method should NOT be used to determine whether this method's return value
1274     * affects the enablement of field level help
1275     */
1276    protected static boolean isLookupFieldLevelHelpEnabled(Class businessObjectClass, String attributeName) {
1277        return false;
1278    }
1279
1280    /**
1281     * Determines whether field level help is disabled for the field corresponding to the dataObjectClass and attribute name
1282     *
1283     * If this value is true and {@link #isLookupFieldLevelHelpEnabled(Class, String)} returns false,
1284     * then the field level help will not be rendered.  If both this and {@link #isLookupFieldLevelHelpEnabled(Class, String)} return false, then the system-wide
1285     * setting will determine whether field level help is enabled.  Note that if a field is read-only, that may cause field-level help to not be rendered.
1286     *
1287     * @param businessObjectClass the looked up class
1288     * @param attributeName the attribute for the field
1289     * @return true if field level help is disabled, false if the value of this method should NOT be used to determine whether this method's return value
1290     * affects the enablement of field level help
1291     */
1292    protected static boolean isLookupFieldLevelHelpDisabled(Class businessObjectClass, String attributeName) {
1293        return false;
1294    }
1295
1296    public static List<Field> createAndPopulateFieldsForLookup(List<String> lookupFieldAttributeList, List<String> readOnlyFieldsList, Class businessObjectClass) throws InstantiationException, IllegalAccessException {
1297        List<Field> fields = new ArrayList<Field>();
1298        BusinessObjectEntry boe = (BusinessObjectEntry) getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(businessObjectClass.getName());
1299
1300        Map<String, Boolean> isHiddenMap = new HashMap<String, Boolean>();
1301        Map<String, Boolean> isReadOnlyMap = new HashMap<String, Boolean>();
1302
1303        /*
1304         * Check if any field is hidden or read only.  This allows us to
1305         * set lookup criteria as hidden/readonly outside the controlDefinition.
1306         */
1307        if(boe.hasLookupDefinition()){
1308                List<FieldDefinition> fieldDefs = boe.getLookupDefinition().getLookupFields();
1309                for(FieldDefinition field : fieldDefs){
1310                                isReadOnlyMap.put(field.getAttributeName(), Boolean.valueOf(field.isReadOnly()));
1311                                isHiddenMap.put(field.getAttributeName(), Boolean.valueOf(field.isHidden()));
1312                }
1313        }
1314
1315        for( String attributeName : lookupFieldAttributeList )
1316        {
1317            Field field = FieldUtils.getPropertyField(businessObjectClass, attributeName, true);
1318
1319            if(field.isDatePicker() && field.isRanged()) {
1320
1321                Field newDate = createRangeDateField(field);
1322                fields.add(newDate);
1323            }
1324
1325            BusinessObject newBusinessObjectInstance;
1326            if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface(businessObjectClass)) {
1327                ModuleService moduleService = getKualiModuleService().getResponsibleModuleService(businessObjectClass);
1328                newBusinessObjectInstance = (BusinessObject) moduleService.createNewObjectFromExternalizableClass(businessObjectClass);
1329            }
1330            else {
1331                newBusinessObjectInstance = (BusinessObject) businessObjectClass.newInstance();
1332            }
1333            //quickFinder is synonymous with a field-based Lookup
1334            field = LookupUtils.setFieldQuickfinder(newBusinessObjectInstance, attributeName, field, lookupFieldAttributeList);
1335            field = LookupUtils.setFieldDirectInquiry(newBusinessObjectInstance, attributeName, field);
1336
1337            // overwrite maxLength to allow for wildcards and ranges in the select, but only if it's not a mulitselect box, because maxLength determines the # of entries
1338            if (!Field.MULTISELECT.equals(field.getFieldType())) {
1339                field.setMaxLength(100);
1340            }
1341
1342            // if the attrib name is "active", and BO is Inactivatable, then set the default value to Y
1343            if (attributeName.equals(KRADPropertyConstants.ACTIVE) && MutableInactivatable.class.isAssignableFrom(businessObjectClass)) {
1344                field.setPropertyValue(KRADConstants.YES_INDICATOR_VALUE);
1345                field.setDefaultValue(KRADConstants.YES_INDICATOR_VALUE);
1346            }
1347            // set default value
1348            String defaultValue = getBusinessObjectMetaDataService().getLookupFieldDefaultValue(businessObjectClass, attributeName);
1349            if (defaultValue != null) {
1350                field.setPropertyValue(defaultValue);
1351                field.setDefaultValue(defaultValue);
1352            }
1353
1354            Class defaultValueFinderClass = getBusinessObjectMetaDataService().getLookupFieldDefaultValueFinderClass(businessObjectClass, attributeName);
1355            //getBusinessObjectMetaDataService().getLookupFieldDefaultValue(dataObjectClass, attributeName)
1356            if (defaultValueFinderClass != null) {
1357                field.setPropertyValue(((ValueFinder) defaultValueFinderClass.newInstance()).getValue());
1358                field.setDefaultValue(((ValueFinder) defaultValueFinderClass.newInstance()).getValue());
1359            }
1360            if ( (readOnlyFieldsList != null && readOnlyFieldsList.contains(field.getPropertyName()))
1361                        || ( isReadOnlyMap.containsKey(field.getPropertyName()) && isReadOnlyMap.get(field.getPropertyName()).booleanValue())
1362                ) {
1363                field.setReadOnly(true);
1364            }
1365
1366            populateQuickfinderDefaultsForLookup(businessObjectClass, attributeName, field);
1367
1368                        if ((isHiddenMap.containsKey(field.getPropertyName()) && isHiddenMap.get(field.getPropertyName()).booleanValue())) {
1369                                field.setFieldType(Field.HIDDEN);
1370                        }
1371            
1372            boolean triggerOnChange = getBusinessObjectDictionaryService().isLookupFieldTriggerOnChange(businessObjectClass, attributeName);
1373            field.setTriggerOnChange(triggerOnChange);
1374
1375            field.setFieldLevelHelpEnabled(isLookupFieldLevelHelpEnabled(businessObjectClass, attributeName));
1376            field.setFieldLevelHelpDisabled(isLookupFieldLevelHelpDisabled(businessObjectClass, attributeName));
1377            
1378            fields.add(field);
1379        }
1380        return fields;
1381    }
1382
1383
1384        /**
1385         * This method ...
1386         *
1387         * @param businessObjectClass
1388         * @param attributeName
1389         * @param field
1390         * @throws InstantiationException
1391         * @throws IllegalAccessException
1392         */
1393        private static void populateQuickfinderDefaultsForLookup(
1394                        Class businessObjectClass, String attributeName, Field field)
1395                        throws InstantiationException, IllegalAccessException {
1396                // handle quickfinderParameterString / quickfinderParameterFinderClass
1397                String quickfinderParamString = getBusinessObjectMetaDataService().getLookupFieldQuickfinderParameterString(businessObjectClass, attributeName);
1398                Class<? extends ValueFinder> quickfinderParameterFinderClass =
1399                        getBusinessObjectMetaDataService().getLookupFieldQuickfinderParameterStringBuilderClass(businessObjectClass, attributeName);
1400                if (quickfinderParameterFinderClass != null) {
1401                        quickfinderParamString = quickfinderParameterFinderClass.newInstance().getValue();
1402                }
1403
1404                if (!StringUtils.isEmpty(quickfinderParamString)) {
1405                        String [] params = quickfinderParamString.split(",");
1406                        if (params != null) for (String param : params) {
1407                                if (param.contains(KRADConstants.LOOKUP_PARAMETER_LITERAL_DELIMITER)) {
1408                                        String[] paramChunks = param.split(KRADConstants.LOOKUP_PARAMETER_LITERAL_DELIMITER, 2);
1409                                        field.appendLookupParameters(
1410                                                        KRADConstants.LOOKUP_PARAMETER_LITERAL_PREFIX+KRADConstants.LOOKUP_PARAMETER_LITERAL_DELIMITER+
1411                                                        paramChunks[1]+":"+paramChunks[0]);
1412                                }
1413                        }
1414                }
1415        }
1416
1417
1418        /**
1419         * creates an extra field for date from/to ranges
1420         * @param field
1421         * @return a new date field
1422         */
1423        public static Field createRangeDateField(Field field) {
1424                Field newDate = (Field)ObjectUtils.deepCopy(field);
1425                newDate.setFieldLabel(newDate.getFieldLabel()+" "+KRADConstants.LOOKUP_DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL);
1426                field.setFieldLabel(field.getFieldLabel()+" "+KRADConstants.LOOKUP_DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL);
1427                newDate.setPropertyName(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX+newDate.getPropertyName());
1428                return newDate;
1429        }
1430
1431    private static Field meshContainerFields(Field oldMaintField, Field newMaintField, List keyFieldNames, String maintenanceAction, boolean readOnly, MaintenanceDocumentRestrictions auths, String documentStatus, String documentInitiatorPrincipalId) {
1432        List resultingRows = new ArrayList();
1433        resultingRows.addAll(meshRows(oldMaintField.getContainerRows(), newMaintField.getContainerRows(), keyFieldNames, maintenanceAction, readOnly, auths, documentStatus, documentInitiatorPrincipalId));
1434        Field resultingField = newMaintField;
1435        resultingField.setFieldType(Field.CONTAINER);
1436
1437        // save the summary info
1438        resultingField.setContainerElementName(newMaintField.getContainerElementName());
1439        resultingField.setContainerDisplayFields(newMaintField.getContainerDisplayFields());
1440        resultingField.setNumberOfColumnsForCollection(newMaintField.getNumberOfColumnsForCollection());
1441
1442        resultingField.setContainerRows(resultingRows);
1443        List resultingRowsList = newMaintField.getContainerRows();
1444        if (resultingRowsList.size() > 0) {
1445            List resultingFieldsList = ((Row) resultingRowsList.get(0)).getFields();
1446            if (resultingFieldsList.size() > 0) {
1447                // todo: assign the correct propertyName to the container in the first place. For now, I'm wary of the weird usages
1448                // of constructContainerField().
1449                String containedFieldName = ((Field) (resultingFieldsList.get(0))).getPropertyName();
1450                resultingField.setPropertyName(containedFieldName.substring(0, containedFieldName.lastIndexOf('.')));
1451            }
1452        }
1453        else {
1454            resultingField.setPropertyName(oldMaintField.getPropertyName());
1455        }
1456        return resultingField;
1457    }
1458
1459    /**
1460     * This method modifies the passed in field so that it may be used to render a multiple values lookup button
1461     *
1462     * @param field this object will be modified by this method
1463     * @param parents
1464     * @param definition
1465     */
1466    static final public void modifyFieldToSupportMultipleValueLookups(Field field, String parents, MaintainableCollectionDefinition definition) {
1467        field.setMultipleValueLookedUpCollectionName(parents + definition.getName());
1468        field.setMultipleValueLookupClassName(definition.getSourceClassName().getName());
1469        field.setMultipleValueLookupClassLabel(getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(definition.getSourceClassName().getName()).getObjectLabel());
1470    }
1471
1472    /**
1473     * Returns whether the passed in collection has been properly configured in the maint doc dictionary to support multiple value
1474     * lookups.
1475     *
1476     * @param definition
1477     * @return
1478     */
1479    static final public boolean isCollectionMultipleLookupEnabled(MaintainableCollectionDefinition definition) {
1480        return definition.getSourceClassName() != null && definition.isIncludeMultipleLookupLine();
1481    }
1482
1483    /**
1484     * This method removes any duplicating spacing (internal or on the ends) from a String, meant to be exposed as a tag library
1485     * function.
1486     *
1487     * @param s String to remove duplicate spacing from.
1488     * @return String without duplicate spacing.
1489     */
1490    public static String scrubWhitespace(String s) {
1491        return s.replaceAll("(\\s)(\\s+)", " ");
1492    }
1493
1494    public static List<Row> convertRemotableAttributeFields(List<RemotableAttributeField> remotableAttributeFields) {
1495        List<Row> rows = new ArrayList<Row>();
1496        for (RemotableAttributeField remotableAttributeField : remotableAttributeFields) {
1497            List<Field> fields = convertRemotableAttributeField(remotableAttributeField);
1498            // each field goes in it's own row...
1499            for (Field field : fields) {
1500                Row row = new Row(field);
1501                rows.add(row);
1502            }
1503        }
1504        return rows;
1505    }
1506
1507    public static List<Field> convertRemotableAttributeField(RemotableAttributeField remotableAttributeField) {
1508        // will produce two fields in the case of a range
1509        List<Field> fields = constructFieldsForAttributeDefinition(remotableAttributeField);
1510        for (Field field : fields) {
1511            applyControlAttributes(remotableAttributeField, field);
1512            applyLookupAttributes(remotableAttributeField, field);
1513            applyWidgetAttributes(remotableAttributeField, field);
1514        }
1515        return fields;
1516    }
1517
1518    private static List<Field> constructFieldsForAttributeDefinition(RemotableAttributeField remotableAttributeField) {
1519        List<Field> fields = new ArrayList<Field>();
1520        if (remotableAttributeField.getAttributeLookupSettings() != null
1521                && remotableAttributeField.getAttributeLookupSettings().isRanged()) {
1522            // create two fields, one for the "from" and one for the "to"
1523            AttributeLookupSettings lookupSettings = remotableAttributeField.getAttributeLookupSettings();
1524            // Create a pair of range input fields for a ranged attribute
1525            // the lower bound is prefixed to distinguish it from the upper bound, which retains the original field name
1526            String attrLabel;
1527            if (StringUtils.isBlank(remotableAttributeField.getLongLabel())) {
1528                attrLabel =  remotableAttributeField.getShortLabel();
1529            } else {
1530                attrLabel =  remotableAttributeField.getLongLabel();
1531            }
1532            String label = StringUtils.defaultString(lookupSettings.getLowerLabel(), attrLabel
1533                + " " + KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL);
1534            Field lowerField = new Field(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + remotableAttributeField.getName(), label);
1535            lowerField.setMemberOfRange(true);
1536            lowerField.setAllowInlineRange(false);
1537            lowerField.setRangeFieldInclusive(lookupSettings.isLowerBoundInclusive());
1538            if (lookupSettings.isLowerDatePicker() != null) {
1539                lowerField.setDatePicker(lookupSettings.isLowerDatePicker());
1540            }
1541            if (!remotableAttributeField.getDataType().equals(DataType.CURRENCY)) {
1542                lowerField.setFieldDataType(remotableAttributeField.getDataType().name().toLowerCase());
1543            }
1544            fields.add(lowerField);
1545
1546            label = StringUtils.defaultString(lookupSettings.getUpperLabel(), attrLabel
1547                + " " + KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL);
1548            Field upperField = new Field(remotableAttributeField.getName(), label);
1549            upperField.setMemberOfRange(true);
1550            upperField.setAllowInlineRange(false);
1551            upperField.setRangeFieldInclusive(lookupSettings.isUpperBoundInclusive());
1552            if (lookupSettings.isUpperDatePicker() != null) {
1553                upperField.setDatePicker(lookupSettings.isUpperDatePicker());
1554            }
1555            if (!remotableAttributeField.getDataType().equals(DataType.CURRENCY)) {
1556                upperField.setFieldDataType(remotableAttributeField.getDataType().name().toLowerCase());
1557            }
1558            fields.add(upperField);
1559        } else {
1560            //this ain't right....
1561            Field tempField = new Field(remotableAttributeField.getName(), remotableAttributeField.getLongLabel());
1562            if (remotableAttributeField.getMaxLength() != null) {
1563                tempField.setMaxLength(remotableAttributeField.getMaxLength());
1564            }
1565
1566            if (remotableAttributeField.getShortLabel() != null) {
1567                tempField.setFieldLabel(remotableAttributeField.getShortLabel());
1568            }
1569
1570            if (!remotableAttributeField.getDataType().equals(DataType.CURRENCY)) {
1571                tempField.setFieldDataType(remotableAttributeField.getDataType().name().toLowerCase());
1572            } else {
1573                tempField.setFieldDataType(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT);
1574            }
1575
1576            tempField.setMainFieldLabel(remotableAttributeField.getLongLabel());
1577            tempField.setFieldHelpSummary(remotableAttributeField.getHelpSummary());
1578            tempField.setUpperCase(remotableAttributeField.isForceUpperCase());
1579            if (remotableAttributeField.getMaxLength() != null) {
1580                if (remotableAttributeField.getMaxLength().intValue() > 0) {
1581                    tempField.setMaxLength(remotableAttributeField.getMaxLength().intValue());
1582                } else {
1583                    tempField.setMaxLength(100);
1584                }
1585            }
1586            tempField.setFieldRequired(remotableAttributeField.isRequired());
1587
1588            fields.add(tempField);
1589        }
1590        return fields;
1591    }
1592
1593    public static List<RemotableAttributeField> convertRowsToAttributeFields(List<Row> rows) {
1594        List<RemotableAttributeField> attributeFields = new ArrayList<RemotableAttributeField>();
1595        for (Row row : rows) {
1596            attributeFields.addAll(convertRowToAttributeFields(row));
1597        }
1598        return attributeFields;
1599    }
1600
1601    public static List<RemotableAttributeField> convertRowToAttributeFields(Row row) {
1602        List<RemotableAttributeField> attributeFields = new ArrayList<RemotableAttributeField>();
1603        for (Field field : row.getFields()) {
1604            RemotableAttributeField remotableAttributeField = convertFieldToAttributeField(field);
1605            if (remotableAttributeField != null) {
1606                attributeFields.add(remotableAttributeField);
1607            }
1608        }
1609        return attributeFields;
1610    }
1611
1612    public static RemotableAttributeField convertFieldToAttributeField(Field field) {
1613        RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(field.getPropertyName());
1614
1615        List<RemotableAbstractWidget.Builder> widgets = new ArrayList<RemotableAbstractWidget.Builder>();
1616        builder.setDataType(DataType.valueOf(field.getFieldDataType().toUpperCase()));
1617        builder.setShortLabel(field.getFieldLabel());
1618        builder.setLongLabel(field.getMainFieldLabel());
1619        builder.setHelpSummary(field.getFieldHelpSummary());
1620        //builder.setConstraintText(field.)
1621        //builder.setHelpDescription();
1622        builder.setForceUpperCase(field.isUpperCase());
1623        //builder.setMinLength()
1624        if (field.getMaxLength() > 0) {
1625            builder.setMaxLength(new Integer(field.getMaxLength()));
1626        } else {
1627            builder.setMaxLength(new Integer(100));
1628        }
1629        //builder.setMinValue();
1630        //builder.setMaxValue();
1631        //builder.setRegexConstraint(field.);
1632        //builder.setRegexContraintMsg();
1633        builder.setRequired(field.isFieldRequired());
1634        builder.setDefaultValues(Collections.singletonList(field.getDefaultValue()));
1635        builder.setControl(FieldUtils.constructControl(field, field.getFieldValidValues()));
1636        if (field.getHasLookupable()) {
1637            builder.setAttributeLookupSettings(RemotableAttributeLookupSettings.Builder.create());
1638            RemotableQuickFinder.Builder quickfinder =
1639                    RemotableQuickFinder.Builder.create(field.getBaseLookupUrl(), field.getQuickFinderClassNameImpl());
1640            quickfinder.setFieldConversions(toMap(field.getFieldConversions()));
1641            quickfinder.setLookupParameters(toMap(field.getLookupParameters()));
1642            widgets.add(quickfinder);
1643        }
1644        RemotableAttributeLookupSettings.Builder lookupSettings = null;
1645        if (builder.getDataType().equals(DataType.DATETIME)
1646                || builder.getDataType().equals(DataType.DATE)) {
1647            if (field.isRanged()) {
1648                lookupSettings = RemotableAttributeLookupSettings.Builder.create();
1649                lookupSettings.setRanged(field.isRanged());
1650                if (field.isDatePicker()) {
1651                    lookupSettings.setLowerDatePicker(Boolean.TRUE);
1652                    lookupSettings.setUpperDatePicker(Boolean.TRUE);
1653                }
1654                if (ObjectUtils.isNull(field.getRangeFieldInclusive())) {
1655                    lookupSettings.setUpperBoundInclusive(true);
1656                    lookupSettings.setLowerBoundInclusive(true);
1657                }
1658            }
1659        }
1660
1661        if (!field.isColumnVisible()) {
1662            if (ObjectUtils.isNull(lookupSettings)) {
1663                lookupSettings = RemotableAttributeLookupSettings.Builder.create();
1664            }
1665            lookupSettings.setInResults(field.isColumnVisible());
1666        }
1667
1668        if (ObjectUtils.isNotNull(lookupSettings)) {
1669            builder.setAttributeLookupSettings(lookupSettings);
1670        }
1671
1672        if (field.getFieldType().equals(Field.CURRENCY)) {
1673            builder.setDataType(DataType.CURRENCY);
1674            builder.setMaxLength(field.getFormattedMaxLength());
1675        }
1676        if (field.isDatePicker()) {
1677            widgets.add(RemotableDatepicker.Builder.create());
1678        }
1679        if (field.isExpandedTextArea()) {
1680            widgets.add(RemotableTextExpand.Builder.create());
1681        }
1682        builder.setWidgets(widgets);
1683
1684        return builder.build();
1685    }
1686
1687    private static RemotableAbstractControl.Builder constructControl(Field field, List<KeyValue> options) {
1688
1689        //RemotableAbstractControl.Builder control = null;
1690        Map<String, String> optionMap = new LinkedHashMap<String, String>();
1691        if (options != null) {
1692            for (KeyValue option : options) {
1693                optionMap.put(option.getKey(), option.getValue());
1694            }
1695        }
1696        String type = field.getFieldType();
1697        if (Field.TEXT.equals(type) || Field.DATEPICKER.equals(type)) {
1698            RemotableTextInput.Builder control = RemotableTextInput.Builder.create();
1699            control.setSize(field.getSize());
1700            return control;
1701        } else if (Field.TEXT_AREA.equals(type)) {
1702            RemotableTextarea.Builder control = RemotableTextarea.Builder.create();
1703            control.setCols(field.getCols());
1704            control.setRows(field.getRows());
1705            return control;
1706                } else if (Field.DROPDOWN.equals(type)) {
1707            return RemotableSelect.Builder.create(optionMap);
1708        } else if (Field.DROPDOWN_REFRESH.equals(type)) {
1709            RemotableSelect.Builder control = RemotableSelect.Builder.create(optionMap);
1710            control.setRefreshOnChange(true);
1711            return control;
1712        } else if (Field.CHECKBOX.equals(type)) {
1713            return RemotableCheckbox.Builder.create();
1714                } else if (Field.RADIO.equals(type)) {
1715            return RemotableRadioButtonGroup.Builder.create(optionMap);
1716                } else if (Field.HIDDEN.equals(type)) {
1717            return RemotableHiddenInput.Builder.create();
1718                } else if (Field.MULTIBOX.equals(type)) {
1719            RemotableSelect.Builder control = RemotableSelect.Builder.create(optionMap);
1720            control.setMultiple(true);
1721            return control;
1722        } else if (Field.MULTISELECT.equals(type)) {
1723            RemotableSelect.Builder control = RemotableSelect.Builder.create(optionMap);
1724            control.setMultiple(true);
1725            return control;
1726        } else if (Field.CURRENCY.equals(type)) {
1727            RemotableTextInput.Builder control = RemotableTextInput.Builder.create();
1728            control.setSize(field.getSize());
1729            return control;
1730        } else {
1731                    throw new IllegalArgumentException("Illegal field type found: " + type);
1732        }
1733
1734    }
1735
1736    private static void applyControlAttributes(RemotableAttributeField remotableField, Field field) {
1737        RemotableControlContract control = remotableField.getControl();
1738        String fieldType = null;
1739
1740        if (control == null) {
1741            throw new IllegalStateException("Given attribute field with the following name has a null control: " + remotableField.getName());
1742        }
1743        if (control == null || control instanceof RemotableTextInput) {
1744            fieldType = Field.TEXT;
1745            if (((RemotableTextInput)remotableField.getControl()).getSize() != null) {
1746              field.setSize(((RemotableTextInput)remotableField.getControl()).getSize().intValue());
1747            }
1748            if (((RemotableTextInput)remotableField.getControl()).getSize() != null) {
1749                field.setFormattedMaxLength(((RemotableTextInput)remotableField.getControl()).getSize().intValue());
1750            }
1751        } else if (control instanceof RemotableCheckboxGroup) {
1752            RemotableCheckboxGroup checkbox = (RemotableCheckboxGroup)control;
1753            fieldType = Field.CHECKBOX;
1754            field.setFieldValidValues(FieldUtils.convertMapToKeyValueList(checkbox.getKeyLabels()));
1755        } else if (control instanceof RemotableCheckbox) {
1756            fieldType = Field.CHECKBOX;
1757        } else if (control instanceof RemotableHiddenInput) {
1758            fieldType = Field.HIDDEN;
1759        } else if (control instanceof RemotablePasswordInput) {
1760            throw new IllegalStateException("Password control not currently supported.");
1761        } else if (control instanceof RemotableRadioButtonGroup) {
1762            fieldType = Field.RADIO;
1763            RemotableRadioButtonGroup radioControl = (RemotableRadioButtonGroup)control;
1764            field.setFieldValidValues(FieldUtils.convertMapToKeyValueList(radioControl.getKeyLabels()));
1765        } else if (control instanceof RemotableSelect) {
1766            RemotableSelect selectControl = (RemotableSelect)control;
1767
1768            field.setFieldValidValues(FieldUtils.convertMapToKeyValueList(selectControl.getKeyLabels()));
1769            if (selectControl.isMultiple()) {
1770                fieldType = Field.MULTISELECT;
1771            } else if (selectControl.isRefreshOnChange()) {
1772                fieldType = Field.DROPDOWN_REFRESH;
1773            } else {
1774                fieldType = Field.DROPDOWN;
1775            }
1776        } else if (control instanceof RemotableTextarea) {
1777            fieldType = Field.TEXT_AREA;
1778            if (((RemotableTextarea)remotableField.getControl()).getCols() != null
1779                    && ((RemotableTextarea)remotableField.getControl()).getRows() != null) {
1780                field.setCols(((RemotableTextarea)remotableField.getControl()).getCols().intValue());
1781                field.setSize(((RemotableTextarea)remotableField.getControl()).getRows().intValue());
1782            }
1783        } else {
1784            throw new IllegalArgumentException("Given control type is not supported: " + control.getClass());
1785        }
1786        // compare setting of Field default values to {@link ComponentFactory#translateRemotableField}
1787        if (!remotableField.getDefaultValues().isEmpty()) {
1788            field.setDefaultValue(remotableField.getDefaultValues().iterator().next());
1789            // why are these two not related? :/
1790            field.setPropertyValues(remotableField.getDefaultValues().toArray(new String[remotableField.getDefaultValues().size()]));
1791            field.setPropertyValue(field.getDefaultValue());
1792        }
1793        field.setFieldType(fieldType);
1794    }
1795
1796    private static List<KeyValue> convertMapToKeyValueList(Map<String, String> values) {
1797        ArrayList<KeyValue> validValues = new ArrayList<KeyValue>(values.size());
1798        for (Map.Entry<String, String> entry : values.entrySet()) {
1799            validValues.add(new ConcreteKeyValue(entry.getKey(), entry.getValue()));
1800        }
1801        return validValues;
1802    }
1803
1804    private static void applyLookupAttributes(RemotableAttributeField remotableField, Field field) {
1805        AttributeLookupSettings lookupSettings = remotableField.getAttributeLookupSettings();
1806        if (lookupSettings != null) {
1807            field.setColumnVisible(lookupSettings.isInResults());
1808            if (!lookupSettings.isInCriteria()) {
1809                field.setFieldType(Field.HIDDEN);
1810            }
1811            field.setRanged(lookupSettings.isRanged());
1812            boolean datePickerLow = lookupSettings.isLowerDatePicker() == null ? false : lookupSettings.isLowerDatePicker().booleanValue();
1813            boolean datePickerUpper = lookupSettings.isUpperDatePicker() == null ? false : lookupSettings.isUpperDatePicker().booleanValue();
1814            field.setDatePicker(datePickerLow || datePickerUpper);
1815        }
1816    }
1817
1818    private static void applyWidgetAttributes(RemotableAttributeField remotableField, Field field) {
1819        Collection<? extends RemotableAbstractWidget> widgets = remotableField.getWidgets();
1820
1821        for (RemotableAbstractWidget widget : widgets) {
1822            //yuck, do we really have to do this if else if stuff here?
1823            if (widget instanceof RemotableQuickFinder) {
1824                field.setQuickFinderClassNameImpl(((RemotableQuickFinder)widget).getDataObjectClass());
1825                field.setBaseLookupUrl(((RemotableQuickFinder)widget).getBaseLookupUrl());
1826                field.setLookupParameters(((RemotableQuickFinder)widget).getLookupParameters());
1827                field.setFieldConversions(((RemotableQuickFinder)widget).getFieldConversions());
1828            // datepickerness is dealt with in constructFieldsForAttributeDefinition ranged field construction
1829            // since multiple widgets are set on RemotableAttributeField (why?) and it's not possible to determine
1830            // lower vs. upper bound settings
1831            //} else if (widget instanceof RemotableDatepicker) {
1832            //    field.setDatePicker(true);
1833            } else if (widget instanceof RemotableTextExpand) {
1834                field.setExpandedTextArea(true);
1835            }
1836
1837        }
1838    }
1839
1840    public static Column constructColumnFromAttributeField(RemotableAttributeField attributeField) {
1841        if (attributeField == null) {
1842            throw new IllegalArgumentException("attributeField was null");
1843        }
1844        DataType dataType = DataType.STRING;
1845        if (attributeField.getDataType() != null) {
1846            dataType = attributeField.getDataType();
1847        }
1848        Column column = new Column();
1849        String columnTitle = "";
1850        if (StringUtils.isBlank(attributeField.getShortLabel())) {
1851            if (StringUtils.isBlank(attributeField.getLongLabel())) {
1852                columnTitle = attributeField.getName();
1853            } else {
1854                columnTitle = attributeField.getLongLabel();
1855            }
1856        } else {
1857            columnTitle = attributeField.getShortLabel();
1858        }
1859        column.setColumnTitle(columnTitle);
1860        column.setSortable(Boolean.TRUE.toString());
1861        // TODO - KULRICE-5743 - maybe need this to be smaller than the actual attribute's max length?
1862        if (attributeField.getMaxLength() != null) {
1863            column.setMaxLength(attributeField.getMaxLength());
1864        }
1865        column.setPropertyName(attributeField.getName());
1866        if (attributeField.getDataType() == DataType.MARKUP) {
1867            column.setEscapeXMLValue(false);
1868            // since the field is a markup type, set the action href
1869            column.setColumnAnchor(new AnchorHtmlData());
1870        } else {
1871            column.setEscapeXMLValue(true);
1872        }
1873        column.setComparator(CellComparatorHelper.getAppropriateComparatorForPropertyClass(dataType.getType()));
1874        column.setValueComparator(CellComparatorHelper.getAppropriateValueComparatorForPropertyClass(dataType.getType()));
1875
1876        if(StringUtils.isNotEmpty(attributeField.getFormatterName())) {
1877            try  {
1878                  column.setFormatter(Formatter.getFormatter(Class.forName(attributeField.getFormatterName())));
1879            } catch (ClassNotFoundException e) {
1880                  LOG.error("Unable to find formatter class: " + attributeField.getFormatterName());
1881                  // Fall back to datatype based formatter
1882                column.setFormatter(FieldUtils.getFormatterForDataType(dataType));
1883            }
1884        }  else {
1885            column.setFormatter(FieldUtils.getFormatterForDataType(dataType));
1886        }
1887
1888        return column;
1889    }
1890
1891    public static List<Column> constructColumnsFromAttributeFields(List<RemotableAttributeField> attributeFields) {
1892        List<Column> attributeColumns = new ArrayList<Column>();
1893        if (attributeFields != null) {
1894            for (RemotableAttributeField attributeField : attributeFields) {
1895                    attributeColumns.add(constructColumnFromAttributeField(attributeField));
1896            }
1897        }
1898        return attributeColumns;
1899    }
1900
1901    public static Formatter getFormatterForDataType(DataType dataType) {
1902        return Formatter.getFormatter(dataType.getType());
1903    }
1904
1905    /**
1906     * Finds a container field's sub tab name
1907     *
1908     * @param field the field for which to derive the collection sub tab name
1909     * @return the sub tab name
1910     */
1911    public static String generateCollectionSubTabName(Field field) {
1912        final String containerName = field.getContainerElementName();
1913        final String cleanedContainerName =
1914                (containerName == null) ?
1915                        "" :
1916                        containerName.replaceAll("\\d+", "");
1917        StringBuilder subTabName = new StringBuilder(cleanedContainerName);
1918        if (field.getContainerDisplayFields() != null) {
1919            for (Field containerField : field.getContainerDisplayFields()) {
1920                subTabName.append(containerField.getPropertyValue());
1921            }
1922        }
1923        return subTabName.toString();
1924    }
1925
1926    private static Map<String, String> toMap(String s) {
1927        if (StringUtils.isBlank(s)) {
1928            return Collections.emptyMap();
1929        }
1930        final Map<String, String> map = new HashMap<String, String>();
1931        for (String string : s.split(",")) {
1932            String [] keyVal = string.split(":");
1933            map.put(keyVal[0], keyVal[1]);
1934        }
1935        return Collections.unmodifiableMap(map);
1936    }
1937
1938    private static DataDictionaryService getDataDictionaryService() {
1939        if (dataDictionaryService == null) {
1940                dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1941        }
1942        return dataDictionaryService;
1943    }
1944
1945    private static BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
1946        if (businessObjectMetaDataService == null) {
1947                businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService();
1948        }
1949        return businessObjectMetaDataService;
1950    }
1951
1952    private static BusinessObjectDictionaryService getBusinessObjectDictionaryService() {
1953        if (businessObjectDictionaryService == null) {
1954                businessObjectDictionaryService = KNSServiceLocator.getBusinessObjectDictionaryService();
1955        }
1956        return businessObjectDictionaryService;
1957    }
1958
1959    private static KualiModuleService getKualiModuleService() {
1960        if (kualiModuleService == null) {
1961                kualiModuleService = KRADServiceLocatorWeb.getKualiModuleService();
1962        }
1963        return kualiModuleService;
1964    }
1965}