001/**
002 * Copyright 2005-2018 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.datadictionary.parse;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.util.type.TypeUtils;
023import org.kuali.rice.krad.util.KRADConstants;
024import org.springframework.context.ApplicationContext;
025import org.springframework.context.support.ClassPathXmlApplicationContext;
026import org.springframework.core.io.Resource;
027import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
028import org.springframework.core.io.support.ResourcePatternResolver;
029import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
030import org.springframework.core.type.classreading.MetadataReader;
031import org.springframework.core.type.classreading.MetadataReaderFactory;
032import org.springframework.util.ClassUtils;
033import org.springframework.util.SystemPropertyUtils;
034import org.w3c.dom.Document;
035import org.w3c.dom.NodeList;
036
037import javax.xml.parsers.DocumentBuilder;
038import javax.xml.parsers.DocumentBuilderFactory;
039import java.io.File;
040import java.io.IOException;
041import java.io.InputStream;
042import java.lang.reflect.Method;
043import java.lang.reflect.ParameterizedType;
044import java.lang.reflect.Type;
045import java.util.ArrayList;
046import java.util.HashMap;
047import java.util.HashSet;
048import java.util.List;
049import java.util.Map;
050import java.util.Set;
051
052/**
053 * Creates and stores the information defined for the custom schema.  Loads the classes defined as having associated
054 * custom schemas and creates the information for the schema by parsing there annotations.
055 *
056 * @author Kuali Rice Team (rice.collab@kuali.org)
057 */
058public class CustomTagAnnotations {
059    private static final Log LOG = LogFactory.getLog(CustomTagAnnotations.class);
060
061    private static Map<String, Map<String, BeanTagAttributeInfo>> attributeProperties;
062    private static Map<String, BeanTagInfo> beanTags;
063    private static Set<Class<?>> customTagClasses;
064
065    private static Map<Class<?>, Set<String>> beanTagsByClass;
066
067    private CustomTagAnnotations() {}
068
069    /**
070     * Loads component classes for the custom schema by scanning configured packages.
071     *
072     * <p>Packages to scan are configured using org.kuali.rice.krad.util.KRADConstants.ConfigParameters#SCHEMA_PACKAGES</p>
073     */
074    public static void loadTagClasses() {
075        String scanPackagesStr = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
076                KRADConstants.ConfigParameters.SCHEMA_PACKAGES);
077
078        String[] scanPackages = StringUtils.split(scanPackagesStr, ",");
079
080        loadTagClasses(scanPackages);
081    }
082
083    /**
084     * Loads component classes for the custom schema by scanning the given packages.
085     *
086     * @param scanPackages array of packages to scan
087     */
088    public static void loadTagClasses(String[] scanPackages) {
089        customTagClasses = new HashSet<Class<?>>();
090
091        for (String scanPackage : scanPackages) {
092            try {
093                customTagClasses.addAll(findTagClasses(StringUtils.trim(scanPackage)));
094            } catch (Exception e) {
095                throw new RuntimeException("unable to scan package: " + scanPackage, e);
096            }
097        }
098    }
099
100    /**
101     * Finds all the classes which have a BeanTag or BeanTags annotation
102     *
103     * @param basePackage the package to start in
104     * @return classes which have BeanTag or BeanTags annotation
105     * @throws IOException
106     * @throws ClassNotFoundException
107     */
108    protected static List<Class<?>> findTagClasses(String basePackage) throws IOException, ClassNotFoundException {
109        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
110        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
111
112        List<Class<?>> classes = new ArrayList<Class<?>>();
113
114        String resolvedBasePackage = ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(
115                basePackage));
116        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
117                resolvedBasePackage + "/" + "**/*.class";
118
119        Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
120        for (Resource resource : resources) {
121            if (resource.isReadable()) {
122                MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
123                if (metadataReader != null && isBeanTag(metadataReader)) {
124                    classes.add(Class.forName(metadataReader.getClassMetadata().getClassName()));
125                }
126            }
127        }
128
129        return classes;
130    }
131
132    /**
133     * Returns true if the metadataReader representing the class has a BeanTag or BeanTags annotation
134     *
135     * @param metadataReader MetadataReader representing the class to analyze
136     * @return true if BeanTag or BeanTags annotation is present
137     */
138    protected static boolean isBeanTag(MetadataReader metadataReader) {
139        try {
140            Class<?> c = Class.forName(metadataReader.getClassMetadata().getClassName());
141            if (c.getAnnotation(BeanTag.class) != null || c.getAnnotation(BeanTags.class) != null) {
142                return true;
143            }
144        } catch (Exception e) {
145            throw new RuntimeException(e);
146        }
147
148        return false;
149    }
150
151    /**
152     * Load the attribute information of the properties in the class repersented by the new tag.
153     *
154     * @param doc the RescourceBundle containing the documentation
155     */
156    protected static void loadAttributeProperties(String tagName, Class<?> tagClass) {
157        Map<String, BeanTagAttributeInfo> entries = new HashMap<String, BeanTagAttributeInfo>();
158
159        entries.putAll(getAttributes(tagClass));
160
161        attributeProperties.put(tagName, entries);
162    }
163
164    /**
165     * Creates a map of entries the properties marked by the annotation of the bean class.
166     *
167     * @param tagClass class being mapped for attributes
168     * @return Return a map of the properties found in the class with there associated information.
169     */
170    public static Map<String, BeanTagAttributeInfo> getAttributes(Class<?> tagClass) {
171        Map<String, BeanTagAttributeInfo> entries = new HashMap<String, BeanTagAttributeInfo>();
172
173        // search the methods of the class using reflection for the attribute annotation
174        Method methods[] = tagClass.getMethods();
175        for (Method attributeMethod : methods) {
176            BeanTagAttribute attribute = attributeMethod.getAnnotation(BeanTagAttribute.class);
177            if (attribute == null) {
178                continue;
179            }
180
181            BeanTagAttributeInfo info = new BeanTagAttributeInfo();
182
183            String propertyName = getFieldName(attributeMethod.getName());
184            info.setPropertyName(propertyName);
185
186            if (StringUtils.isBlank(attribute.name())) {
187                info.setName(propertyName);
188            } else {
189                info.setName(attribute.name());
190            }
191
192            if (BeanTagAttribute.AttributeType.NOTSET.equals(attribute.type())) {
193                BeanTagAttribute.AttributeType derivedType = deriveTypeFromMethod(attributeMethod);
194
195                if (derivedType != null) {
196                    info.setType(derivedType);
197                }
198            } else {
199                info.setType(attribute.type());
200            }
201
202            info.setValueType(attributeMethod.getReturnType());
203            info.setGenericType(attributeMethod.getGenericReturnType());
204
205            validateBeanAttributes(tagClass.getName(), attribute.name(), entries);
206
207            entries.put(info.getName(), info);
208        }
209
210        return entries;
211    }
212
213    protected static BeanTagAttribute.AttributeType deriveTypeFromMethod(Method attributeMethod) {
214        BeanTagAttribute.AttributeType type = null;
215
216        Class<?> returnType = attributeMethod.getReturnType();
217        Type genericReturnType = attributeMethod.getGenericReturnType();
218
219        if (TypeUtils.isSimpleType(returnType) || TypeUtils.isClassClass(returnType) || Enum.class.isAssignableFrom(
220                returnType)) {
221            type = BeanTagAttribute.AttributeType.SINGLEVALUE;
222        } else if (returnType.isArray() || List.class.isAssignableFrom(returnType)) {
223            type = BeanTagAttribute.AttributeType.LISTBEAN;
224            if (genericReturnType instanceof ParameterizedType
225                    && ((ParameterizedType) genericReturnType).getActualTypeArguments().length == 1) {
226                Type genericParm = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
227
228                if (isValueType(genericParm)) {
229                    type = BeanTagAttribute.AttributeType.LISTVALUE;
230                }
231            }
232        } else if (Set.class.isAssignableFrom(returnType)) {
233            type = BeanTagAttribute.AttributeType.SETBEAN;
234            if (genericReturnType instanceof ParameterizedType
235                    && ((ParameterizedType) genericReturnType).getActualTypeArguments().length == 1) {
236                Type genericParm = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
237
238                if (isValueType(genericParm)) {
239                    type = BeanTagAttribute.AttributeType.SETVALUE;
240                }
241            }
242        } else if (Map.class.isAssignableFrom(returnType)) {
243            type = BeanTagAttribute.AttributeType.MAPBEAN;
244            if (genericReturnType instanceof ParameterizedType
245                    && ((ParameterizedType) genericReturnType).getActualTypeArguments().length == 2) {
246                Type genericParmKey = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
247                Type genericParmValue = ((ParameterizedType) genericReturnType).getActualTypeArguments()[1];
248
249                if (isValueType(genericParmKey) && isValueType(genericParmValue)) {
250                    type = BeanTagAttribute.AttributeType.MAPVALUE;
251                }
252            }
253        } else {
254            type = BeanTagAttribute.AttributeType.SINGLEBEAN;
255        }
256
257        return type;
258    }
259
260    protected static boolean isValueType(Type type) {
261        if (type instanceof Class<?>) {
262            Class<?> typeClass = (Class<?>) type;
263
264            if (TypeUtils.isSimpleType(typeClass) || Object.class.equals(typeClass)) {
265                return true;
266            }
267        }
268
269        return false;
270    }
271
272    protected static boolean isBeanType(Type type) {
273        if (type instanceof Class<?>) {
274            Class<?> typeClass = (Class<?>) type;
275
276            if (!TypeUtils.isSimpleType(typeClass)) {
277                return true;
278            }
279        }
280
281        return false;
282    }
283
284    /**
285     * Load the information for the xml bean tags defined in the custom schema through annotation of the represented
286     * classes.
287     */
288    protected static void loadBeanTags() {
289        // Load the list of class to be searched for annotation definitions
290        if (customTagClasses == null) {
291            loadTagClasses();
292        }
293
294        beanTags = new HashMap<String, BeanTagInfo>();
295        attributeProperties = new HashMap<String, Map<String, BeanTagAttributeInfo>>();
296        beanTagsByClass = new HashMap<Class<?>, Set<String>>();
297
298        // for each class create the bean tag information and its associated attribute properties
299        for (Class<?> tagClass : customTagClasses) {
300            BeanTag[] annotations = new BeanTag[1];
301
302            BeanTag tag = tagClass.getAnnotation(BeanTag.class);
303            if (tag != null) {
304                //single tag case
305                annotations[0] = tag;
306            } else {
307                //multi-tag case
308                BeanTags tags = tagClass.getAnnotation(BeanTags.class);
309
310                if (tags != null) {
311                    annotations = tags.value();
312                } else {
313                    //TODO throw exception instead?
314                    continue;
315                }
316            }
317
318            Set<String> classBeanTags = new HashSet<String>();
319            for (int j = 0; j < annotations.length; j++) {
320                BeanTag annotation = annotations[j];
321
322                BeanTagInfo info = new BeanTagInfo();
323                info.setTag(annotation.name());
324
325                if (j == 0) {
326                    info.setDefaultTag(true);
327                }
328
329                info.setBeanClass(tagClass);
330                info.setParent(annotation.parent());
331
332                validateBeanTag(annotation.name());
333
334                beanTags.put(annotation.name(), info);
335
336                loadAttributeProperties(annotation.name(), tagClass);
337
338                classBeanTags.add(annotation.name());
339            }
340
341            beanTagsByClass.put(tagClass, classBeanTags);
342        }
343    }
344
345    /**
346     * Retrieves the name of the property being defined by the tag by parsing the method name attached to the
347     * annotation.  All annotations should be attached to the get method for the associated property.
348     *
349     * @param methodName - The name of the method attached to the annotation
350     * @return Returns the property name associated witht he method.
351     */
352    private static String getFieldName(String methodName) {
353        // Check if function is of the form isPropertyName()
354        if (methodName.substring(0, 2).toLowerCase().compareTo("is") == 0) {
355            String letter = methodName.substring(2, 3);
356            return letter.toLowerCase() + methodName.substring(3, methodName.length());
357        }
358
359        // Since the annotation is attached to the get function the property name starts at the 4th letter
360        // and has been upper-cased as assumed by the Spring Beans.
361        String letter = methodName.substring(3, 4);
362        return letter.toLowerCase() + methodName.substring(4, methodName.length());
363    }
364
365    /**
366     * Validates that the tag name is not already taken.
367     *
368     * @param tagName the name of the tag for the new bean
369     * @return true if the validation passes, false otherwise
370     */
371    protected static boolean validateBeanTag(String tagName) {
372        boolean valid = true;
373
374        Set<String> tagNames = beanTags.keySet();
375        if (tagNames.contains(tagName)) {
376            LOG.error("Duplicate tag name " + tagName);
377
378            valid = false;
379        }
380
381        return valid;
382    }
383
384    /**
385     * Validates that the tagName for the next property is not already taken.
386     *
387     * @param className - The name of the class being checked.
388     * @param tagName - The name of the new attribute tag.
389     * @param attributes - A map of the attribute tags already created
390     * @return Returns true if the validation passes, false otherwise.
391     */
392    private static boolean validateBeanAttributes(String className, String tagName,
393            Map<String, BeanTagAttributeInfo> attributes) {
394        boolean valid = true;
395
396        // Check for reserved tag names: ref, parent, abstract
397        if ((tagName.compareTo("parent") == 0) || (tagName.compareTo("ref") == 0) || (tagName.compareTo("abstract")
398                == 0)) {
399            //LOG.error("Reserved tag name " + tagName + " in bean " + className);
400            return false;
401        }
402
403        String tags[] = new String[attributes.keySet().size()];
404        tags = attributes.keySet().toArray(tags);
405        for (int j = 0; j < tags.length; j++) {
406            if (tagName.compareTo(tags[j]) == 0) {
407                LOG.error("Duplicate attribute tag name " + tagName + " in bean " + className);
408                valid = false;
409            }
410        }
411
412        return valid;
413    }
414
415    /**
416     * Retrieves the map of bean tags.  If the map has not been created yet the tags are loaded.
417     * The Bean tag map is created using the xml tag name of the bean as the key with the value consisting of
418     * information about the tag stored in a BeanTagInfo object.
419     *
420     * @return A map of xml tags and their associated information.
421     */
422    public static Map<String, BeanTagInfo> getBeanTags() {
423        if (beanTags == null || beanTags.isEmpty()) {
424            loadBeanTags();
425        }
426
427        return beanTags;
428    }
429
430    /**
431     * Retrieves the set of tag names that are valid beans for the given class.
432     *
433     * @param clazz class to retrieve tag names for
434     * @return set of tag names as string, or null if none are found
435     */
436    public static Set<String> getBeanTagsByClass(Class<?> clazz) {
437        Set<String> beanTags = null;
438
439        if (beanTagsByClass != null && beanTagsByClass.containsKey(clazz)) {
440            beanTags = beanTagsByClass.get(clazz);
441        }
442        if (beanTags == null) {
443            loadBeanTags();
444        }
445
446        return beanTags;
447    }
448
449    /**
450     * Retrieves a map of attribute and property information for the bean tags.  if the map has not been created yet
451     * the bean tags are loaded. The attribute map is a double layer map with the outer layer consisting of the xml
452     * tag as the key linked to a inner map of all properties associated with it.  The inner map uses the attribute
453     * or xml sub tag as the key to the information about the property stored in a BeanTagAttributeInfo object.
454     *
455     * @return A map of xml tags and their associated property information.
456     */
457    public static Map<String, Map<String, BeanTagAttributeInfo>> getAttributeProperties() {
458        if ((attributeProperties == null) || attributeProperties.isEmpty()) {
459            loadBeanTags();
460        }
461
462        return attributeProperties;
463    }
464
465    public static String findPropertyByType(String parentTag, String childTag) {
466        String propertyName = null;
467
468        Class<?> childTagClass = beanTags.get(childTag).getBeanClass();
469
470        Map<String, BeanTagAttributeInfo> propertyInfos = attributeProperties.get(parentTag);
471
472        for (Map.Entry<String, BeanTagAttributeInfo> propertyInfo : propertyInfos.entrySet()) {
473            BeanTagAttributeInfo info = propertyInfo.getValue();
474
475            if (info.getType().equals(BeanTagAttribute.AttributeType.BYTYPE)
476                    || info.getType().equals(BeanTagAttribute.AttributeType.DIRECTORBYTYPE)) {
477                if (info.getValueType().isAssignableFrom(childTagClass)) {
478                    propertyName = info.getPropertyName();
479                }
480            }
481        }
482
483        return propertyName;
484    }
485
486    /**
487     * Loads the list of classes involved in the custom schema from an xml document.  The list included in these xmls
488     * can include lists from other documents so recursion is used to go through these other list and compile them all
489     * together.
490     *
491     * @param path - The classpath resource to the list
492     * @return A list of all classes to involved in the schema
493     */
494    private static ArrayList<String> getClassList(String path) {
495        ArrayList<String> completeList = new ArrayList<String>();
496        try {
497            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
498            DocumentBuilder builder = factory.newDocumentBuilder();
499
500            ApplicationContext app = new ClassPathXmlApplicationContext();
501            InputStream stream = app.getResource(path).getInputStream();
502
503            Document document = builder.parse(stream);
504
505            // Read package names into a comma separated list
506            NodeList classes = document.getElementsByTagName("class");
507            String classList = "";
508            for (int i = 0; i < classes.getLength(); i++) {
509                classList = classList + classes.item(i).getTextContent() + ",";
510            }
511
512            // Split array into list by ,
513            if (classList.length() > 0) {
514                if (classList.charAt(classList.length() - 1) == ',') {
515                    classList = classList.substring(0, classList.length() - 1);
516                }
517
518                String list[] = classList.split(",");
519                for (int i = 0; i < list.length; i++) {
520                    completeList.add(list[i]);
521                }
522            }
523
524            // Add any schemas being built off of.
525            NodeList includes = document.getElementsByTagName("include");
526            for (int i = 0; i < includes.getLength(); i++) {
527                completeList.addAll(getClassList(includes.item(i).getTextContent()));
528            }
529
530        } catch (Exception e) {
531            try {
532                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
533                DocumentBuilder builder = factory.newDocumentBuilder();
534
535                File file = new File(path);
536                Document document = builder.parse(file);
537
538                // Read package names into a comma separated list
539                NodeList classes = document.getElementsByTagName("class");
540                String classList = "";
541                for (int i = 0; i < classes.getLength(); i++) {
542                    classList = classList + classes.item(i).getTextContent() + ",";
543                }
544
545                // Split array into list by ,
546                if (classList.length() > 0) {
547                    if (classList.charAt(classList.length() - 1) == ',') {
548                        classList = classList.substring(0, classList.length() - 1);
549                    }
550
551                    String list[] = classList.split(",");
552                    for (int i = 0; i < list.length; i++) {
553                        completeList.add(list[i]);
554                    }
555                }
556
557                // Add any schemas being built off of.
558                NodeList includes = document.getElementsByTagName("include");
559                for (int i = 0; i < includes.getLength(); i++) {
560                    completeList.addAll(getClassList(includes.item(i).getTextContent()));
561                }
562            } catch (Exception e1) {
563                throw new RuntimeException(e1);
564            }
565        }
566
567        return completeList;
568    }
569
570}