View Javadoc

1   /**
2    * Copyright 2010 The Kuali Foundation Licensed under the Educational Community License, Version 2.0 (the "License"); you may
3    * not use this file except in compliance with the License. You may obtain a copy of the License at
4    * http://www.osedu.org/licenses/ECL-2.0 Unless required by applicable law or agreed to in writing, software distributed
5    * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
6    * implied. See the License for the specific language governing permissions and limitations under the License.
7    */
8   
9   package org.kuali.student.common.assembly.dictionary;
10  
11  import java.text.DateFormat;
12  import java.text.ParseException;
13  import java.text.SimpleDateFormat;
14  import java.util.ArrayList;
15  import java.util.HashMap;
16  import java.util.List;
17  import java.util.Map;
18  
19  import org.apache.log4j.Logger;
20  import org.kuali.student.common.assembly.data.ConstraintMetadata;
21  import org.kuali.student.common.assembly.data.Data;
22  import org.kuali.student.common.assembly.data.LookupMetadata;
23  import org.kuali.student.common.assembly.data.LookupParamMetadata;
24  import org.kuali.student.common.assembly.data.Metadata;
25  import org.kuali.student.common.assembly.data.UILookupConfig;
26  import org.kuali.student.common.assembly.data.UILookupData;
27  import org.kuali.student.common.assembly.data.Data.DataType;
28  import org.kuali.student.common.assembly.data.Data.Value;
29  import org.kuali.student.common.assembly.data.LookupMetadata.WidgetOption;
30  import org.kuali.student.common.assembly.data.Metadata.WriteAccess;
31  import org.kuali.student.common.dictionary.dto.CaseConstraint;
32  import org.kuali.student.common.dictionary.dto.CommonLookupParam;
33  import org.kuali.student.common.dictionary.dto.Constraint;
34  import org.kuali.student.common.dictionary.dto.FieldDefinition;
35  import org.kuali.student.common.dictionary.dto.ObjectStructureDefinition;
36  import org.kuali.student.common.dictionary.dto.WhenConstraint;
37  import org.kuali.student.common.dictionary.service.DictionaryService;
38  import org.kuali.student.common.dto.DtoConstants;
39  import org.kuali.student.common.dto.DtoConstants.DtoState;
40  import org.kuali.student.common.validation.dto.ValidationResultInfo.ErrorLevel;
41  import org.springframework.beans.BeanUtils;
42  import org.springframework.context.ApplicationContext;
43  import org.springframework.context.support.ClassPathXmlApplicationContext;
44  
45  import edu.emory.mathcs.backport.java.util.Arrays;
46  
47  /**
48   * This class provides metadata lookup for service dto objects.
49   * 
50   * @author Kuali Student Team
51   */
52  public class MetadataServiceImpl {
53      final Logger LOG = Logger.getLogger(MetadataServiceImpl.class);
54  
55      private Map<String, DictionaryService> dictionaryServiceMap = new HashMap<String, DictionaryService>();
56      private List<UILookupConfig> lookupObjectStructures;
57      private String uiLookupContext;
58  
59      private static class RecursionCounter {
60          public static final int MAX_DEPTH = 4;
61  
62          private Map<String, Integer> recursions = new HashMap<String, Integer>();
63  
64          public int increment(String objectName) {
65              Integer hits = recursions.get(objectName);
66  
67              if (hits == null) {
68                  hits = new Integer(1);
69              } else {
70                  hits++;
71              }
72              recursions.put(objectName, hits);
73              return hits;
74          }
75  
76          public int decrement(String objectName) {
77              Integer hits = recursions.get(objectName);
78              if (hits >= 1) {
79                  hits--;
80              }
81  
82              recursions.put(objectName, hits);
83              return hits;
84          }
85      }
86  
87  	public MetadataServiceImpl() {
88  		super();
89  	}
90  
91      /**
92       * Create a metadata service initializing it with all known dictionary services
93       * 
94       * @param dictionaryServices
95       */
96      public MetadataServiceImpl(DictionaryService... dictionaryServices) {
97      	setDictionaryServices(Arrays.asList(dictionaryServices));
98      }
99  
100     public synchronized void setDictionaryServices(List<DictionaryService> dictionaryServices) {
101     	if (dictionaryServices != null) {
102     		this.dictionaryServiceMap.clear();
103             for (DictionaryService d : dictionaryServices) {
104                 List<String> objectTypes = d.getObjectTypes();
105                 for (String objectType : objectTypes) {
106                     dictionaryServiceMap.put(objectType, d);
107                 }
108             }
109     	}
110 	}
111 
112 
113 	/**
114      * This method gets the metadata for the given object key, type, state and nextState
115      * 
116      * @param objectKey
117      * @param type The type of the object (value can be null)
118      * @param state The state for which to retrieve object constraints (value can be null)
119      * @param nextState The state to to check requiredForNextState indicators (value can be null)
120      * @param documentTypeName The type of the document (value can be null)
121      * @return
122      */
123     public Metadata getMetadata(String objectKey, String type, String state, String nextState, String documentTypeName) {
124     	nextState = (nextState == null || nextState.length() <=0 ? DtoState.getNextStateAsString(state):nextState);
125     	state = state==null?null:state.toUpperCase();
126     	nextState = nextState==null?null:nextState.toUpperCase();
127     	//FIXME/TODO: documentTypeName is only passed here, because it is eventually used in ProgramMetadataServiceImpl's getConstraints() method
128     	return getMetadataFromDictionaryService(objectKey, type, state, nextState, null, documentTypeName);
129     }
130 
131 	/**
132      * This method gets the metadata for the given object id key, workflowNode and documentTypeName
133      * 
134      * @return
135      */
136     public Metadata getMetadataByWorkflowNode(String objectKey, String workflowNode, String documentTypeName) {
137     	//FIXME/TODO: documentTypeName is only passed here, because it is eventually used in ProgramMetadataServiceImpl's getConstraints() method
138     	return getMetadataFromDictionaryService(objectKey, null, DtoState.DRAFT.toString(), null, workflowNode, documentTypeName);
139     }
140 
141     /**
142      * This method gets the metadata for the given object key, type and state
143      * 
144      * @param objectKey
145      * @param type The type of the object (value can be null)
146      * @param state The state for which to retrieve object constraints (value can be null)
147      * @return
148      */
149     public Metadata getMetadata(String objectKey, String type, String state) {
150     	state = (state == null ? DtoState.DRAFT.toString():state);
151     	String nextState = DtoState.getNextStateAsString(state);
152     	
153     	return getMetadata(objectKey, type, state.toUpperCase(), nextState, null);
154     }
155 
156 
157     /**
158      * This method gets the metadata for the given object key and state
159      * 
160      * @param objectKey
161      * @param type The type of the object (value can be null)
162      */
163     public Metadata getMetadata(String objectKey, String state) {
164     	return getMetadata(objectKey, null, state);
165     }
166 
167     /**
168      * This method gets the metadata for the given object key for state DRAFT.
169      * 
170      * @see MetadataServiceImpl#getMetadata(String, String)
171      * @param objectKey
172      * @return
173      */
174     public Metadata getMetadata(String objectKey) {
175         return getMetadata(objectKey, null, null);
176     }
177 
178     /**
179      * This invokes the appropriate dictionary service to get the object structure and then converts it to a metadata
180      * structure.
181      * 
182      * @param objectKey
183      * @param type
184      * @param state
185      * @param documentTypeName
186      * @return
187      */
188     protected Metadata getMetadataFromDictionaryService(String objectKey, String type, String state, String nextState, String workflowNode, String documentTypeName) {
189         
190         Metadata metadata = new Metadata();
191 
192         ObjectStructureDefinition objectStructure = getObjectStructure(objectKey);
193 		//FIXME/TODO: documentTypeName is only passed here, because it is eventually used in ProgramMetadataServiceImpl's getConstraints() method
194         metadata.setProperties(getProperties(objectStructure, type, state, nextState, workflowNode, new RecursionCounter(), documentTypeName));
195 
196         metadata.setWriteAccess(WriteAccess.ALWAYS);
197         metadata.setDataType(DataType.DATA);
198         addLookupstoMetadata(objectKey, metadata, type);
199         return metadata;
200     }
201 
202     /**
203      * This method is used to convert a list of dictionary fields into metadata properties
204      * @param type
205      * @param state
206      * @param documentTypeName TODO
207      * @param fields
208      * 
209      * @return
210      */
211     private Map<String, Metadata> getProperties(ObjectStructureDefinition objectStructure, String type, String state, String nextState, String workflowNode, RecursionCounter counter, String documentTypeName) {
212         String objectName = objectStructure.getName();
213         int hits = counter.increment(objectName);
214 
215         Map<String, Metadata> properties = null;
216 
217         if (hits < RecursionCounter.MAX_DEPTH) {
218             properties = new HashMap<String, Metadata>();
219 
220             List<FieldDefinition> attributes = objectStructure.getAttributes();
221             for (FieldDefinition fd : attributes) {
222 
223                 Metadata metadata = new Metadata();
224 
225                 // Set constraints, authz flags, default value
226                 metadata.setWriteAccess(WriteAccess.ALWAYS);
227                 metadata.setDataType(convertDictionaryDataType(fd.getDataType()));
228                 //FIXME/TODO: documentTypeName is only passed here, because it is eventually used in ProgramMetadataServiceImpl's getConstraints() method
229                 metadata.setConstraints(getConstraints(fd, type, state, nextState, workflowNode, documentTypeName));
230                 metadata.setCanEdit(!fd.isReadOnly());
231                 metadata.setCanUnmask(!fd.isMask());
232                 metadata.setCanView(!fd.isHide());
233                 metadata.setDynamic(fd.isDynamic());
234                 metadata.setLabelKey(fd.getLabelKey());
235                 metadata.setDefaultValue(convertDefaultValue(metadata.getDataType(), fd.getDefaultValue()));
236                 metadata.setDefaultValuePath(fd.getDefaultValuePath());
237                 
238 	           	if (fd.isPartialMask()){
239 	           		metadata.setPartialMaskFormatter(fd.getPartialMaskFormatter());
240 	           	}
241 	           	
242 	           	if (fd.isMask()){
243 	           		metadata.setMaskFormatter(fd.getMaskFormatter());
244 	           	}
245 
246                 // Get properties for nested object structure
247                 Map<String, Metadata> nestedProperties = null;
248                 if (fd.getDataType() == org.kuali.student.common.dictionary.dto.DataType.COMPLEX && fd.getDataObjectStructure() != null) {
249                     nestedProperties = getProperties(fd.getDataObjectStructure(), type, state, nextState, workflowNode, counter, documentTypeName);
250                 }
251 
252                 // For repeating field, create a LIST with wildcard in metadata structure
253                 if (isRepeating(fd)) {
254                     Metadata repeatingMetadata = new Metadata();
255                     metadata.setDataType(DataType.LIST);
256 
257                     repeatingMetadata.setWriteAccess(WriteAccess.ALWAYS);
258                     repeatingMetadata.setOnChangeRefreshMetadata(false);
259                     repeatingMetadata.setDataType(convertDictionaryDataType(fd.getDataType()));
260 
261                     if (nestedProperties != null) {
262                         repeatingMetadata.setProperties(nestedProperties);
263                     }
264 
265                     Map<String, Metadata> repeatingProperty = new HashMap<String, Metadata>();
266                     repeatingProperty.put("*", repeatingMetadata);
267                     metadata.setProperties(repeatingProperty);
268                 } else if (nestedProperties != null) {
269                     metadata.setProperties(nestedProperties);
270                 }
271 
272                 properties.put(fd.getName(), metadata);
273 
274             }
275         }
276 
277         counter.decrement(objectName);
278         return properties;
279     }
280 
281     /**
282      * This method determines if a field is repeating
283      * 
284      * @param fd
285      * @return
286      */
287     protected boolean isRepeating(FieldDefinition fd) {
288         boolean isRepeating = false;
289         try {
290             int maxOccurs = Integer.parseInt(fd.getMaxOccurs());
291             isRepeating = maxOccurs > 1;
292         } catch (NumberFormatException nfe) {
293             isRepeating = FieldDefinition.UNBOUNDED.equals(fd.getMaxOccurs());
294         }
295 
296         return isRepeating;
297     }
298 
299     /**
300      * This method gets the object structure for given objectKey from a dictionaryService
301      * 
302      * @param objectKey
303      * @return
304      */
305     protected ObjectStructureDefinition getObjectStructure(String objectKey) {
306         DictionaryService dictionaryService = dictionaryServiceMap.get(objectKey);
307 
308         if (dictionaryService == null) {
309             throw new RuntimeException("Dictionary service not provided for objectKey=[" + objectKey + "].");
310         }
311 
312         return dictionaryService.getObjectStructure(objectKey);
313     }
314 
315 	//FIXME/TODO: documentTypeName is only passed here, because it is used(overridden) in ProgramMetadataServiceImpl's getConstraints() method
316     protected List<ConstraintMetadata> getConstraints(FieldDefinition fd, String type, String state, String nextState, String workflowNode, String documentTypeName) {
317         List<ConstraintMetadata> constraints = new ArrayList<ConstraintMetadata>();
318 
319         ConstraintMetadata constraintMetadata = new ConstraintMetadata();
320 
321         updateConstraintMetadata(constraintMetadata, (Constraint) fd, type, state, nextState, workflowNode);
322         constraints.add(constraintMetadata);
323 
324         return constraints;
325     }
326 
327     /**
328      * This updates the constraintMetadata with defintions from the dictionary constraint field.
329      * 
330      * @param constraintMetadata
331      * @param constraint
332      */
333     protected void updateConstraintMetadata(ConstraintMetadata constraintMetadata, Constraint constraint, String type, String state, String nextState, String workflowNode) {
334         // For now ignoring the serverSide flag and making determination of which constraints
335         // should be passed up to the UI via metadata.
336 
337         // Min Length
338         if (constraint.getMinLength() != null) {
339             constraintMetadata.setMinLength(constraint.getMinLength());
340         }
341 
342         // Max Length
343         try {
344             if (constraint.getMaxLength() != null) {
345                 constraintMetadata.setMaxLength(Integer.parseInt(constraint.getMaxLength()));
346             }
347             // Do we need to add another constraint and label it required if minOccurs = 1
348         } catch (NumberFormatException nfe) {
349             // Ignoring an unbounded length, cannot be handled in metadata structure, maybe change Metadata to string or set
350             // to -1
351             constraintMetadata.setMaxLength(9999);
352         }
353 
354         // Min Occurs
355         if (constraint.getMinOccurs() != null) {
356             constraintMetadata.setMinOccurs(constraint.getMinOccurs());
357         }
358 
359         // Max Occurs
360         String maxOccurs = constraint.getMaxOccurs();
361         if (maxOccurs != null) {
362             try {
363                 constraintMetadata.setMaxOccurs(Integer.parseInt(maxOccurs));
364                 if (!FieldDefinition.SINGLE.equals(maxOccurs)) {
365                     constraintMetadata.setId("repeating");
366                 }
367             } catch (NumberFormatException nfe) {
368                 // Setting unbounded to a value of 9999, since unbounded not handled by metadata
369                 if (FieldDefinition.UNBOUNDED.equals(maxOccurs)) {
370                     constraintMetadata.setId("repeating");
371                     constraintMetadata.setMaxOccurs(9999);
372                 }
373             }
374         }
375 
376         // Min Value
377         if (constraint.getExclusiveMin() != null) {
378             constraintMetadata.setMinValue(constraint.getExclusiveMin());
379         }
380 
381         // Max Value
382         if (constraint.getInclusiveMax() != null) {
383             constraintMetadata.setMaxValue(constraint.getInclusiveMax());
384         }
385 
386         if (constraint.getValidChars() != null) {
387             constraintMetadata.setValidChars(constraint.getValidChars().getValue());
388             constraintMetadata.setValidCharsMessageId(constraint.getValidChars().getLabelKey());
389         }
390 
391         // Case constraints
392         if (constraint.getCaseConstraint() != null) {
393             processCaseConstraint(constraintMetadata, constraint.getCaseConstraint(), type, state, nextState, workflowNode);
394         }
395     }
396 
397     /**
398      * Currently this only handles requiredness indicators for case constraints with the following field paths:
399      * 
400      *  type, state, and proposal/workflowNode
401      */
402     protected void processCaseConstraint(ConstraintMetadata constraintMetadata, CaseConstraint caseConstraint, String type, String state, String nextState, String workflowNode) {
403         String fieldPath = caseConstraint.getFieldPath();
404         fieldPath = (fieldPath != null ? fieldPath.toUpperCase() : fieldPath);
405         
406         if (workflowNode != null && fieldPath != null && fieldPath.startsWith("PROPOSAL/WORKFLOWNODE")){
407         	processRequiredByNodeCaseConstraint(constraintMetadata, caseConstraint, workflowNode);        	
408         } else if ("STATE".equals(fieldPath)) {
409         	processStateCaseConstraint(constraintMetadata, caseConstraint, type, state, nextState, workflowNode);
410         } else if ("TYPE".equals(fieldPath)) {
411         	processTypeCaseConstraint(constraintMetadata, caseConstraint, type, state, nextState, workflowNode);
412         }
413     }
414         
415     /**
416      * Modifies the constraintMetadata to add required to save or required to approve constraints based on the 
417      * workflow route node the proposal is currently in.
418      * 
419      * @param constraintMetadata The fields constraintMetadata to be modified
420      * @param caseConstraint The caseConstraint defined in dictionary for field
421      * @param workflowNode The current node in workflow process
422      */
423     private void processRequiredByNodeCaseConstraint(ConstraintMetadata constraintMetadata, CaseConstraint caseConstraint,  String workflowNode) {
424         List<WhenConstraint> whenConstraints = caseConstraint.getWhenConstraint();
425         
426     	if ("EQUALS".equals(caseConstraint.getOperator()) && whenConstraints != null) {
427             for (WhenConstraint whenConstraint : whenConstraints) {
428                 List<Object> values = whenConstraint.getValues();
429                 Constraint constraint = whenConstraint.getConstraint();
430 
431                 if (constraint.getErrorLevel() == ErrorLevel.ERROR && constraint.getMinOccurs() != null && constraint.getMinOccurs() > 0){
432                     //This is a required field, so need to determine if it is required to save or required to approve based on the
433                 	//workflowNode parameter. The order of workflow nodes defined in the case constraint on this field is important in 
434                 	//determining if required to approve or required to save. If the workflowNode parameter equals is the first  
435                 	//node defined in the constraint, then it's required to approve, otherwise it's required to save.
436                		
437                 	if (isWorkflowNodeFirstConstraintValue(workflowNode, values)) {
438                 		//Field is required to approve. Indicated this by setting the required for next state flag in metadata.
439                 		//If node is PreRoute, then the next state will be set to "SUBMIT" to indicate submit action, otherwise
440                 		//will be set to "APPROVED" to indicate approval action for node transition.
441                			constraintMetadata.setRequiredForNextState(true);
442                			if (DtoConstants.WORKFLOW_NODE_PRE_ROUTE.equals(workflowNode)){
443                				constraintMetadata.setNextState(DtoState.SUBMITTED.toString());
444                			} else {
445                				constraintMetadata.setNextState(DtoState.APPROVED.toString());
446                			}
447                			constraintMetadata.setMinOccurs(0);
448                     } else if (values.contains(workflowNode)){
449                     	//Field is required only for save
450                			constraintMetadata.setRequiredForNextState(false);
451                			constraintMetadata.setNextState(null);
452                			constraintMetadata.setMinOccurs(1);
453                     }
454                 }
455             }
456         }
457     }
458     
459     /** 
460      * @param values
461      * @param workflowNode
462      * @return true if workflowNode is first item in values, otherwise returns false
463      */
464     private boolean isWorkflowNodeFirstConstraintValue(String workflowNode, List<Object> values){
465     	if (values != null && !values.isEmpty()){
466     		return values.get(0).equals(workflowNode);
467     	} else {
468     		return false;
469     	}
470     }
471 
472 	/**
473      * Processes a case constraint with field path of state. 
474      */
475     private void processStateCaseConstraint(ConstraintMetadata constraintMetadata,	CaseConstraint caseConstraint, String type, String state, String nextState, String workflowNode) {
476         List<WhenConstraint> whenConstraints = caseConstraint.getWhenConstraint();
477         
478         if ("EQUALS".equals(caseConstraint.getOperator()) && whenConstraints != null) {
479             for (WhenConstraint whenConstraint : whenConstraints) {
480                 List<Object> values = whenConstraint.getValues();
481                 if (values != null) {
482                     Constraint constraint = whenConstraint.getConstraint();
483 
484                     if (constraint.getErrorLevel() == ErrorLevel.ERROR){
485                     	//NOTE: if the constraint has a nested constraint with fieldPath="lookup:proposal...", 
486                     	//the required, requiredForNextState, and nextState values will be reset based on workflow node	           
487                     	
488                     	// Set the required for next state flag. 
489                         if (values.contains(nextState)) {
490                             if (constraint.getMinOccurs() != null && constraint.getMinOccurs() > 0) {
491                                 constraintMetadata.setRequiredForNextState(true);
492                                 constraintMetadata.setNextState(nextState);
493                             }
494                         }
495 
496                         // Update constraints based on state constraints
497                         if (values.contains(state)) {
498                             updateConstraintMetadata(constraintMetadata, constraint, type, state, nextState, workflowNode);
499                         }
500                     }
501                 }
502             }
503         }		
504 	}
505     
506     /**
507      * Process a case constraint with fieldPath of type 
508      */
509     private void processTypeCaseConstraint(ConstraintMetadata constraintMetadata, CaseConstraint caseConstraint, String type, String state,	String nextState, String workflowNode) {
510         List<WhenConstraint> whenConstraints = caseConstraint.getWhenConstraint();
511     	
512         if ("EQUALS".equals(caseConstraint.getOperator()) && whenConstraints != null) {
513             for (WhenConstraint whenConstraint : whenConstraints) {
514                 List<Object> values = whenConstraint.getValues();
515                 if (values != null && values.contains(type)) {
516                     Constraint constraint = whenConstraint.getConstraint();
517                     updateConstraintMetadata(constraintMetadata, constraint, type, state, nextState, workflowNode);
518                 }
519             }
520         }		
521 	}
522     
523 
524 	/**
525      * Convert Object value to respective DataType. Method return null for object Value.
526      * 
527      * @param dataType
528      * @param value
529      * @return
530      */
531     protected Value convertDefaultValue(DataType dataType, Object value) {
532         Value v = null;
533         if (value instanceof String) {
534             String s = (String) value;
535             switch (dataType) {
536                 case STRING:
537                     v = new Data.StringValue(s);
538                     break;
539                 case BOOLEAN:
540                     v = new Data.BooleanValue(Boolean.valueOf(s));
541                     break;
542                 case FLOAT:
543                     v = new Data.FloatValue(Float.valueOf(s));
544                     break;
545                 case DATE:
546                     DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
547                     try {
548                         v = new Data.DateValue(format.parse(s));
549                     } catch (ParseException e) {
550                         LOG.error("Unable to get default date value from metadata definition");
551                     }
552                     break;
553                 case LONG:
554                     if (!s.isEmpty()) {
555                         v = new Data.LongValue(Long.valueOf(s));
556                     }
557                     break;
558                 case DOUBLE:
559                     v = new Data.DoubleValue(Double.valueOf(s));
560                     break;
561                 case INTEGER:
562                     v = new Data.IntegerValue(Integer.valueOf(s));
563                     break;
564             }
565         } else {
566             v = convertDefaultValue(value);
567         }
568 
569         return v;
570     }
571 
572     protected Value convertDefaultValue(Object value) {
573         Value v = null;
574 
575         if (value instanceof String) {
576             v = new Data.StringValue((String) value);
577         } else if (value instanceof Boolean) {
578             v = new Data.BooleanValue((Boolean) value);
579         } else if (value instanceof Integer) {
580             v = new Data.IntegerValue((Integer) value);
581         } else if (value instanceof Double) {
582             v = new Data.DoubleValue((Double) value);
583         } else if (value instanceof Long) {
584             v = new Data.LongValue((Long) value);
585         } else if (value instanceof Short) {
586             v = new Data.ShortValue((Short) value);
587         } else if (value instanceof Float) {
588             v = new Data.FloatValue((Float) value);
589         }
590 
591         return v;
592     }
593 
594     protected DataType convertDictionaryDataType(org.kuali.student.common.dictionary.dto.DataType dataType) {
595         switch (dataType) {
596             case STRING:
597                 return DataType.STRING;
598             case BOOLEAN:
599                 return DataType.BOOLEAN;
600             case INTEGER:
601                 return DataType.INTEGER;
602             case FLOAT:
603                 return DataType.FLOAT;
604             case COMPLEX:
605                 return DataType.DATA;
606             case DATE:
607                 return DataType.DATE;
608             case DOUBLE:
609                 return DataType.DOUBLE;
610             case LONG:
611                 return DataType.LONG;
612         }
613 
614         return null;
615     }
616 
617     public void setUiLookupContext(String uiLookupContext) {
618         this.uiLookupContext = uiLookupContext;
619         init();
620 
621     }
622 
623     @SuppressWarnings("unchecked")
624     private void init() {
625         ApplicationContext ac = new ClassPathXmlApplicationContext(uiLookupContext);
626 
627         Map<String, UILookupConfig> beansOfType = (Map<String, UILookupConfig>) ac.getBeansOfType(UILookupConfig.class);
628         lookupObjectStructures = new ArrayList<UILookupConfig>();
629         for (UILookupConfig objStr : beansOfType.values()) {
630             lookupObjectStructures.add(objStr);
631         }
632         System.out.println("UILookup loaded");
633     }
634 
635     private String calcSimpleName(String objectKey) {
636         int lastDot = objectKey.lastIndexOf(".");
637         if (lastDot == -1) {
638             return objectKey;
639         }
640         return objectKey.substring(lastDot + 1);
641 
642     }
643 
644     private boolean matchesObjectKey(String objectKey, String path) {
645         String simpleName = calcSimpleName(objectKey);
646         if (path.toLowerCase().startsWith(simpleName.toLowerCase())) {
647             // System.out.println ("matchesObjectKey: is TRUE for " + objectKey + " and " + path);
648             return true;
649         }
650         // System.out.println ("matchesObjectKey: is FALSE for " + objectKey + " and " + path);
651         return false;
652     }
653 
654     private boolean matchesType(String paramType, String lookupType) {
655         // both null
656         if (paramType == null && lookupType == null) {
657             return true;
658         }
659         // not asking for type specific but the lookup defnition is type specific then
660         // no match
661         if (paramType == null && lookupType != null) {
662             return false;
663         }
664         // if looking for type specific but the lookup is not specific then
665         // take as default
666         // If configuration has both a null type (i.e. default) AND has a type
667         // specific one the type specific one has to be entered into the configuration
668         // file first so it is found first
669         if (paramType != null && lookupType == null) {
670             return true;
671         }
672         if (paramType.equalsIgnoreCase(lookupType)) {
673             // System.out.println ("matchesType: is TRUE for " + paramType + " and " + lookupType);
674             return true;
675         }
676         // System.out.println ("matchesType: is FALSE for " + paramType + " and " + lookupType);
677         return false;
678     }
679 
680     private void addLookupstoMetadata(String objectKey, Metadata metadata, String type) {
681         if (lookupObjectStructures != null) {
682             for (UILookupConfig lookup : lookupObjectStructures) {
683                 if (!matchesObjectKey(objectKey, lookup.getPath())) {
684                     continue;
685                 }
686                 if (!matchesType(type, lookup.getType())) {
687                     continue;
688                 }
689                 // TODO: figure out why path=courseInfo.creditOptions.type matches any structure that has a type on it so
690                 // that lookup gets returned for all types
691                 Map<String, Metadata> parsedMetadataMap = metadata.getProperties();
692                 Metadata parsedMetadata = null;
693                 String parsedMetadataKey = "";
694                 String lookupFieldPath = lookup.getPath();
695                 String[] lookupPathTokens = getPathTokens(lookupFieldPath);
696                 for (int i = 1; i < lookupPathTokens.length; i++) {
697                     if (parsedMetadataMap == null) {
698                         break;
699                     }
700                     if (i == lookupPathTokens.length - 1) {
701                         // get the metadata on the last path key token
702                         parsedMetadata = parsedMetadataMap.get(lookupPathTokens[i]);
703                         parsedMetadataKey = parsedMetadataKey + "." + lookupPathTokens[i];
704                     }
705                     if (parsedMetadataMap.get(lookupPathTokens[i]) != null) {
706                         parsedMetadataMap = parsedMetadataMap.get(lookupPathTokens[i]).getProperties();
707                     } else if (parsedMetadataMap.get("*") != null) {
708                         // Lookup wildcard in case of unbounded elements in metadata.
709                         parsedMetadataMap = parsedMetadataMap.get("*").getProperties();
710                         i--;
711                     }
712 
713                 }
714                 if (parsedMetadata != null) {
715                     // System.out.println ("addLookupstoMetadata:" + parsedMetadataKey + " was found as a match for " +
716                     // lookup.getPath ());
717                     UILookupData initialLookup = lookup.getInitialLookup();
718                     if (initialLookup != null) {
719                         mapLookupDatatoMeta(initialLookup);
720                         parsedMetadata.setInitialLookup(mapLookupDatatoMeta(lookup.getInitialLookup()));
721                     }
722                     List<LookupMetadata> additionalLookupMetadata = null;
723                     if (lookup.getAdditionalLookups() != null) {
724                         additionalLookupMetadata = new ArrayList<LookupMetadata>();
725                         for (UILookupData additionallookup : lookup.getAdditionalLookups()) {
726                             additionalLookupMetadata.add(mapLookupDatatoMeta(additionallookup));
727                         }
728                         parsedMetadata.setAdditionalLookups(additionalLookupMetadata);
729                     }
730                 }
731             }
732         }
733     }
734 
735     private LookupMetadata mapLookupDatatoMeta(UILookupData lookupData) {
736         LookupMetadata lookupMetadata = new LookupMetadata();
737         List<LookupParamMetadata> paramsMetadata;
738         BeanUtils.copyProperties(lookupData, lookupMetadata, new String[]{"widget", "usage", "widgetOptions", "params"});
739         if (lookupData.getWidget() != null) {
740             lookupMetadata.setWidget(org.kuali.student.common.assembly.data.LookupMetadata.Widget.valueOf(lookupData.getWidget().toString()));
741         }
742         if (lookupData.getUsage() != null) {
743             lookupMetadata.setUsage(org.kuali.student.common.assembly.data.LookupMetadata.Usage.valueOf(lookupData.getUsage().toString()));
744         }
745         if (lookupData.getWidgetOptions () != null) {
746          lookupMetadata.setWidgetOptions (new HashMap<WidgetOption, String> ());
747          for (UILookupData.WidgetOption wo: lookupData.getWidgetOptions ().keySet ()) {
748           String value = lookupData.getWidgetOptions ().get (wo);
749           LookupMetadata.WidgetOption key = LookupMetadata.WidgetOption.valueOf(wo.toString());
750           lookupMetadata.getWidgetOptions ().put (key, value);
751          }
752         }
753         if (lookupData.getParams() != null) {
754             paramsMetadata = new ArrayList<LookupParamMetadata>();
755             for (CommonLookupParam param : lookupData.getParams()) {
756                 paramsMetadata.add(mapLookupParamMetadata(param));
757             }
758             lookupMetadata.setParams(paramsMetadata);
759         }
760         // WidgetOptions is not used as of now. So not setting it into metadata.
761         return lookupMetadata;
762     }
763 
764     private LookupParamMetadata mapLookupParamMetadata(CommonLookupParam param) {
765         LookupParamMetadata paramMetadata = new LookupParamMetadata();
766         BeanUtils.copyProperties(param, paramMetadata, new String[]{"childLookup", "dataType", "writeAccess", "usage", "widget"});
767         if (param.getChildLookup() != null) {
768             paramMetadata.setChildLookup(mapLookupDatatoMeta((UILookupData) param.getChildLookup()));
769         }
770         if (param.getDataType() != null) {
771             paramMetadata.setDataType(org.kuali.student.common.assembly.data.Data.DataType.valueOf(param.getDataType().toString()));
772         }
773         if (param.getWriteAccess() != null) {
774             paramMetadata.setWriteAccess(org.kuali.student.common.assembly.data.Metadata.WriteAccess.valueOf(param.getWriteAccess().toString()));
775         }
776         if (param.getUsage() != null) {
777             paramMetadata.setUsage(org.kuali.student.common.assembly.data.LookupMetadata.Usage.valueOf(param.getUsage().toString()));
778         }
779         if (param.getWidget() != null) {
780             paramMetadata.setWidget(org.kuali.student.common.assembly.data.LookupParamMetadata.Widget.valueOf(param.getWidget().toString()));
781         }
782 
783         return paramMetadata;
784     }
785 
786     private static String[] getPathTokens(String fieldPath) {
787         return (fieldPath != null && fieldPath.contains(".") ? fieldPath.split("\\.") : new String[]{fieldPath});
788     }
789 }