View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.util;
17  
18  import org.apache.commons.beanutils.NestedNullException;
19  import org.apache.commons.beanutils.PropertyUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.joda.time.DateTime;
22  import org.kuali.rice.core.api.CoreApiServiceLocator;
23  import org.kuali.rice.core.api.data.DataType;
24  import org.kuali.rice.core.api.encryption.EncryptionService;
25  import org.kuali.rice.core.api.mo.common.active.MutableInactivatable;
26  import org.kuali.rice.core.api.uif.AttributeLookupSettings;
27  import org.kuali.rice.core.api.uif.RemotableAbstractControl;
28  import org.kuali.rice.core.api.uif.RemotableAbstractWidget;
29  import org.kuali.rice.core.api.uif.RemotableAttributeField;
30  import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
31  import org.kuali.rice.core.api.uif.RemotableCheckbox;
32  import org.kuali.rice.core.api.uif.RemotableCheckboxGroup;
33  import org.kuali.rice.core.api.uif.RemotableControlContract;
34  import org.kuali.rice.core.api.uif.RemotableDatepicker;
35  import org.kuali.rice.core.api.uif.RemotableHiddenInput;
36  import org.kuali.rice.core.api.uif.RemotablePasswordInput;
37  import org.kuali.rice.core.api.uif.RemotableQuickFinder;
38  import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
39  import org.kuali.rice.core.api.uif.RemotableSelect;
40  import org.kuali.rice.core.api.uif.RemotableTextExpand;
41  import org.kuali.rice.core.api.uif.RemotableTextInput;
42  import org.kuali.rice.core.api.uif.RemotableTextarea;
43  import org.kuali.rice.core.api.util.ClassLoaderUtils;
44  import org.kuali.rice.core.api.util.ConcreteKeyValue;
45  import org.kuali.rice.core.api.util.KeyValue;
46  import org.kuali.rice.core.api.util.io.SerializationUtils;
47  import org.kuali.rice.core.web.format.FormatException;
48  import org.kuali.rice.core.web.format.Formatter;
49  import org.kuali.rice.kew.api.KewApiConstants;
50  import org.kuali.rice.kim.api.identity.Person;
51  import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
52  import org.kuali.rice.kns.datadictionary.FieldDefinition;
53  import org.kuali.rice.kns.datadictionary.MaintainableCollectionDefinition;
54  import org.kuali.rice.kns.datadictionary.control.ButtonControlDefinition;
55  import org.kuali.rice.kns.datadictionary.control.CurrencyControlDefinition;
56  import org.kuali.rice.kns.datadictionary.control.KualiUserControlDefinition;
57  import org.kuali.rice.kns.datadictionary.control.LinkControlDefinition;
58  import org.kuali.rice.kns.document.authorization.FieldRestriction;
59  import org.kuali.rice.kns.document.authorization.MaintenanceDocumentRestrictions;
60  import org.kuali.rice.kns.inquiry.Inquirable;
61  import org.kuali.rice.kns.lookup.HtmlData;
62  import org.kuali.rice.kns.lookup.HtmlData.AnchorHtmlData;
63  import org.kuali.rice.kns.lookup.LookupUtils;
64  import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
65  import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
66  import org.kuali.rice.kns.service.KNSServiceLocator;
67  import org.kuali.rice.kns.web.comparator.CellComparatorHelper;
68  import org.kuali.rice.kns.web.ui.Column;
69  import org.kuali.rice.kns.web.ui.Field;
70  import org.kuali.rice.kns.web.ui.PropertyRenderingConfigElement;
71  import org.kuali.rice.kns.web.ui.Row;
72  import org.kuali.rice.kns.web.ui.Section;
73  import org.kuali.rice.krad.bo.BusinessObject;
74  import org.kuali.rice.krad.bo.DataObjectRelationship;
75  import org.kuali.rice.krad.bo.KualiCode;
76  import org.kuali.rice.krad.bo.PersistableBusinessObject;
77  import org.kuali.rice.krad.datadictionary.control.ControlDefinition;
78  import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
79  import org.kuali.rice.krad.datadictionary.mask.MaskFormatter;
80  import org.kuali.rice.krad.keyvalues.IndicatorValuesFinder;
81  import org.kuali.rice.krad.keyvalues.KeyValuesFinder;
82  import org.kuali.rice.krad.keyvalues.PersistableBusinessObjectValuesFinder;
83  import org.kuali.rice.krad.service.DataDictionaryService;
84  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
85  import org.kuali.rice.krad.service.KualiModuleService;
86  import org.kuali.rice.krad.service.ModuleService;
87  import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
88  import org.kuali.rice.krad.util.GlobalVariables;
89  import org.kuali.rice.krad.util.KRADConstants;
90  import org.kuali.rice.krad.util.KRADPropertyConstants;
91  import org.kuali.rice.krad.util.MessageMap;
92  import org.kuali.rice.krad.util.ObjectUtils;
93  import org.kuali.rice.krad.valuefinder.ValueFinder;
94  
95  import java.lang.reflect.InvocationTargetException;
96  import java.security.GeneralSecurityException;
97  import java.util.ArrayList;
98  import java.util.Collection;
99  import java.util.Collections;
100 import java.util.HashMap;
101 import java.util.Iterator;
102 import java.util.LinkedHashMap;
103 import java.util.List;
104 import 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
113 public 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 }