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