001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.data.provider.annotation.impl;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.data.DataType;
020    import org.kuali.rice.krad.data.DataObjectService;
021    import org.kuali.rice.krad.data.KradDataServiceLocator;
022    import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
023    import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
024    import org.kuali.rice.krad.data.metadata.DataObjectCollection;
025    import org.kuali.rice.krad.data.metadata.DataObjectCollectionSortAttribute;
026    import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
027    import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
028    import org.kuali.rice.krad.data.metadata.MetadataConfigurationException;
029    import org.kuali.rice.krad.data.metadata.MetadataMergeAction;
030    import org.kuali.rice.krad.data.metadata.MetadataRepository;
031    import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeImpl;
032    import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeRelationshipImpl;
033    import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionImpl;
034    import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionSortAttributeImpl;
035    import org.kuali.rice.krad.data.metadata.impl.DataObjectMetadataImpl;
036    import org.kuali.rice.krad.data.metadata.impl.DataObjectRelationshipImpl;
037    import org.kuali.rice.krad.data.metadata.impl.MetadataCommonBase;
038    import org.kuali.rice.krad.data.provider.annotation.AttributeRelationship;
039    import org.kuali.rice.krad.data.provider.annotation.BusinessKey;
040    import org.kuali.rice.krad.data.provider.annotation.CollectionRelationship;
041    import org.kuali.rice.krad.data.provider.annotation.CollectionSortAttribute;
042    import org.kuali.rice.krad.data.provider.annotation.Description;
043    import org.kuali.rice.krad.data.provider.annotation.ForceUppercase;
044    import org.kuali.rice.krad.data.provider.annotation.InheritProperties;
045    import org.kuali.rice.krad.data.provider.annotation.InheritProperty;
046    import org.kuali.rice.krad.data.provider.annotation.KeyValuesFinderClass;
047    import org.kuali.rice.krad.data.provider.annotation.Label;
048    import org.kuali.rice.krad.data.provider.annotation.MergeAction;
049    import org.kuali.rice.krad.data.provider.annotation.NonPersistentProperty;
050    import org.kuali.rice.krad.data.provider.annotation.PropertyEditorClass;
051    import org.kuali.rice.krad.data.provider.annotation.ReadOnly;
052    import org.kuali.rice.krad.data.provider.annotation.Relationship;
053    import org.kuali.rice.krad.data.provider.annotation.Sensitive;
054    import org.kuali.rice.krad.data.provider.annotation.ShortLabel;
055    import org.kuali.rice.krad.data.provider.annotation.UifAutoCreateViews;
056    import org.kuali.rice.krad.data.provider.annotation.UifDisplayHint;
057    import org.kuali.rice.krad.data.provider.annotation.UifDisplayHints;
058    import org.kuali.rice.krad.data.provider.annotation.UifValidCharactersConstraintBeanName;
059    import org.kuali.rice.krad.data.provider.impl.MetadataProviderBase;
060    
061    import javax.validation.constraints.NotNull;
062    import javax.validation.constraints.Size;
063    import java.lang.annotation.Annotation;
064    import java.lang.reflect.Field;
065    import java.lang.reflect.Method;
066    import java.lang.reflect.ParameterizedType;
067    import java.lang.reflect.Type;
068    import java.util.ArrayList;
069    import java.util.Arrays;
070    import java.util.Collection;
071    import java.util.Collections;
072    import java.util.HashSet;
073    import java.util.List;
074    
075    /**
076     * Parses custom krad-data annotations for additional metadata to layer on top of that provided by the persistence
077     * metadata provider which should have run before this one.
078     *
079     * <p>
080     * At the moment, it will only process classes which were previously identified by the JPA implementation.
081     * </p>
082     *
083     * <p>
084     * TODO: Addition of a new Annotation which will need to be scanned for in order to process non-persistent classes as data objects.
085     * </p>
086     * 
087     * @author Kuali Rice Team (rice.collab@kuali.org)
088     */
089    public class AnnotationMetadataProviderImpl extends MetadataProviderBase {
090            private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
091                            .getLogger(AnnotationMetadataProviderImpl.class);
092    
093            private boolean initializationAttempted = false;
094        private DataObjectService dataObjectService;
095    
096        /**
097         * {@inheritDoc}
098         */
099            @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    }