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.XmlEnumValue;
41  import javax.xml.bind.annotation.XmlType;
42  import java.io.File;
43  import java.io.IOException;
44  import java.net.URL;
45  import java.util.Collection;
46  import java.util.Collections;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.SortedMap;
50  import java.util.SortedSet;
51  import java.util.TreeMap;
52  
53  /**
54   * <p>The schemagen tool operates on compiled bytecode, where JavaDoc comments are not present.
55   * However, the javadoc documentation present in java source files is required within the generated
56   * XSD to increase usability and produce an XSD which does not loose out on important usage information.</p>
57   * <p>The JavaDocExtractor is used as a post processor after creating the XSDs within the compilation
58   * unit, and injects XSD annotations into the appropriate XSD elements or types.</p>
59   *
60   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>, jGuru Europe AB
61   * @since 2.0
62   */
63  public class JavaDocExtractor {
64  
65      // Internal state
66      private JavaProjectBuilder builder;
67      private Log log;
68  
69      /**
70       * Creates a JavaDocExtractor wrapping the supplied Maven Log.
71       *
72       * @param log A non-null Log.
73       */
74      public JavaDocExtractor(final Log log) {
75  
76          // Check sanity
77          Validate.notNull(log, "log");
78  
79          // Create internal state
80          this.log = log;
81          this.builder = new JavaProjectBuilder();
82      }
83  
84      /**
85       * Assigns the encoding of the underlying {@link JavaProjectBuilder}.
86       *
87       * @param encoding The non-empty encoding to be set into the underlying {@link JavaProjectBuilder}.
88       */
89      public void setEncoding(final String encoding) {
90          this.builder.setEncoding(encoding);
91      }
92  
93      /**
94       * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
95       *
96       * @param sourceCodeFiles The non-null List of source code files to add.
97       * @return This JavaDocExtractor, for call chaining.
98       * @throws IllegalArgumentException If any of the given sourceCodeFiles could not be read properly.
99       */
100     public JavaDocExtractor addSourceFiles(final List<File> sourceCodeFiles) throws IllegalArgumentException {
101 
102         // Check sanity
103         Validate.notNull(sourceCodeFiles, "addSourceFiles");
104 
105         // Add the files.
106         for (File current : sourceCodeFiles) {
107             try {
108                 builder.addSource(current);
109             } catch (IOException e) {
110                 throw new IllegalArgumentException("Could not add file ["
111                         + FileSystemUtilities.getCanonicalPath(current) + "]", e);
112             }
113         }
114 
115         // All done.
116         return this;
117     }
118 
119     /**
120      * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
121      *
122      * @param sourceCodeURLs The non-null List of source code URLs to add.
123      * @return This JavaDocExtractor, for call chaining.
124      * @throws IllegalArgumentException If any of the given sourceCodeURLs could not be read properly.
125      */
126     public JavaDocExtractor addSourceURLs(final List<URL> sourceCodeURLs) throws IllegalArgumentException {
127 
128         // Check sanity
129         Validate.notNull(sourceCodeURLs, "sourceCodeURLs");
130 
131         // Add the URLs
132         for (URL current : sourceCodeURLs) {
133             try {
134                 builder.addSource(current);
135             } catch (IOException e) {
136                 throw new IllegalArgumentException("Could not add URL [" + current.toString() + "]", e);
137             }
138         }
139 
140         // All done
141         return this;
142     }
143 
144     /**
145      * Processes all supplied Java source Files and URLs to extract JavaDocData for all ClassLocations from which
146      * JavaDoc has been collected.
147      *
148      * @return A SearchableDocumentation relating SortableLocations and their paths to harvested JavaDocData.
149      */
150     public SearchableDocumentation process() {
151 
152         // Start processing.
153         final SortedMap<SortableLocation, JavaDocData> dataHolder = new TreeMap<SortableLocation, JavaDocData>();
154         final Collection<JavaSource> sources = builder.getSources();
155 
156         if (log.isInfoEnabled()) {
157             log.info("Processing [" + sources.size() + "] java sources.");
158         }
159 
160         for (JavaSource current : sources) {
161 
162             // Add the package-level JavaDoc
163             final JavaPackage currentPackage = current.getPackage();
164             final String packageName = currentPackage.getName();
165             addEntry(dataHolder, new PackageLocation(packageName), currentPackage);
166 
167             if (log.isDebugEnabled()) {
168                 log.debug("Added package-level JavaDoc for [" + packageName + "]");
169             }
170 
171             for (JavaClass currentClass : current.getClasses()) {
172 
173                 // Add the class-level JavaDoc
174                 final String simpleClassName = currentClass.getName();
175                 final String classXmlName = getAnnotationAttributeValuleFrom(XmlType.class,
176                         "name",
177                         currentClass.getAnnotations());
178 
179                 final ClassLocation classLocation = new ClassLocation(packageName, simpleClassName, classXmlName);
180                 addEntry(dataHolder, classLocation, currentClass);
181 
182                 if (log.isDebugEnabled()) {
183                     log.debug("Added class-level JavaDoc for [" + classLocation + "]");
184                 }
185 
186                 for (JavaField currentField : currentClass.getFields()) {
187 
188                     // Find the XML name if provided within an annotation.
189                     String annotatedXmlName = getAnnotationAttributeValuleFrom(
190                             XmlElement.class,
191                             "name",
192                             currentField.getAnnotations());
193 
194                     if (annotatedXmlName == null) {
195                         annotatedXmlName = getAnnotationAttributeValuleFrom(
196                                 XmlAttribute.class,
197                                 "name",
198                                 currentField.getAnnotations());
199                     }
200                     if (annotatedXmlName == null) {
201                         annotatedXmlName = getAnnotationAttributeValuleFrom(
202                                 XmlEnumValue.class,
203                                 "value",
204                                 currentField.getAnnotations());
205                     }
206 
207                     // Add the field-level JavaDoc
208                     final FieldLocation fieldLocation = new FieldLocation(
209                             packageName,
210                             simpleClassName,
211                             classXmlName,
212                             currentField.getName(),
213                             annotatedXmlName);
214 
215                     addEntry(dataHolder, fieldLocation, currentField);
216 
217                     if (log.isDebugEnabled()) {
218                         log.debug("Added field-level JavaDoc for [" + fieldLocation + "]");
219                     }
220                 }
221 
222                 for (JavaMethod currentMethod : currentClass.getMethods()) {
223 
224                     // Find the XML name if provided within an annotation.
225                     String annotatedXmlName = getAnnotationAttributeValuleFrom(
226                             XmlElement.class,
227                             "name",
228                             currentMethod.getAnnotations());
229 
230                     if (annotatedXmlName == null) {
231                         annotatedXmlName = getAnnotationAttributeValuleFrom(
232                                 XmlAttribute.class,
233                                 "name",
234                                 currentMethod.getAnnotations());
235                     }
236 
237                     // Add the method-level JavaDoc
238                     final MethodLocation location = new MethodLocation(packageName,
239                             simpleClassName,
240                             classXmlName,
241                             currentMethod.getName(),
242                             annotatedXmlName,
243                             currentMethod.getParameters());
244                     addEntry(dataHolder, location, currentMethod);
245 
246                     if (log.isDebugEnabled()) {
247                         log.debug("Added method-level JavaDoc for [" + location + "]");
248                     }
249                 }
250             }
251         }
252 
253         // All done.
254         return new ReadOnlySearchableDocumentation(dataHolder);
255     }
256 
257     /**
258      * Finds the value of the attribute with the supplied name within the first matching JavaAnnotation of
259      * the given type encountered in the given annotations List. This is typically used for reading values of
260      * annotations such as {@link XmlElement}, {@link XmlAttribute} or {@link XmlEnumValue}.
261      *
262      * @param annotations    The list of JavaAnnotations to filter from.
263      * @param annotationType The type of annotation to read attribute values from.
264      * @param attributeName  The name of the attribute the value of which should be returned.
265      * @return The first matching JavaAnnotation of type annotationType within the given annotations
266      * List, or {@code null} if none was found.
267      * @since 2.2
268      */
269     private static String getAnnotationAttributeValuleFrom(
270             final Class<?> annotationType,
271             final String attributeName,
272             final List<JavaAnnotation> annotations) {
273 
274         // QDox uses the fully qualified class name of the annotation for comparison.
275         // Extract it.
276         final String fullyQualifiedClassName = annotationType.getName();
277 
278         JavaAnnotation annotation = null;
279         String toReturn = null;
280 
281         if (annotations != null) {
282 
283             for (JavaAnnotation current : annotations) {
284                 if (current.getType().isA(fullyQualifiedClassName)) {
285                     annotation = current;
286                     break;
287                 }
288             }
289 
290             if (annotation != null) {
291 
292                 final Object nameValue = annotation.getNamedParameter(attributeName);
293 
294                 if (nameValue != null && nameValue instanceof String) {
295 
296                     toReturn = ((String) nameValue).trim();
297 
298                     // Remove initial and trailing " chars, if present.
299                     if (toReturn.startsWith("\"") && toReturn.endsWith("\"")) {
300                         toReturn = (((String) nameValue).trim()).substring(1, toReturn.length() - 1);
301                     }
302                 }
303             }
304         }
305 
306         // All Done.
307         return toReturn;
308     }
309 
310     //
311     // Private helpers
312     //
313 
314     private void addEntry(final SortedMap<SortableLocation, JavaDocData> map,
315             final SortableLocation key,
316             final JavaAnnotatedElement value) {
317 
318         // Check sanity
319         if (map.containsKey(key)) {
320 
321             // Get something to compare with
322             final JavaDocData existing = map.get(key);
323 
324             // Is this an empty package-level documentation?
325             if (key instanceof PackageLocation) {
326 
327                 final boolean emptyExisting = existing.getComment() == null || existing.getComment().isEmpty();
328                 final boolean emptyGiven = value.getComment() == null || value.getComment().isEmpty();
329 
330                 if (emptyGiven) {
331                     if (log.isDebugEnabled()) {
332                         log.debug("Skipping processing empty Package javadoc from [" + key + "]");
333                     }
334                     return;
335                 } else if (emptyExisting && log.isWarnEnabled()) {
336                     log.warn("Overwriting empty Package javadoc from [" + key + "]");
337                 }
338             } else {
339                 final String given = "[" + value.getClass().getName() + "]: " + value.getComment();
340                 throw new IllegalArgumentException("Not processing duplicate SortableLocation [" + key + "]. "
341                         + "\n Existing: " + existing
342                         + ".\n Given: [" + given + "]");
343             }
344         }
345 
346         // Validate.isTrue(!map.containsKey(key), "Found duplicate SortableLocation [" + key + "] in map. "
347         //         + "Current map keySet: " + map.keySet() + ". Got comment: [" + value.getComment() + "]");
348 
349         map.put(key, new JavaDocData(value.getComment(), value.getTags()));
350     }
351 
352     /**
353      * Standard read-only SearchableDocumentation implementation.
354      */
355     private class ReadOnlySearchableDocumentation implements SearchableDocumentation {
356 
357         // Internal state
358         private TreeMap<String, SortableLocation> keyMap;
359         private SortedMap<? extends SortableLocation, JavaDocData> valueMap;
360 
361         ReadOnlySearchableDocumentation(final SortedMap<SortableLocation, JavaDocData> valueMap) {
362 
363             // Create internal state
364             this.valueMap = valueMap;
365 
366             keyMap = new TreeMap<String, SortableLocation>();
367             for (Map.Entry<SortableLocation, JavaDocData> current : valueMap.entrySet()) {
368 
369                 final SortableLocation key = current.getKey();
370                 keyMap.put(key.getPath(), key);
371             }
372         }
373 
374         /**
375          * {@inheritDoc}
376          */
377         @Override
378         public SortedSet<String> getPaths() {
379             return Collections.unmodifiableSortedSet(keyMap.navigableKeySet());
380         }
381 
382         /**
383          * {@inheritDoc}
384          */
385         @Override
386         public JavaDocData getJavaDoc(final String path) {
387 
388             // Check sanity
389             Validate.notNull(path, "path");
390 
391             // All done.
392             final SortableLocation location = getLocation(path);
393             return (location == null) ? null : valueMap.get(location);
394         }
395 
396         /**
397          * {@inheritDoc}
398          */
399         @Override
400         @SuppressWarnings("unchecked")
401         public <T extends SortableLocation> T getLocation(final String path) {
402 
403             // Check sanity
404             Validate.notNull(path, "path");
405 
406             // All done
407             return (T) keyMap.get(path);
408         }
409 
410         /**
411          * {@inheritDoc}
412          */
413         @Override
414         @SuppressWarnings("unchecked")
415         public SortedMap<SortableLocation, JavaDocData> getAll() {
416             return (SortedMap<SortableLocation, JavaDocData>) Collections.unmodifiableSortedMap(valueMap);
417         }
418 
419         /**
420          * {@inheritDoc}
421          */
422         @Override
423         @SuppressWarnings("unchecked")
424         public <T extends SortableLocation> SortedMap<T, JavaDocData> getAll(final Class<T> type) {
425 
426             // Check sanity
427             Validate.notNull(type, "type");
428 
429             // Filter the valueMap.
430             final SortedMap<T, JavaDocData> toReturn = new TreeMap<T, JavaDocData>();
431             for (Map.Entry<? extends SortableLocation, JavaDocData> current : valueMap.entrySet()) {
432                 if (type == current.getKey().getClass()) {
433                     toReturn.put((T) current.getKey(), current.getValue());
434                 }
435             }
436 
437             // All done.
438             return toReturn;
439         }
440     }
441 }