001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.data.provider.annotation.impl;
017
018import java.lang.annotation.Annotation;
019import java.lang.reflect.Field;
020import java.lang.reflect.Method;
021import java.lang.reflect.ParameterizedType;
022import java.lang.reflect.Type;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029
030import javax.validation.constraints.NotNull;
031import javax.validation.constraints.Size;
032
033import org.apache.commons.lang.StringUtils;
034import org.kuali.rice.core.api.data.DataType;
035import org.kuali.rice.krad.data.DataObjectService;
036import org.kuali.rice.krad.data.KradDataServiceLocator;
037import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
038import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
039import org.kuali.rice.krad.data.metadata.DataObjectCollection;
040import org.kuali.rice.krad.data.metadata.DataObjectCollectionSortAttribute;
041import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
042import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
043import org.kuali.rice.krad.data.metadata.MetadataConfigurationException;
044import org.kuali.rice.krad.data.metadata.MetadataMergeAction;
045import org.kuali.rice.krad.data.metadata.MetadataRepository;
046import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeImpl;
047import org.kuali.rice.krad.data.metadata.impl.DataObjectAttributeRelationshipImpl;
048import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionImpl;
049import org.kuali.rice.krad.data.metadata.impl.DataObjectCollectionSortAttributeImpl;
050import org.kuali.rice.krad.data.metadata.impl.DataObjectMetadataImpl;
051import org.kuali.rice.krad.data.metadata.impl.DataObjectRelationshipImpl;
052import org.kuali.rice.krad.data.metadata.impl.MetadataCommonBase;
053import org.kuali.rice.krad.data.provider.annotation.AttributeRelationship;
054import org.kuali.rice.krad.data.provider.annotation.BusinessKey;
055import org.kuali.rice.krad.data.provider.annotation.CollectionRelationship;
056import org.kuali.rice.krad.data.provider.annotation.CollectionSortAttribute;
057import org.kuali.rice.krad.data.provider.annotation.Description;
058import org.kuali.rice.krad.data.provider.annotation.ForceUppercase;
059import org.kuali.rice.krad.data.provider.annotation.InheritProperties;
060import org.kuali.rice.krad.data.provider.annotation.InheritProperty;
061import org.kuali.rice.krad.data.provider.annotation.KeyValuesFinderClass;
062import org.kuali.rice.krad.data.provider.annotation.Label;
063import org.kuali.rice.krad.data.provider.annotation.MergeAction;
064import org.kuali.rice.krad.data.provider.annotation.NonPersistentProperty;
065import org.kuali.rice.krad.data.provider.annotation.PropertyEditorClass;
066import org.kuali.rice.krad.data.provider.annotation.ReadOnly;
067import org.kuali.rice.krad.data.provider.annotation.Relationship;
068import org.kuali.rice.krad.data.provider.annotation.Sensitive;
069import org.kuali.rice.krad.data.provider.annotation.ShortLabel;
070import org.kuali.rice.krad.data.provider.annotation.UifAutoCreateViews;
071import org.kuali.rice.krad.data.provider.annotation.UifDisplayHint;
072import org.kuali.rice.krad.data.provider.annotation.UifDisplayHints;
073import org.kuali.rice.krad.data.provider.annotation.UifValidCharactersConstraintBeanName;
074import org.kuali.rice.krad.data.provider.impl.MetadataProviderBase;
075
076/**
077 * Parses custom krad-data annotations for additional metadata to layer on top of that provided by the persistence
078 * metadata provider which should have run before this one.
079 *
080 * <p>
081 * At the moment, it will only process classes which were previously identified by the JPA implementation.
082 * </p>
083 *
084 * <p>
085 * TODO: Addition of a new Annotation which will need to be scanned for in order to process non-persistent classes as data objects.
086 * </p>
087 * 
088 * @author Kuali Rice Team (rice.collab@kuali.org)
089 */
090public class AnnotationMetadataProviderImpl extends MetadataProviderBase {
091        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
092                        .getLogger(AnnotationMetadataProviderImpl.class);
093
094        private boolean initializationAttempted = false;
095    private DataObjectService dataObjectService;
096
097    /**
098     * {@inheritDoc}
099     */
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}