View Javadoc
1   package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import com.thoughtworks.qdox.JavaProjectBuilder;
23  import com.thoughtworks.qdox.model.JavaAnnotatedElement;
24  import com.thoughtworks.qdox.model.JavaAnnotation;
25  import com.thoughtworks.qdox.model.JavaClass;
26  import com.thoughtworks.qdox.model.JavaField;
27  import com.thoughtworks.qdox.model.JavaMethod;
28  import com.thoughtworks.qdox.model.JavaPackage;
29  import com.thoughtworks.qdox.model.JavaSource;
30  import org.apache.maven.plugin.logging.Log;
31  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation;
32  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation;
33  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation;
34  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.PackageLocation;
35  import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
36  import org.codehaus.mojo.jaxb2.shared.Validate;
37  
38  import javax.xml.bind.annotation.XmlAttribute;
39  import javax.xml.bind.annotation.XmlElement;
40  import javax.xml.bind.annotation.XmlElementWrapper;
41  import javax.xml.bind.annotation.XmlEnumValue;
42  import javax.xml.bind.annotation.XmlType;
43  import java.io.File;
44  import java.io.IOException;
45  import java.net.URL;
46  import java.util.Collection;
47  import java.util.Collections;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.SortedMap;
51  import java.util.SortedSet;
52  import java.util.TreeMap;
53  
54  /**
55   * <p>The schemagen tool operates on compiled bytecode, where JavaDoc comments are not present.
56   * However, the javadoc documentation present in java source files is required within the generated
57   * XSD to increase usability and produce an XSD which does not loose out on important usage information.</p>
58   * <p>The JavaDocExtractor is used as a post processor after creating the XSDs within the compilation
59   * unit, and injects XSD annotations into the appropriate XSD elements or types.</p>
60   *
61   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>, jGuru Europe AB
62   * @since 2.0
63   */
64  public class JavaDocExtractor {
65  
66      /**
67       * The default value given as the return value from some annotation classes whenever the attribute
68       * has not been supplied within the codebase.
69       */
70      private static final String DEFAULT_VALUE = "##default";
71  
72      // Internal state
73      private JavaProjectBuilder builder;
74      private Log log;
75  
76      /**
77       * Creates a JavaDocExtractor wrapping the supplied Maven Log.
78       *
79       * @param log A non-null Log.
80       */
81      public JavaDocExtractor(final Log log) {
82  
83          // Check sanity
84          Validate.notNull(log, "log");
85  
86          // Create internal state
87          this.log = log;
88          this.builder = new JavaProjectBuilder();
89      }
90  
91      /**
92       * Assigns the encoding of the underlying {@link JavaProjectBuilder}.
93       *
94       * @param encoding The non-empty encoding to be set into the underlying {@link JavaProjectBuilder}.
95       */
96      public void setEncoding(final String encoding) {
97          this.builder.setEncoding(encoding);
98      }
99  
100     /**
101      * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
102      *
103      * @param sourceCodeFiles The non-null List of source code files to add.
104      * @return This JavaDocExtractor, for call chaining.
105      * @throws IllegalArgumentException If any of the given sourceCodeFiles could not be read properly.
106      */
107     public JavaDocExtractor addSourceFiles(final List<File> sourceCodeFiles) throws IllegalArgumentException {
108 
109         // Check sanity
110         Validate.notNull(sourceCodeFiles, "addSourceFiles");
111 
112         // Add the files.
113         for (File current : sourceCodeFiles) {
114             try {
115                 builder.addSource(current);
116             } catch (IOException e) {
117                 throw new IllegalArgumentException("Could not add file ["
118                         + FileSystemUtilities.getCanonicalPath(current) + "]", e);
119             }
120         }
121 
122         // All done.
123         return this;
124     }
125 
126     /**
127      * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
128      *
129      * @param sourceCodeURLs The non-null List of source code URLs to add.
130      * @return This JavaDocExtractor, for call chaining.
131      * @throws IllegalArgumentException If any of the given sourceCodeURLs could not be read properly.
132      */
133     public JavaDocExtractor addSourceURLs(final List<URL> sourceCodeURLs) throws IllegalArgumentException {
134 
135         // Check sanity
136         Validate.notNull(sourceCodeURLs, "sourceCodeURLs");
137 
138         // Add the URLs
139         for (URL current : sourceCodeURLs) {
140             try {
141                 builder.addSource(current);
142             } catch (IOException e) {
143                 throw new IllegalArgumentException("Could not add URL [" + current.toString() + "]", e);
144             }
145         }
146 
147         // All done
148         return this;
149     }
150 
151     /**
152      * Processes all supplied Java source Files and URLs to extract JavaDocData for all ClassLocations from which
153      * JavaDoc has been collected.
154      *
155      * @return A SearchableDocumentation relating SortableLocations and their paths to harvested JavaDocData.
156      */
157     public SearchableDocumentation process() {
158 
159         // Start processing.
160         final SortedMap<SortableLocation, JavaDocData> dataHolder = new TreeMap<SortableLocation, JavaDocData>();
161         final Collection<JavaSource> sources = builder.getSources();
162 
163         if (log.isInfoEnabled()) {
164             log.info("Processing [" + sources.size() + "] java sources.");
165         }
166 
167         for (JavaSource current : sources) {
168 
169             // Add the package-level JavaDoc
170             final JavaPackage currentPackage = current.getPackage();
171             final String packageName = currentPackage.getName();
172             addEntry(dataHolder, new PackageLocation(packageName), currentPackage);
173 
174             if (log.isDebugEnabled()) {
175                 log.debug("Added package-level JavaDoc for [" + packageName + "]");
176             }
177 
178             for (JavaClass currentClass : current.getClasses()) {
179 
180                 // Add the class-level JavaDoc
181                 final String simpleClassName = currentClass.getName();
182                 final String classXmlName = getAnnotationAttributeValueFrom(XmlType.class,
183                         "name",
184                         currentClass.getAnnotations());
185 
186                 final ClassLocation classLocation = new ClassLocation(packageName, simpleClassName, classXmlName);
187                 addEntry(dataHolder, classLocation, currentClass);
188 
189                 if (log.isDebugEnabled()) {
190                     log.debug("Added class-level JavaDoc for [" + classLocation + "]");
191                 }
192 
193                 for (JavaField currentField : currentClass.getFields()) {
194 
195                     final List<JavaAnnotation> currentFieldAnnotations = currentField.getAnnotations();
196                     String annotatedXmlName = null;
197 
198                     //
199                     // Is this field a collection, annotated with @XmlElementWrapper?
200                     // If so, the documentation should pertain to the corresponding XML Sequence,
201                     // rather than the individual XML elements.
202                     //
203                     if (hasAnnotation(XmlElementWrapper.class, currentFieldAnnotations)) {
204 
205                         // There are 2 cases here:
206                         //
207                         // 1: The XmlElementWrapper is named.
208                         // ==================================
209                         // @XmlElementWrapper(name = "foobar")
210                         // @XmlElement(name = "aString")
211                         // private List<String> strings;
212                         //
213                         // ==> annotatedXmlName == "foobar"
214                         //
215                         // 2: The XmlElementWrapper is not named.
216                         // ======================================
217                         // @XmlElementWrapper
218                         // @XmlElement(name = "anInteger")
219                         // private SortedSet<Integer> integerSet;
220                         //
221                         // ==> annotatedXmlName == "integerSet"
222                         //
223                         annotatedXmlName = getAnnotationAttributeValueFrom(
224                                 XmlElementWrapper.class,
225                                 "name",
226                                 currentFieldAnnotations);
227 
228                         if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) {
229                             annotatedXmlName = currentField.getName();
230                         }
231                     }
232 
233                     // Find the XML name if provided within an annotation.
234                     if (annotatedXmlName == null) {
235                         annotatedXmlName = getAnnotationAttributeValueFrom(
236                                 XmlElement.class,
237                                 "name",
238                                 currentFieldAnnotations);
239                     }
240 
241                     if (annotatedXmlName == null) {
242                         annotatedXmlName = getAnnotationAttributeValueFrom(
243                                 XmlAttribute.class,
244                                 "name",
245                                 currentFieldAnnotations);
246                     }
247                     if (annotatedXmlName == null) {
248                         annotatedXmlName = getAnnotationAttributeValueFrom(
249                                 XmlEnumValue.class,
250                                 "value",
251                                 currentFieldAnnotations);
252                     }
253 
254                     // Add the field-level JavaDoc
255                     final FieldLocation fieldLocation = new FieldLocation(
256                             packageName,
257                             simpleClassName,
258                             classXmlName,
259                             currentField.getName(),
260                             annotatedXmlName);
261 
262                     addEntry(dataHolder, fieldLocation, currentField);
263 
264                     if (log.isDebugEnabled()) {
265                         log.debug("Added field-level JavaDoc for [" + fieldLocation + "]");
266                     }
267                 }
268 
269                 for (JavaMethod currentMethod : currentClass.getMethods()) {
270 
271                     final List<JavaAnnotation> currentMethodAnnotations = currentMethod.getAnnotations();
272                     String annotatedXmlName = null;
273 
274                     //
275                     // Is this field a collection, annotated with @XmlElementWrapper?
276                     // If so, the documentation should pertain to the corresponding XML Sequence,
277                     // rather than the individual XML elements.
278                     //
279                     if (hasAnnotation(XmlElementWrapper.class, currentMethodAnnotations)) {
280 
281                         // There are 2 cases here:
282                         //
283                         // 1: The XmlElementWrapper is named.
284                         // ==================================
285                         // @XmlElementWrapper(name = "foobar")
286                         // @XmlElement(name = "aString")
287                         // public List<String> getStrings() { ... };
288                         //
289                         // ==> annotatedXmlName == "foobar"
290                         //
291                         // 2: The XmlElementWrapper is not named.
292                         // ======================================
293                         // @XmlElementWrapper
294                         // @XmlElement(name = "anInteger")
295                         // public SortedSet<Integer> getIntegerSet() { ... };
296                         //
297                         // ==> annotatedXmlName == "getIntegerSet"
298                         //
299                         annotatedXmlName = getAnnotationAttributeValueFrom(
300                                 XmlElementWrapper.class,
301                                 "name",
302                                 currentMethodAnnotations);
303 
304                         if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) {
305                             annotatedXmlName = currentMethod.getName();
306                         }
307                     }
308 
309 
310                     // Find the XML name if provided within an annotation.
311                     if (annotatedXmlName == null) {
312                         annotatedXmlName = getAnnotationAttributeValueFrom(
313                                 XmlElement.class,
314                                 "name",
315                                 currentMethod.getAnnotations());
316                     }
317 
318                     if (annotatedXmlName == null) {
319                         annotatedXmlName = getAnnotationAttributeValueFrom(
320                                 XmlAttribute.class,
321                                 "name",
322                                 currentMethod.getAnnotations());
323                     }
324 
325                     // Add the method-level JavaDoc
326                     final MethodLocation location = new MethodLocation(packageName,
327                             simpleClassName,
328                             classXmlName,
329                             currentMethod.getName(),
330                             annotatedXmlName,
331                             currentMethod.getParameters());
332                     addEntry(dataHolder, location, currentMethod);
333 
334                     if (log.isDebugEnabled()) {
335                         log.debug("Added method-level JavaDoc for [" + location + "]");
336                     }
337                 }
338             }
339         }
340 
341         // All done.
342         return new ReadOnlySearchableDocumentation(dataHolder);
343     }
344 
345     /**
346      * Finds the value of the attribute with the supplied name within the first matching JavaAnnotation of
347      * the given type encountered in the given annotations List. This is typically used for reading values of
348      * annotations such as {@link XmlElement}, {@link XmlAttribute} or {@link XmlEnumValue}.
349      *
350      * @param annotations    The list of JavaAnnotations to filter from.
351      * @param annotationType The type of annotation to read attribute values from.
352      * @param attributeName  The name of the attribute the value of which should be returned.
353      * @return The first matching JavaAnnotation of type annotationType within the given annotations
354      * List, or {@code null} if none was found.
355      * @since 2.2
356      */
357     private static String getAnnotationAttributeValueFrom(
358             final Class<?> annotationType,
359             final String attributeName,
360             final List<JavaAnnotation> annotations) {
361 
362         // QDox uses the fully qualified class name of the annotation for comparison.
363         // Extract it.
364         final String fullyQualifiedClassName = annotationType.getName();
365 
366         JavaAnnotation annotation = null;
367         String toReturn = null;
368 
369         if (annotations != null) {
370 
371             for (JavaAnnotation current : annotations) {
372                 if (current.getType().isA(fullyQualifiedClassName)) {
373                     annotation = current;
374                     break;
375                 }
376             }
377 
378             if (annotation != null) {
379 
380                 final Object nameValue = annotation.getNamedParameter(attributeName);
381 
382                 if (nameValue != null && nameValue instanceof String) {
383 
384                     toReturn = ((String) nameValue).trim();
385 
386                     // Remove initial and trailing " chars, if present.
387                     if (toReturn.startsWith("\"") && toReturn.endsWith("\"")) {
388                         toReturn = (((String) nameValue).trim()).substring(1, toReturn.length() - 1);
389                     }
390                 }
391             }
392         }
393 
394         // All Done.
395         return toReturn;
396     }
397 
398     private static boolean hasAnnotation(final Class<?> annotationType,
399                                          final List<JavaAnnotation> annotations) {
400 
401         if (annotations != null && !annotations.isEmpty() && annotationType != null) {
402 
403             final String fullAnnotationClassName = annotationType.getName();
404 
405             for (JavaAnnotation current : annotations) {
406                 if (current.getType().isA(fullAnnotationClassName)) {
407                     return true;
408                 }
409             }
410         }
411 
412         return false;
413     }
414 
415     //
416     // Private helpers
417     //
418 
419     private void addEntry(final SortedMap<SortableLocation, JavaDocData> map,
420                           final SortableLocation key,
421                           final JavaAnnotatedElement value) {
422 
423         // Check sanity
424         if (map.containsKey(key)) {
425 
426             // Get something to compare with
427             final JavaDocData existing = map.get(key);
428 
429             // Is this an empty package-level documentation?
430             if (key instanceof PackageLocation) {
431 
432                 final boolean emptyExisting = existing.getComment() == null || existing.getComment().isEmpty();
433                 final boolean emptyGiven = value.getComment() == null || value.getComment().isEmpty();
434 
435                 if (emptyGiven) {
436                     if (log.isDebugEnabled()) {
437                         log.debug("Skipping processing empty Package javadoc from [" + key + "]");
438                     }
439                     return;
440                 } else if (emptyExisting && log.isWarnEnabled()) {
441                     log.warn("Overwriting empty Package javadoc from [" + key + "]");
442                 }
443             } else {
444                 final String given = "[" + value.getClass().getName() + "]: " + value.getComment();
445                 throw new IllegalArgumentException("Not processing duplicate SortableLocation [" + key + "]. "
446                         + "\n Existing: " + existing
447                         + ".\n Given: [" + given + "]");
448             }
449         }
450 
451         // Validate.isTrue(!map.containsKey(key), "Found duplicate SortableLocation [" + key + "] in map. "
452         //         + "Current map keySet: " + map.keySet() + ". Got comment: [" + value.getComment() + "]");
453 
454         map.put(key, new JavaDocData(value.getComment(), value.getTags()));
455     }
456 
457     /**
458      * Standard read-only SearchableDocumentation implementation.
459      */
460     private class ReadOnlySearchableDocumentation implements SearchableDocumentation {
461 
462         // Internal state
463         private TreeMap<String, SortableLocation> keyMap;
464         private SortedMap<? extends SortableLocation, JavaDocData> valueMap;
465 
466         ReadOnlySearchableDocumentation(final SortedMap<SortableLocation, JavaDocData> valueMap) {
467 
468             // Create internal state
469             this.valueMap = valueMap;
470 
471             keyMap = new TreeMap<String, SortableLocation>();
472             for (Map.Entry<SortableLocation, JavaDocData> current : valueMap.entrySet()) {
473 
474                 final SortableLocation key = current.getKey();
475                 keyMap.put(key.getPath(), key);
476             }
477         }
478 
479         /**
480          * {@inheritDoc}
481          */
482         @Override
483         public SortedSet<String> getPaths() {
484             return Collections.unmodifiableSortedSet(keyMap.navigableKeySet());
485         }
486 
487         /**
488          * {@inheritDoc}
489          */
490         @Override
491         public JavaDocData getJavaDoc(final String path) {
492 
493             // Check sanity
494             Validate.notNull(path, "path");
495 
496             // All done.
497             final SortableLocation location = getLocation(path);
498             return (location == null) ? null : valueMap.get(location);
499         }
500 
501         /**
502          * {@inheritDoc}
503          */
504         @Override
505         @SuppressWarnings("unchecked")
506         public <T extends SortableLocation> T getLocation(final String path) {
507 
508             // Check sanity
509             Validate.notNull(path, "path");
510 
511             // All done
512             return (T) keyMap.get(path);
513         }
514 
515         /**
516          * {@inheritDoc}
517          */
518         @Override
519         @SuppressWarnings("unchecked")
520         public SortedMap<SortableLocation, JavaDocData> getAll() {
521             return (SortedMap<SortableLocation, JavaDocData>) Collections.unmodifiableSortedMap(valueMap);
522         }
523 
524         /**
525          * {@inheritDoc}
526          */
527         @Override
528         @SuppressWarnings("unchecked")
529         public <T extends SortableLocation> SortedMap<T, JavaDocData> getAll(final Class<T> type) {
530 
531             // Check sanity
532             Validate.notNull(type, "type");
533 
534             // Filter the valueMap.
535             final SortedMap<T, JavaDocData> toReturn = new TreeMap<T, JavaDocData>();
536             for (Map.Entry<? extends SortableLocation, JavaDocData> current : valueMap.entrySet()) {
537                 if (type == current.getKey().getClass()) {
538                     toReturn.put((T) current.getKey(), current.getValue());
539                 }
540             }
541 
542             // All done.
543             return toReturn;
544         }
545     }
546 }