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.toURI().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}