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