001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.xbean.spring.generator; 018 019import java.io.File; 020import java.io.IOException; 021import java.net.URL; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Enumeration; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.TreeSet; 031import java.util.jar.JarEntry; 032import java.util.jar.JarFile; 033 034import com.thoughtworks.qdox.JavaDocBuilder; 035import com.thoughtworks.qdox.model.BeanProperty; 036import com.thoughtworks.qdox.model.DocletTag; 037import com.thoughtworks.qdox.model.JavaClass; 038import com.thoughtworks.qdox.model.JavaMethod; 039import com.thoughtworks.qdox.model.JavaParameter; 040import com.thoughtworks.qdox.model.JavaSource; 041import com.thoughtworks.qdox.model.Type; 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044 045/** 046 * @author Dain Sundstrom 047 * @version $Id$ 048 * @since 1.0 049 */ 050public class QdoxMappingLoader implements MappingLoader { 051 public static final String XBEAN_ANNOTATION = "org.apache.xbean.XBean"; 052 public static final String PROPERTY_ANNOTATION = "org.apache.xbean.Property"; 053 public static final String INIT_METHOD_ANNOTATION = "org.apache.xbean.InitMethod"; 054 public static final String DESTROY_METHOD_ANNOTATION = "org.apache.xbean.DestroyMethod"; 055 public static final String FACTORY_METHOD_ANNOTATION = "org.apache.xbean.FactoryMethod"; 056 public static final String MAP_ANNOTATION = "org.apache.xbean.Map"; 057 public static final String FLAT_PROPERTY_ANNOTATION = "org.apache.xbean.Flat"; 058 public static final String FLAT_COLLECTION_ANNOTATION = "org.apache.xbean.FlatCollection"; 059 public static final String ELEMENT_ANNOTATION = "org.apache.xbean.Element"; 060 061 private static final Log log = LogFactory.getLog(QdoxMappingLoader.class); 062 private final String defaultNamespace; 063 private final File[] srcDirs; 064 private final String[] excludedClasses; 065 private Type collectionType; 066 067 public QdoxMappingLoader(String defaultNamespace, File[] srcDirs, String[] excludedClasses) { 068 this.defaultNamespace = defaultNamespace; 069 this.srcDirs = srcDirs; 070 this.excludedClasses = excludedClasses; 071 } 072 073 public String getDefaultNamespace() { 074 return defaultNamespace; 075 } 076 077 public File[] getSrcDirs() { 078 return srcDirs; 079 } 080 081 public Set<NamespaceMapping> loadNamespaces() throws IOException { 082 JavaDocBuilder builder = new JavaDocBuilder(); 083 084 log.debug("Source directories: "); 085 086 for (File sourceDirectory : srcDirs) { 087 if (!sourceDirectory.isDirectory() && !sourceDirectory.toString().endsWith(".jar")) { 088 log.warn("Specified source directory isn't a directory or a jar file: '" + sourceDirectory.getAbsolutePath() + "'."); 089 } 090 log.debug(" - " + sourceDirectory.getAbsolutePath()); 091 092 getSourceFiles(sourceDirectory, excludedClasses, builder); 093 } 094 095 collectionType = builder.getClassByName("java.util.Collection").asType(); 096 return loadNamespaces(builder); 097 } 098 099 private Set<NamespaceMapping> loadNamespaces(JavaDocBuilder builder) { 100 // load all of the elements 101 List<ElementMapping> elements = loadElements(builder); 102 103 // index the elements by namespace and find the root element of each namespace 104 Map<String, Set<ElementMapping>> elementsByNamespace = new HashMap<String, Set<ElementMapping>>(); 105 Map<String, ElementMapping> namespaceRoots = new HashMap<String, ElementMapping>(); 106 for (ElementMapping element : elements) { 107 String namespace = element.getNamespace(); 108 Set<ElementMapping> namespaceElements = elementsByNamespace.get(namespace); 109 if (namespaceElements == null) { 110 namespaceElements = new HashSet<ElementMapping>(); 111 elementsByNamespace.put(namespace, namespaceElements); 112 } 113 namespaceElements.add(element); 114 if (element.isRootElement()) { 115 if (namespaceRoots.containsKey(namespace)) { 116 log.info("Multiple root elements found for namespace " + namespace); 117 } 118 namespaceRoots.put(namespace, element); 119 } 120 } 121 122 // build the NamespaceMapping objects 123 Set<NamespaceMapping> namespaces = new TreeSet<NamespaceMapping>(); 124 for (Map.Entry<String, Set<ElementMapping>> entry : elementsByNamespace.entrySet()) { 125 String namespace = entry.getKey(); 126 Set namespaceElements = entry.getValue(); 127 ElementMapping rootElement = namespaceRoots.get(namespace); 128 NamespaceMapping namespaceMapping = new NamespaceMapping(namespace, namespaceElements, rootElement); 129 namespaces.add(namespaceMapping); 130 } 131 return Collections.unmodifiableSet(namespaces); 132 } 133 134 private List<ElementMapping> loadElements(JavaDocBuilder builder) { 135 JavaSource[] javaSources = builder.getSources(); 136 List<ElementMapping> elements = new ArrayList<ElementMapping>(); 137 for (JavaSource javaSource : javaSources) { 138 if (javaSource.getClasses().length == 0) { 139 log.info("No Java Classes defined in: " + javaSource.getURL()); 140 } else { 141 JavaClass[] classes = javaSource.getClasses(); 142 for (JavaClass javaClass : classes) { 143 ElementMapping element = loadElement(builder, javaClass); 144 if (element != null && !javaClass.isAbstract()) { 145 elements.add(element); 146 } else { 147 log.debug("No XML annotation found for type: " + javaClass.getFullyQualifiedName()); 148 } 149 } 150 } 151 } 152 return elements; 153 } 154 155 private ElementMapping loadElement(JavaDocBuilder builder, JavaClass javaClass) { 156 DocletTag xbeanTag = javaClass.getTagByName(XBEAN_ANNOTATION); 157 if (xbeanTag == null) { 158 return null; 159 } 160 161 String element = getElementName(javaClass, xbeanTag); 162 String description = getProperty(xbeanTag, "description"); 163 if (description == null) { 164 description = javaClass.getComment(); 165 166 } 167 String namespace = getProperty(xbeanTag, "namespace", defaultNamespace); 168 boolean root = getBooleanProperty(xbeanTag, "rootElement"); 169 String contentProperty = getProperty(xbeanTag, "contentProperty"); 170 String factoryClass = getProperty(xbeanTag, "factoryClass"); 171 172 Map<String, MapMapping> mapsByPropertyName = new HashMap<String, MapMapping>(); 173 List<String> flatProperties = new ArrayList<String>(); 174 Map<String, String> flatCollections = new HashMap<String, String>(); 175 Set<AttributeMapping> attributes = new HashSet<AttributeMapping>(); 176 Map<String, AttributeMapping> attributesByPropertyName = new HashMap<String, AttributeMapping>(); 177 178 for (JavaClass jClass = javaClass; jClass != null; jClass = jClass.getSuperJavaClass()) { 179 BeanProperty[] beanProperties = jClass.getBeanProperties(); 180 for (BeanProperty beanProperty : beanProperties) { 181 // we only care about properties with a setter 182 if (beanProperty.getMutator() != null) { 183 AttributeMapping attributeMapping = loadAttribute(beanProperty, ""); 184 if (attributeMapping != null) { 185 attributes.add(attributeMapping); 186 attributesByPropertyName.put(attributeMapping.getPropertyName(), attributeMapping); 187 } 188 JavaMethod acc = beanProperty.getAccessor(); 189 if (acc != null) { 190 DocletTag mapTag = acc.getTagByName(MAP_ANNOTATION); 191 if (mapTag != null) { 192 MapMapping mm = new MapMapping( 193 mapTag.getNamedParameter("entryName"), 194 mapTag.getNamedParameter("keyName"), 195 Boolean.valueOf(mapTag.getNamedParameter("flat")), 196 mapTag.getNamedParameter("dups"), 197 mapTag.getNamedParameter("defaultKey")); 198 mapsByPropertyName.put(beanProperty.getName(), mm); 199 } 200 201 DocletTag flatColTag = acc.getTagByName(FLAT_COLLECTION_ANNOTATION); 202 if (flatColTag != null) { 203 String childName = flatColTag.getNamedParameter("childElement"); 204 if (childName == null) 205 throw new InvalidModelException("Flat collections must specify the childElement attribute."); 206 flatCollections.put(beanProperty.getName(), childName); 207 } 208 209 DocletTag flatPropTag = acc.getTagByName(FLAT_PROPERTY_ANNOTATION); 210 if (flatPropTag != null) { 211 flatProperties.add(beanProperty.getName()); 212 } 213 } 214 } 215 } 216 } 217 218 String initMethod = null; 219 String destroyMethod = null; 220 String factoryMethod = null; 221 for (JavaClass jClass = javaClass; jClass != null; jClass = jClass.getSuperJavaClass()) { 222 JavaMethod[] methods = javaClass.getMethods(); 223 for (JavaMethod method : methods) { 224 if (method.isPublic() && !method.isConstructor()) { 225 if (initMethod == null && method.getTagByName(INIT_METHOD_ANNOTATION) != null) { 226 initMethod = method.getName(); 227 } 228 if (destroyMethod == null && method.getTagByName(DESTROY_METHOD_ANNOTATION) != null) { 229 destroyMethod = method.getName(); 230 } 231 if (factoryMethod == null && method.getTagByName(FACTORY_METHOD_ANNOTATION) != null) { 232 factoryMethod = method.getName(); 233 } 234 235 } 236 } 237 } 238 239 List<List<ParameterMapping>> constructorArgs = new ArrayList<List<ParameterMapping>>(); 240 JavaMethod[] methods = javaClass.getMethods(); 241 for (JavaMethod method : methods) { 242 JavaParameter[] parameters = method.getParameters(); 243 if (isValidConstructor(factoryMethod, method, parameters)) { 244 List<ParameterMapping> args = new ArrayList<ParameterMapping>(parameters.length); 245 for (JavaParameter parameter : parameters) { 246 AttributeMapping attributeMapping = attributesByPropertyName.get(parameter.getName()); 247 if (attributeMapping == null) { 248 attributeMapping = loadParameter(parameter); 249 250 attributes.add(attributeMapping); 251 attributesByPropertyName.put(attributeMapping.getPropertyName(), attributeMapping); 252 } 253 args.add(new ParameterMapping(attributeMapping.getPropertyName(), toMappingType(parameter.getType(), null))); 254 } 255 constructorArgs.add(Collections.unmodifiableList(args)); 256 } 257 } 258 259 HashSet<String> interfaces = new HashSet<String>(); 260 interfaces.addAll(getFullyQualifiedNames(javaClass.getImplementedInterfaces())); 261 262 JavaClass actualClass = javaClass; 263 if (factoryClass != null) { 264 JavaClass clazz = builder.getClassByName(factoryClass); 265 if (clazz != null) { 266 log.info("Detected factory: using " + factoryClass + " instead of " + javaClass.getFullyQualifiedName()); 267 actualClass = clazz; 268 } else { 269 log.info("Could not load class built by factory: " + factoryClass); 270 } 271 } 272 273 ArrayList<String> superClasses = new ArrayList<String>(); 274 JavaClass p = actualClass; 275 if (actualClass != javaClass) { 276 superClasses.add(actualClass.getFullyQualifiedName()); 277 } 278 while (true) { 279 JavaClass s = p.getSuperJavaClass(); 280 if (s == null || s.equals(p) || "java.lang.Object".equals(s.getFullyQualifiedName())) { 281 break; 282 } 283 p = s; 284 superClasses.add(p.getFullyQualifiedName()); 285 interfaces.addAll(getFullyQualifiedNames(p.getImplementedInterfaces())); 286 } 287 288 return new ElementMapping(namespace, 289 element, 290 javaClass.getFullyQualifiedName(), 291 description, 292 root, 293 initMethod, 294 destroyMethod, 295 factoryMethod, 296 contentProperty, 297 attributes, 298 constructorArgs, 299 flatProperties, 300 mapsByPropertyName, 301 flatCollections, 302 superClasses, 303 interfaces); 304 } 305 306 private List<String> getFullyQualifiedNames(JavaClass[] implementedInterfaces) { 307 ArrayList<String> l = new ArrayList<String>(); 308 for (JavaClass implementedInterface : implementedInterfaces) { 309 l.add(implementedInterface.getFullyQualifiedName()); 310 } 311 return l; 312 } 313 314 private String getElementName(JavaClass javaClass, DocletTag tag) { 315 String elementName = getProperty(tag, "element"); 316 if (elementName == null) { 317 String className = javaClass.getFullyQualifiedName(); 318 int index = className.lastIndexOf("."); 319 if (index > 0) { 320 className = className.substring(index + 1); 321 } 322 // strip off "Bean" from a spring factory bean 323 if (className.endsWith("FactoryBean")) { 324 className = className.substring(0, className.length() - 4); 325 } 326 elementName = Utils.decapitalise(className); 327 } 328 return elementName; 329 } 330 331 private AttributeMapping loadAttribute(BeanProperty beanProperty, String defaultDescription) { 332 DocletTag propertyTag = getPropertyTag(beanProperty); 333 334 if (getBooleanProperty(propertyTag, "hidden")) { 335 return null; 336 } 337 338 String attribute = getProperty(propertyTag, "alias", beanProperty.getName()); 339 String attributeDescription = getAttributeDescription(beanProperty, propertyTag, defaultDescription); 340 String defaultValue = getProperty(propertyTag, "default"); 341 boolean fixed = getBooleanProperty(propertyTag, "fixed"); 342 boolean required = getBooleanProperty(propertyTag, "required"); 343 String nestedType = getProperty(propertyTag, "nestedType"); 344 String propertyEditor = getProperty(propertyTag, "propertyEditor"); 345 346 return new AttributeMapping(attribute, 347 beanProperty.getName(), 348 attributeDescription, 349 toMappingType(beanProperty.getType(), nestedType), 350 defaultValue, 351 fixed, 352 required, 353 propertyEditor); 354 } 355 356 private static DocletTag getPropertyTag(BeanProperty beanProperty) { 357 JavaMethod accessor = beanProperty.getAccessor(); 358 if (accessor != null) { 359 DocletTag propertyTag = accessor.getTagByName(PROPERTY_ANNOTATION); 360 if (propertyTag != null) { 361 return propertyTag; 362 } 363 } 364 JavaMethod mutator = beanProperty.getMutator(); 365 if (mutator != null) { 366 DocletTag propertyTag = mutator.getTagByName(PROPERTY_ANNOTATION); 367 if (propertyTag != null) { 368 return propertyTag; 369 } 370 } 371 return null; 372 } 373 374 private String getAttributeDescription(BeanProperty beanProperty, DocletTag propertyTag, String defaultDescription) { 375 String description = getProperty(propertyTag, "description"); 376 if (description != null && description.trim().length() > 0) { 377 return description.trim(); 378 } 379 380 JavaMethod accessor = beanProperty.getAccessor(); 381 if (accessor != null) { 382 description = accessor.getComment(); 383 if (description != null && description.trim().length() > 0) { 384 return description.trim(); 385 } 386 } 387 388 JavaMethod mutator = beanProperty.getMutator(); 389 if (mutator != null) { 390 description = mutator.getComment(); 391 if (description != null && description.trim().length() > 0) { 392 return description.trim(); 393 } 394 } 395 return defaultDescription; 396 } 397 398 private AttributeMapping loadParameter(JavaParameter parameter) { 399 String parameterName = parameter.getName(); 400 String parameterDescription = getParameterDescription(parameter); 401 402 // first attempt to load the attribute from the java beans accessor methods 403 JavaClass javaClass = parameter.getParentMethod().getParentClass(); 404 BeanProperty beanProperty = javaClass.getBeanProperty(parameterName); 405 if (beanProperty != null) { 406 AttributeMapping attributeMapping = loadAttribute(beanProperty, parameterDescription); 407 // if the attribute mapping is null, the property was tagged as hidden and this is an error 408 if (attributeMapping == null) { 409 throw new InvalidModelException("Hidden property usage: " + 410 "The construction method " + toMethodLocator(parameter.getParentMethod()) + 411 " can not use a hidded property " + parameterName); 412 } 413 return attributeMapping; 414 } 415 416 // create an attribute solely based on the parameter information 417 return new AttributeMapping(parameterName, 418 parameterName, 419 parameterDescription, 420 toMappingType(parameter.getType(), null), 421 null, 422 false, 423 false, 424 null); 425 } 426 427 private String getParameterDescription(JavaParameter parameter) { 428 String parameterName = parameter.getName(); 429 DocletTag[] tags = parameter.getParentMethod().getTagsByName("param"); 430 for (DocletTag tag : tags) { 431 if (tag.getParameters()[0].equals(parameterName)) { 432 String parameterDescription = tag.getValue().trim(); 433 if (parameterDescription.startsWith(parameterName)) { 434 parameterDescription = parameterDescription.substring(parameterName.length()).trim(); 435 } 436 return parameterDescription; 437 } 438 } 439 return null; 440 } 441 442 private boolean isValidConstructor(String factoryMethod, JavaMethod method, JavaParameter[] parameters) { 443 if (!method.isPublic() || parameters.length == 0) { 444 return false; 445 } 446 447 if (factoryMethod == null) { 448 return method.isConstructor(); 449 } else { 450 return method.getName().equals(factoryMethod); 451 } 452 } 453 454 private static String getProperty(DocletTag propertyTag, String propertyName) { 455 return getProperty(propertyTag, propertyName, null); 456 } 457 458 private static String getProperty(DocletTag propertyTag, String propertyName, String defaultValue) { 459 String value = null; 460 if (propertyTag != null) { 461 value = propertyTag.getNamedParameter(propertyName); 462 } 463 if (value == null) { 464 return defaultValue; 465 } 466 return value; 467 } 468 469 private boolean getBooleanProperty(DocletTag propertyTag, String propertyName) { 470 return toBoolean(getProperty(propertyTag, propertyName)); 471 } 472 473 private static boolean toBoolean(String value) { 474 if (value != null) { 475 return Boolean.valueOf(value); 476 } 477 return false; 478 } 479 480 private org.apache.xbean.spring.generator.Type toMappingType(Type type, String nestedType) { 481 try { 482 if (type.isArray()) { 483 return org.apache.xbean.spring.generator.Type.newArrayType(type.getValue(), type.getDimensions()); 484 } else if (type.isA(collectionType)) { 485 if (nestedType == null) nestedType = "java.lang.Object"; 486 return org.apache.xbean.spring.generator.Type.newCollectionType(type.getValue(), 487 org.apache.xbean.spring.generator.Type.newSimpleType(nestedType)); 488 } 489 } catch (Throwable t) { 490 log.debug("Could not load type mapping", t); 491 } 492 return org.apache.xbean.spring.generator.Type.newSimpleType(type.getValue()); 493 } 494 495 private static String toMethodLocator(JavaMethod method) { 496 StringBuffer buf = new StringBuffer(); 497 buf.append(method.getParentClass().getFullyQualifiedName()); 498 if (!method.isConstructor()) { 499 buf.append(".").append(method.getName()); 500 } 501 buf.append("("); 502 JavaParameter[] parameters = method.getParameters(); 503 for (int i = 0; i < parameters.length; i++) { 504 JavaParameter parameter = parameters[i]; 505 if (i > 0) { 506 buf.append(", "); 507 } 508 buf.append(parameter.getName()); 509 } 510 buf.append(") : ").append(method.getLineNumber()); 511 return buf.toString(); 512 } 513 514 private static void getSourceFiles(File base, String[] excludedClasses, JavaDocBuilder builder) throws IOException { 515 if (base.isDirectory()) { 516 listAllFileNames(base, "", excludedClasses, builder); 517 } else { 518 listAllJarEntries(base, excludedClasses, builder); 519 } 520 } 521 522 private static void listAllFileNames(File base, String prefix, String[] excludedClasses, JavaDocBuilder builder) throws IOException { 523 if (!base.canRead() || !base.isDirectory()) { 524 throw new IllegalArgumentException(base.getAbsolutePath()); 525 } 526 File[] hits = base.listFiles(); 527 for (File hit : hits) { 528 String name = prefix.equals("") ? hit.getName() : prefix + "/" + hit.getName(); 529 if (hit.canRead() && !isExcluded(name, excludedClasses)) { 530 if (hit.isDirectory()) { 531 listAllFileNames(hit, name, excludedClasses, builder); 532 } else if (name.endsWith(".java")) { 533 builder.addSource(hit); 534 } 535 } 536 } 537 } 538 539 private static void listAllJarEntries(File base, String[] excludedClasses, JavaDocBuilder builder) throws IOException { 540 JarFile jarFile = new JarFile(base); 541 for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { 542 JarEntry entry = (JarEntry) entries.nextElement(); 543 String name = entry.getName(); 544 if (name.endsWith(".java") && !isExcluded(name, excludedClasses) && !name.endsWith("/package-info.java")) { 545 builder.addSource(new URL("jar:" + base.toURL().toString() + "!/" + name)); 546 } 547 } 548 } 549 550 private static boolean isExcluded(String sourceName, String[] excludedClasses) { 551 if (excludedClasses == null) { 552 return false; 553 } 554 555 String className = sourceName; 556 if (sourceName.endsWith(".java")) { 557 className = className.substring(0, className.length() - ".java".length()); 558 } 559 className = className.replace('/', '.'); 560 for (String excludedClass : excludedClasses) { 561 if (className.equals(excludedClass)) { 562 return true; 563 } 564 } 565 return false; 566 } 567}