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