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