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