View Javadoc
1   /**
2    * Copyright 2005-2014 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.krad.data.provider.annotation.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.data.DataType;
20  import org.kuali.rice.krad.data.DataObjectService;
21  import org.kuali.rice.krad.data.KradDataServiceLocator;
22  import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
23  import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
24  import org.kuali.rice.krad.data.metadata.DataObjectCollection;
25  import org.kuali.rice.krad.data.metadata.DataObjectCollectionSortAttribute;
26  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
27  import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
28  import org.kuali.rice.krad.data.metadata.MetadataConfigurationException;
29  import org.kuali.rice.krad.data.metadata.MetadataMergeAction;
30  import org.kuali.rice.krad.data.metadata.MetadataRepository;
31  import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeImpl;
32  import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeRelationshipImpl;
33  import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionImpl;
34  import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionSortAttributeImpl;
35  import org.kuali.rice.krad.data.metadata.impl.DataObjectMetadataImpl;
36  import org.kuali.rice.krad.data.metadata.impl.DataObjectRelationshipImpl;
37  import org.kuali.rice.krad.data.metadata.impl.MetadataCommonBase;
38  import org.kuali.rice.krad.data.provider.annotation.AttributeRelationship;
39  import org.kuali.rice.krad.data.provider.annotation.BusinessKey;
40  import org.kuali.rice.krad.data.provider.annotation.CollectionRelationship;
41  import org.kuali.rice.krad.data.provider.annotation.CollectionSortAttribute;
42  import org.kuali.rice.krad.data.provider.annotation.Description;
43  import org.kuali.rice.krad.data.provider.annotation.ForceUppercase;
44  import org.kuali.rice.krad.data.provider.annotation.InheritProperties;
45  import org.kuali.rice.krad.data.provider.annotation.InheritProperty;
46  import org.kuali.rice.krad.data.provider.annotation.KeyValuesFinderClass;
47  import org.kuali.rice.krad.data.provider.annotation.Label;
48  import org.kuali.rice.krad.data.provider.annotation.MergeAction;
49  import org.kuali.rice.krad.data.provider.annotation.NonPersistentProperty;
50  import org.kuali.rice.krad.data.provider.annotation.PropertyEditorClass;
51  import org.kuali.rice.krad.data.provider.annotation.ReadOnly;
52  import org.kuali.rice.krad.data.provider.annotation.Relationship;
53  import org.kuali.rice.krad.data.provider.annotation.Sensitive;
54  import org.kuali.rice.krad.data.provider.annotation.ShortLabel;
55  import org.kuali.rice.krad.data.provider.annotation.UifAutoCreateViews;
56  import org.kuali.rice.krad.data.provider.annotation.UifDisplayHint;
57  import org.kuali.rice.krad.data.provider.annotation.UifDisplayHints;
58  import org.kuali.rice.krad.data.provider.annotation.UifValidCharactersConstraintBeanName;
59  import org.kuali.rice.krad.data.provider.impl.MetadataProviderBase;
60  
61  import javax.validation.constraints.NotNull;
62  import javax.validation.constraints.Size;
63  import java.lang.annotation.Annotation;
64  import java.lang.reflect.Field;
65  import java.lang.reflect.Method;
66  import java.lang.reflect.ParameterizedType;
67  import java.lang.reflect.Type;
68  import java.util.ArrayList;
69  import java.util.Arrays;
70  import java.util.Collection;
71  import java.util.Collections;
72  import java.util.HashSet;
73  import java.util.List;
74  
75  /**
76   * Parses custom krad-data annotations for additional metadata to layer on top of that provided by the persistence
77   * metadata provider which should have run before this one.
78   *
79   * <p>
80   * At the moment, it will only process classes which were previously identified by the JPA implementation.
81   * </p>
82   *
83   * <p>
84   * TODO: Addition of a new Annotation which will need to be scanned for in order to process non-persistent classes as data objects.
85   * </p>
86   * 
87   * @author Kuali Rice Team (rice.collab@kuali.org)
88   */
89  public class AnnotationMetadataProviderImpl extends MetadataProviderBase {
90  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
91  			.getLogger(AnnotationMetadataProviderImpl.class);
92  
93  	private boolean initializationAttempted = false;
94      private DataObjectService dataObjectService;
95  
96      /**
97       * {@inheritDoc}
98       */
99  	@Override
100 	protected void initializeMetadata(Collection<Class<?>> types) {
101 		if (initializationAttempted) {
102 			return;
103 		}
104         initializationAttempted = true;
105 		if (LOG.isDebugEnabled()) {
106 			LOG.debug("Processing annotations for the given list of data objects: " + types);
107 		}
108 		if (types == null || types.isEmpty()) {
109             LOG.warn(getClass().getSimpleName() + " was passed an empty list of types to initialize, doing nothing");
110             return;
111 		}
112 		LOG.info("Started Scanning For Metadata Annotations");
113 		for (Class<?> type : types) {
114 			if (LOG.isDebugEnabled()) {
115 				LOG.debug("Processing Annotations on : " + type);
116 			}
117 			boolean annotationsFound = false;
118 			DataObjectMetadataImpl metadata = new DataObjectMetadataImpl();
119 			metadata.setProviderName(this.getClass().getSimpleName());
120 			metadata.setType(type);
121 			// check for class level annotations
122 			annotationsFound |= processClassLevelAnnotations(type, metadata);
123 			// check for field level annotations
124 			annotationsFound |= processFieldLevelAnnotations(type, metadata);
125 			// check for method (getter) level annotations
126 			annotationsFound |= processMethodLevelAnnotations(type, metadata);
127 			// Look for inherited properties
128 			annotationsFound |= processInheritedAttributes(type, metadata);
129 			if (annotationsFound) {
130 				masterMetadataMap.put(type, metadata);
131 			}
132 		}
133 		LOG.info("Completed Scanning For Metadata Annotations");
134 		if (LOG.isDebugEnabled()) {
135 			LOG.debug("Annotation Metadata: " + masterMetadataMap);
136 		}
137 	}
138 
139 	/**
140 	 * Handle annotations made at the class level and add their data to the given metadata object.
141      *
142      * @param clazz the class to process.
143 	 * @param metadata the metadata for the class.
144 	 * @return <b>true</b> if any annotations are found.
145 	 */
146 	protected boolean processClassLevelAnnotations(Class<?> clazz, DataObjectMetadataImpl metadata) {
147 		boolean classAnnotationFound = false;
148 		boolean fieldAnnotationsFound = false;
149 		// get the class annotations
150 		List<DataObjectAttribute> attributes = new ArrayList<DataObjectAttribute>(metadata.getAttributes());
151 		Annotation[] classAnnotations = clazz.getAnnotations();
152 		if (LOG.isDebugEnabled()) {
153 			LOG.debug("Class-level annotations: " + Arrays.asList(classAnnotations));
154 		}
155 		for (Annotation a : classAnnotations) {
156 			// check if it's one we can handle
157 			// do something with it
158 			if (processAnnotationsforCommonMetadata(a, metadata)) {
159 				classAnnotationFound = true;
160 				continue;
161 			}
162 			if (a instanceof MergeAction) {
163 				MetadataMergeAction mma = ((MergeAction) a).value();
164 				if (!(mma == MetadataMergeAction.MERGE || mma == MetadataMergeAction.REMOVE)) {
165 					throw new MetadataConfigurationException(
166 							"Only the MERGE and REMOVE merge actions are supported since the annotation metadata provider can not specify all required properties and may only be used as an overlay.");
167 				}
168 				metadata.setMergeAction(mma);
169 				classAnnotationFound = true;
170 				continue;
171 			}
172 			if (a instanceof UifAutoCreateViews) {
173 				metadata.setAutoCreateUifViewTypes(Arrays.asList(((UifAutoCreateViews) a).value()));
174 				classAnnotationFound = true;
175 			}
176 		}
177 		if (fieldAnnotationsFound) {
178 			metadata.setAttributes(attributes);
179 		}
180 		return classAnnotationFound;
181 	}
182 
183 	/**
184 	 * Handle annotations made at the field level and add their data to the given metadata object.
185      *
186 	 * @param clazz the class to process.
187      * @param metadata the metadata for the class.
188 	 * @return <b>true</b> if any annotations are found.
189 	 */
190 	protected boolean processFieldLevelAnnotations(Class<?> clazz, DataObjectMetadataImpl metadata) {
191 		boolean fieldAnnotationsFound = false;
192 		boolean additionalClassAnnotationsFound = false;
193 		List<DataObjectAttribute> attributes = new ArrayList<DataObjectAttribute>();
194 		for (Field f : clazz.getDeclaredFields()) {
195 			boolean fieldAnnotationFound = false;
196 			String propertyName = f.getName();
197 			DataObjectAttributeImpl attr = (DataObjectAttributeImpl) metadata.getAttribute(propertyName);
198 			boolean existingAttribute = attr != null;
199 			if (!existingAttribute) {
200 				attr = new DataObjectAttributeImpl();
201 				attr.setName(propertyName);
202 				attr.setType(f.getType());
203 				DataType dataType = DataType.getDataTypeFromClass(f.getType());
204 				if (dataType == null) {
205 					dataType = DataType.STRING;
206 				}
207 				attr.setDataType(dataType);
208 				attr.setOwningType(metadata.getType());
209 			}
210 			Annotation[] fieldAnnotations = f.getDeclaredAnnotations();
211 			if (LOG.isDebugEnabled()) {
212 				LOG.debug(f.getDeclaringClass() + "." + f.getName() + " Field-level annotations: "
213 						+ Arrays.asList(fieldAnnotations));
214 			}
215 			for (Annotation a : fieldAnnotations) {
216 				// check if it's one we can handle then do something with it
217 				fieldAnnotationFound |= processAnnotationForAttribute(a, attr, metadata);
218 				if (!fieldAnnotationFound) {
219 					if (a instanceof BusinessKey) {
220 						ArrayList<String> businessKeys = new ArrayList<String>(metadata.getBusinessKeyAttributeNames());
221 						businessKeys.add(f.getName());
222 						metadata.setBusinessKeyAttributeNames(businessKeys);
223 						// We are not altering the field definition, so dont set the flag
224 						// fieldAnnotationFound = true;
225 						additionalClassAnnotationsFound = true;
226 						continue;
227 					}
228 					if (a instanceof Relationship) {
229 						addDataObjectRelationship(metadata, f, (Relationship) a);
230 
231 						additionalClassAnnotationsFound = true;
232 						continue;
233 					}
234 					if (a instanceof CollectionRelationship) {
235 						addDataObjectCollection(metadata, f, (CollectionRelationship) a);
236 
237 						additionalClassAnnotationsFound = true;
238 						continue;
239 					}
240 				}
241 			}
242 			if (fieldAnnotationFound) {
243 				attributes.add(attr);
244 				fieldAnnotationsFound = true;
245 			}
246 		}
247 		if (fieldAnnotationsFound) {
248 			metadata.setAttributes(attributes);
249 		}
250 		return fieldAnnotationsFound || additionalClassAnnotationsFound;
251 	}
252 
253 	/**
254 	 * Helper method to process the annotations which can be present on attributes or classes.
255      *
256      * @param a the annotation to process.
257      * @param metadata the metadata for the class.
258 	 * @return <b>true</b> if a valid annotation is found
259 	 */
260 	protected boolean processAnnotationsforCommonMetadata(Annotation a, MetadataCommonBase metadata) {
261 		if (a instanceof Label) {
262 			if (StringUtils.isNotBlank(((Label) a).value())) {
263 				metadata.setLabel(((Label) a).value());
264 				return true;
265 			}
266 		}
267 		if (a instanceof ShortLabel) {
268 			metadata.setShortLabel(((ShortLabel) a).value());
269 			return true;
270 		}
271 		if (a instanceof Description) {
272 			metadata.setDescription(((Description) a).value());
273 			return true;
274 		}
275 		return false;
276 	}
277 
278 	/**
279 	 * Helper method to process the annotations which can be present on attributes.
280      *
281      * <p>Used to abstract the logic so it can be applied to both field and method-level annotations.</p>
282      *
283      * @param a the annotation to process.
284      * @param attr the attribute for the field.
285      * @param metadata the metadata for the class.
286 	 * 
287 	 * @return true if any annotations were processed, false if not
288 	 */
289 	protected boolean processAnnotationForAttribute(Annotation a, DataObjectAttributeImpl attr,
290 			DataObjectMetadataImpl metadata) {
291 		if (a == null) {
292 			return false;
293 		}
294 		if (a instanceof NonPersistentProperty) {
295 			attr.setPersisted(false);
296 			return true;
297 		}
298 		if (processAnnotationsforCommonMetadata(a, attr)) {
299 			return true;
300 		}
301 		if (a instanceof ReadOnly) {
302 			attr.setReadOnly(true);
303 			return true;
304 		}
305 		if (a instanceof UifValidCharactersConstraintBeanName) {
306 			attr.setValidCharactersConstraintBeanName(((UifValidCharactersConstraintBeanName) a).value());
307 			return true;
308 		}
309 		if (a instanceof KeyValuesFinderClass) {
310 			try {
311 				attr.setValidValues(((KeyValuesFinderClass) a).value().newInstance());
312 				return true;
313 			} catch (Exception ex) {
314 				LOG.error("Unable to instantiate options finder: " + ((KeyValuesFinderClass) a).value(), ex);
315 			}
316 		}
317 		if (a instanceof NotNull) {
318 			attr.setRequired(true);
319 			return true;
320 		}
321 		if (a instanceof ForceUppercase) {
322 			attr.setForceUppercase(true);
323 			return true;
324 		}
325 		if (a instanceof PropertyEditorClass) {
326 			try {
327 				attr.setPropertyEditor(((PropertyEditorClass) a).value().newInstance());
328 				return true;
329 			} catch (Exception ex) {
330 				LOG.warn("Unable to instantiate property editor class for " + metadata.getTypeClassName()
331 						+ "." + attr.getName() + " : " + ((PropertyEditorClass) a).value());
332 			}
333 		}
334 		if (a instanceof Size) {
335 			// We only process it at the moment if the max length has been set
336 			// Otherwise, we want the JPA value (max column length) to pass through
337 			if (((Size) a).max() != Integer.MAX_VALUE) {
338 				attr.setMaxLength((long) ((Size) a).max());
339 				return true;
340 			}
341 		}
342 		if (a instanceof Sensitive) {
343 			attr.setSensitive(true);
344 			return true;
345 		}
346 		if (a instanceof UifDisplayHints) {
347 			attr.setDisplayHints(new HashSet<UifDisplayHint>(Arrays.asList(((UifDisplayHints) a).value())));
348 			return true;
349 		}
350 		if (a instanceof MergeAction) {
351 			MetadataMergeAction mma = ((MergeAction) a).value();
352 			if (!(mma == MetadataMergeAction.MERGE || mma == MetadataMergeAction.REMOVE)) {
353 				throw new MetadataConfigurationException(
354 						"Only the MERGE and REMOVE merge actions are supported since the annotation metadata provider can not specify all required properties and may only be used as an overlay.");
355 			}
356 			attr.setMergeAction(mma);
357 			return true;
358 		}
359 		return false;
360 	}
361 
362 	/**
363 	 * Used to find the property name from a getter method.
364 	 * 
365 	 * <p>(Not using PropertyUtils since it required an instance of the class.)</p>
366      *
367      * @param m the method from which to get the property name.
368      * @return the property name.
369 	 */
370 	protected String getPropertyNameFromGetterMethod(Method m) {
371 		String propertyName = "";
372 		if (m.getName().startsWith("get")) {
373 			propertyName = StringUtils.uncapitalize(StringUtils.removeStart(m.getName(), "get"));
374 		} else { // must be "is"
375 			propertyName = StringUtils.uncapitalize(StringUtils.removeStart(m.getName(), "is"));
376 		}
377 		return propertyName;
378 	}
379 
380 	/**
381 	 * Handle annotations made at the method level and add their data to the given metadata object.
382      *
383      * @param clazz the class to process.
384      * @param metadata the metadata for the class.
385 	 * 
386 	 * @return <b>true</b> if any annotations are found.
387 	 */
388 	protected boolean processMethodLevelAnnotations(Class<?> clazz, DataObjectMetadataImpl metadata) {
389 		boolean fieldAnnotationsFound = false;
390 		if (LOG.isDebugEnabled()) {
391 			LOG.debug("Processing Method Annotations on " + clazz);
392 		}
393 		List<DataObjectAttribute> attributes = new ArrayList<DataObjectAttribute>(metadata.getAttributes());
394 		for (Method m : clazz.getDeclaredMethods()) {
395 			// we only care about properties which are designated as non-persistent
396 			// we don't want to load metadata about everything just because it's there
397 			// (E.g., we don't know how expensive all method calls are)
398 			if (!m.isAnnotationPresent(NonPersistentProperty.class)) {
399 				if (LOG.isTraceEnabled()) {
400 					LOG.trace("Rejecting method " + m.getName()
401 							+ " because does not have NonPersistentProperty annotation");
402 				}
403 				continue;
404 			}
405 			// we only care about getters
406 			if (!m.getName().startsWith("get") && !m.getName().startsWith("is")) {
407 				if (LOG.isDebugEnabled()) {
408 					LOG.debug("Rejecting method " + m.getName() + " because name does not match getter pattern");
409 				}
410 				continue;
411 			}
412 			// we also need it to return a value and have no arguments to be a proper getter
413 			if (m.getReturnType() == null || m.getParameterTypes().length > 0) {
414 				if (LOG.isDebugEnabled()) {
415 					LOG.debug("Rejecting method " + m.getName() + " because has no return type or has arguments");
416 				}
417 				continue;
418 			}
419 			String propertyName = getPropertyNameFromGetterMethod(m);
420 			boolean fieldAnnotationFound = false;
421 			boolean existingAttribute = true;
422 			DataObjectAttributeImpl attr = (DataObjectAttributeImpl) metadata.getAttribute(propertyName);
423 			if (attr == null) {
424 				existingAttribute = false;
425 				attr = new DataObjectAttributeImpl();
426 				attr.setName(propertyName);
427 				attr.setType(m.getReturnType());
428 				DataType dataType = DataType.getDataTypeFromClass(m.getReturnType());
429 				if (dataType == null) {
430 					dataType = DataType.STRING;
431 				}
432 				attr.setDataType(dataType);
433 				attr.setOwningType(metadata.getType());
434 			}
435 			Annotation[] methodAnnotations = m.getDeclaredAnnotations();
436 			if (LOG.isDebugEnabled()) {
437 				LOG.debug(m.getDeclaringClass() + "." + m.getName() + " Method-level annotations: "
438 						+ Arrays.asList(methodAnnotations));
439 			}
440 			for (Annotation a : methodAnnotations) {
441 				fieldAnnotationFound |= processAnnotationForAttribute(a, attr, metadata);
442 			}
443 			if (fieldAnnotationFound) {
444 				if (!existingAttribute) {
445 					attributes.add(attr);
446 				}
447 				fieldAnnotationsFound = true;
448 			}
449 		}
450 		if (fieldAnnotationsFound) {
451 			metadata.setAttributes(attributes);
452 		}
453 
454 		return fieldAnnotationsFound;
455 	}
456 
457     /**
458      * Adds a relationship for a field to the metadata object.
459      *
460      * @param metadata the metadata for the class.
461      * @param f the field to process.
462      * @param a the relationship to add.
463      */
464 	protected void addDataObjectRelationship(DataObjectMetadataImpl metadata, Field f, Relationship a) {
465 		List<DataObjectRelationship> relationships = new ArrayList<DataObjectRelationship>(metadata.getRelationships());
466 		DataObjectRelationshipImpl relationship = new DataObjectRelationshipImpl();
467 		relationship.setName(f.getName());
468 		Class<?> childType = f.getType();
469 		relationship.setRelatedType(childType);
470 		relationship.setReadOnly(true);
471 		relationship.setSavedWithParent(false);
472 		relationship.setDeletedWithParent(false);
473 		relationship.setLoadedAtParentLoadTime(false);
474 		relationship.setLoadedDynamicallyUponUse(true);
475 
476 		List<DataObjectAttributeRelationship> attributeRelationships = new ArrayList<DataObjectAttributeRelationship>();
477 		List<String> referencePkFields = Collections.emptyList();
478         MetadataRepository metadataRepository = getDataObjectService().getMetadataRepository();
479 		if (metadataRepository.contains(childType)) {
480             DataObjectMetadata childMetadata = metadataRepository.getMetadata(childType);
481 			referencePkFields = childMetadata.getPrimaryKeyAttributeNames();
482 		} else {
483 			// HACK ALERT!!!!!!!! FIXME: can be removed once Person is annotated for JPA
484 			if (f.getType().getName().equals("org.kuali.rice.kim.api.identity.Person")) {
485 				referencePkFields = Collections.singletonList("principalId");
486 			}
487 		}
488 		int index = 0;
489 		for (String pkField : a.foreignKeyFields()) {
490 			attributeRelationships.add(new DataObjectAttributeRelationshipImpl(pkField, referencePkFields.get(index)));
491 			index++;
492 		}
493 		relationship.setAttributeRelationships(attributeRelationships);
494 
495 		relationships.add(relationship);
496 		metadata.setRelationships(relationships);
497 	}
498 
499     /**
500      * Adds a collection relationship for a field to the metadata object.
501      *
502      * @param metadata the metadata for the class.
503      * @param f the field to process.
504      * @param a the collection relationship to add.
505      */
506 	protected void addDataObjectCollection(DataObjectMetadataImpl metadata, Field f, CollectionRelationship a) {
507 		List<DataObjectCollection> collections = new ArrayList<DataObjectCollection>(metadata.getCollections());
508 		DataObjectCollectionImpl collection = new DataObjectCollectionImpl();
509 		collection.setName(f.getName());
510 		
511 		if ( !Collection.class.isAssignableFrom(f.getType()) ) {
512 			throw new IllegalArgumentException(
513 					"@CollectionRelationship annotations can only be on attributes of Collection type.  Field: "
514 							+ f.getDeclaringClass().getName() + "." + f.getName() + " (" + f.getType() + ")");
515 		}
516 		
517 		if (a.collectionElementClass().equals(Object.class)) { // Object is the default (and meaningless anyway)
518 			Type[] genericArgs = ((ParameterizedType) f.getGenericType()).getActualTypeArguments();
519 			if (genericArgs.length == 0) {
520 				throw new IllegalArgumentException(
521 						"You can only leave off the collectionElementClass annotation on a @CollectionRelationship when the Collection type has been <typed>.  Field: "
522 								+ f.getDeclaringClass().getName() + "." + f.getName() + " (" + f.getType() + ")");
523 			}
524 			collection.setRelatedType((Class<?>) genericArgs[0]);
525 		} else {
526 			collection.setRelatedType(a.collectionElementClass());
527 		}
528 		
529 		List<DataObjectAttributeRelationship> attributeRelationships = new ArrayList<DataObjectAttributeRelationship>(
530 				a.attributeRelationships().length);
531 		for (AttributeRelationship rel : a.attributeRelationships()) {
532 			attributeRelationships.add(new DataObjectAttributeRelationshipImpl(rel.parentAttributeName(), rel
533 					.childAttributeName()));
534 		}
535 		collection.setAttributeRelationships(attributeRelationships);
536 
537 		collection.setReadOnly(false);
538 		collection.setSavedWithParent(false);
539 		collection.setDeletedWithParent(false);
540 		collection.setLoadedAtParentLoadTime(true);
541 		collection.setLoadedDynamicallyUponUse(false);
542 		List<DataObjectCollectionSortAttribute> sortAttributes = new ArrayList<DataObjectCollectionSortAttribute>(
543 				a.sortAttributes().length);
544 		for (CollectionSortAttribute csa : a.sortAttributes()) {
545 			sortAttributes.add(new DataObjectCollectionSortAttributeImpl(csa.value(), csa.sortDirection()));
546 		}
547 		collection.setDefaultCollectionOrderingAttributeNames(sortAttributes);
548 
549 		collection.setIndirectCollection(a.indirectCollection());
550 		collection.setMinItemsInCollection(a.minItemsInCollection());
551 		collection.setMaxItemsInCollection(a.maxItemsInCollection());
552 		if (StringUtils.isNotBlank(a.label())) {
553 			collection.setLabel(a.label());
554 		}
555 		if (StringUtils.isNotBlank(a.elementLabel())) {
556 			collection.setLabel(a.elementLabel());
557 		}
558 
559 		collections.add(collection);
560 		metadata.setCollections(collections);
561 	}
562 
563     /**
564      * Handle inherited properties and add their data to the given metadata object.
565      *
566      * @param clazz the class to process.
567      * @param metadata the metadata for the class.
568      *
569      * @return <b>true</b> if any annotations are found.
570      */
571 	protected boolean processInheritedAttributes(Class<?> clazz, DataObjectMetadataImpl metadata) {
572 		if (LOG.isDebugEnabled()) {
573 			LOG.debug("Processing InheritProperties field Annotations on " + clazz);
574 		}
575 		List<DataObjectAttribute> attributes = new ArrayList<DataObjectAttribute>(metadata.getAttributes());
576 		boolean fieldAnnotationsFound = false;
577 		for (Field f : clazz.getDeclaredFields()) {
578 			boolean fieldAnnotationFound = false;
579 			String propertyName = f.getName();
580 
581 			if (!f.isAnnotationPresent(InheritProperties.class) && !f.isAnnotationPresent(InheritProperty.class)) {
582 				continue;
583 			}
584 			fieldAnnotationFound = true;
585 			// Get the list of inherited properties, either from a single annotation or the "plural" version
586 			InheritProperty[] propertyList = null;
587 			InheritProperties a = f.getAnnotation(InheritProperties.class);
588 			if (a != null) {
589 				propertyList = a.value();
590 			} else {
591 				// if the above is not present, then there must be an @InheritProperty annotation
592 				InheritProperty ip = f.getAnnotation(InheritProperty.class);
593 				propertyList = new InheritProperty[] { ip };
594 			}
595 			if (LOG.isDebugEnabled()) {
596 				LOG.debug("InheritProperties found on " + clazz + "." + f.getName() + " : "
597 						+ Arrays.toString(propertyList));
598 			}
599 			for (InheritProperty inheritedProperty : propertyList) {
600 				String inheritedPropertyName = inheritedProperty.name();
601 				String extendedPropertyName = propertyName + "." + inheritedPropertyName;
602 				DataObjectAttributeImpl attr = (DataObjectAttributeImpl) metadata.getAttribute(extendedPropertyName);
603 				boolean existingAttribute = attr != null;
604 				if (!existingAttribute) {
605 					// NOTE: dropping to reflection here as the related metadata may not be loaded yet...
606 					// TODO: this may need to be reworked to allow for "real-time" inheritance
607 					// since the values seen here should reflect overrides performed later in the chain
608 					// (e.g., by the MessageServiceMetadataProvider)
609 					attr = new DataObjectAttributeImpl();
610 					attr.setName(extendedPropertyName);
611 					Class<?> relatedClass = f.getType();
612 					try {
613 						attr.setType(getTypeOfProperty(relatedClass, inheritedPropertyName));
614 						DataType dataType = DataType.getDataTypeFromClass(attr.getType());
615 						if (dataType == null) {
616 							dataType = DataType.STRING;
617 						}
618 						attr.setDataType(dataType);
619 					} catch (Exception e) {
620 						throw new IllegalArgumentException("no field with name " + inheritedPropertyName
621 								+ " exists on " + relatedClass, e);
622 					}
623 					// Since this attribute is really part of another object, we want to indicate that it's not
624 					// persistent (as far as this object is concerned)
625 					attr.setPersisted(false);
626 					attr.setOwningType(metadata.getType());
627 					attr.setInheritedFromType(relatedClass);
628 					attr.setInheritedFromAttributeName(inheritedPropertyName);
629 					attr.setInheritedFromParentAttributeName(propertyName);
630 
631 					// Handle the label override, if present
632 					processAnnotationForAttribute(inheritedProperty.label(), attr, metadata);
633 					// Handle the UIF displayoverride, if present
634 					processAnnotationForAttribute(inheritedProperty.displayHints(), attr, metadata);
635 
636 					attributes.add(attr);
637 				}
638 			}
639 
640 			fieldAnnotationsFound |= fieldAnnotationFound;
641 		}
642 		if (fieldAnnotationsFound) {
643 			metadata.setAttributes(attributes);
644 		}
645 		return fieldAnnotationsFound;
646 	}
647 
648 	/**
649 	 * Used to find the property type of a given attribute regardless of whether the attribute exists as a field or only
650 	 * as a getter method.
651 	 * 
652 	 * <p>(Not using PropertyUtils since it required an instance of the class.)</p>
653      *
654      * @param clazz the class that contains the property.
655      * @param propertyName the name of the property.
656      * @return the type of the property.
657 	 */
658 	protected Class<?> getTypeOfProperty(Class<?> clazz, String propertyName) {
659 		try {
660 			Field f = clazz.getField(propertyName);
661 			return f.getType();
662 		} catch (Exception e) {
663 			// Do nothing = field does not exist
664 		}
665 		try {
666 			Method m = clazz.getMethod("get" + StringUtils.capitalize(propertyName));
667 			return m.getReturnType();
668 		} catch (Exception e) {
669 			// Do nothing = method does not exist
670 		}
671 		try {
672 			Method m = clazz.getMethod("is" + StringUtils.capitalize(propertyName));
673 			return m.getReturnType();
674 		} catch (Exception e) {
675 			// Do nothing = method does not exist
676 		}
677 		return null;
678 	}
679 
680 	/**
681 	 * {@inheritDoc}
682      *
683      * Returns true in this implementation. This tells the composite metadata provider to pass in all known metadata to
684 	 * the initializeMetadata method.
685 	 */
686 	@Override
687 	public boolean requiresListOfExistingTypes() {
688 		return true;
689 	}
690 
691     /**
692      * Gets whether initialization was attempted.
693      *
694      * @return whether initialization was attempted.
695      */
696     public boolean isInitializationAttempted() {
697         return initializationAttempted;
698     }
699 
700     /**
701      * Gets the {@link DataObjectService}.
702      * @return the {@link DataObjectService}.
703      */
704     public DataObjectService getDataObjectService() {
705         if (dataObjectService == null) {
706             dataObjectService = KradDataServiceLocator.getDataObjectService();
707         }
708         return dataObjectService;
709     }
710 
711     /**
712      * Setter for the the {@link DataObjectService}.
713      *
714      * @param dataObjectService the the {@link DataObjectService} to set.
715      */
716     public void setDataObjectService(DataObjectService dataObjectService) {
717         this.dataObjectService = dataObjectService;
718     }
719 }