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     */
017    package org.apache.xbean.blueprint.generator;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.net.URL;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Enumeration;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Set;
030    import java.util.TreeSet;
031    import java.util.jar.JarEntry;
032    import java.util.jar.JarFile;
033    
034    import com.thoughtworks.qdox.JavaDocBuilder;
035    import com.thoughtworks.qdox.model.BeanProperty;
036    import com.thoughtworks.qdox.model.DocletTag;
037    import com.thoughtworks.qdox.model.JavaClass;
038    import com.thoughtworks.qdox.model.JavaMethod;
039    import com.thoughtworks.qdox.model.JavaParameter;
040    import com.thoughtworks.qdox.model.JavaSource;
041    import com.thoughtworks.qdox.model.Type;
042    import org.apache.commons.logging.Log;
043    import org.apache.commons.logging.LogFactory;
044    
045    /**
046     * @author Dain Sundstrom
047     * @version $Id$
048     * @since 1.0
049     */
050    public 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.blueprint.generator.Type toMappingType(Type type, String nestedType) {
481            try {
482                if (type.isArray()) {
483                    return org.apache.xbean.blueprint.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.blueprint.generator.Type.newCollectionType(type.getValue(),
487                            org.apache.xbean.blueprint.generator.Type.newSimpleType(nestedType));
488                }
489            } catch (Throwable t) {
490                log.debug("Could not load type mapping", t);
491            }
492            return org.apache.xbean.blueprint.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    }