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