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}