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