View Javadoc

1   /*
2    * Copyright 2010 - org.tinyjee.maven
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *        http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  
17  package org.tinyjee.maven.dim.extensions;
18  
19  import com.thoughtworks.qdox.model.*;
20  import org.apache.maven.doxia.logging.Log;
21  import org.codehaus.plexus.util.FileUtils;
22  import org.tinyjee.maven.dim.spi.Globals;
23  import org.tinyjee.maven.dim.utils.AbstractAliasHandler;
24  import org.tinyjee.maven.dim.utils.JavaClassLoader;
25  import org.tinyjee.maven.dim.utils.PrintableMap;
26  import org.tinyjee.maven.dim.utils.SelectableJavaEntitiesList;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.net.URISyntaxException;
31  import java.net.URL;
32  import java.util.*;
33  
34  import static java.lang.Boolean.parseBoolean;
35  import static java.lang.String.valueOf;
36  import static java.util.Collections.emptyMap;
37  import static org.tinyjee.maven.dim.spi.ResourceResolver.findAllSources;
38  
39  /**
40   * JavaSourceLoader is a 'source-class' compatible helper class that loads a java class (at source level including JavaDoc)
41   * and makes the class details available to velocity templates. It can effectively be used to include content into maven sites
42   * that is similar (and in sync) with JavaDoc, with the difference that only those portions can be included that are important
43   * within a certain scope.
44   * <p/>
45   * Use this loader when you want to access the parsed content of Java sources within velocity templates using the variables that are
46   * exposed by this loader. This extension is not needed if your aim is to include code snippets.
47   * <p/>
48   * <b>Usage:</b><div><code>
49   * %{include|source=template.vm|source-class=org.tinyjee.maven.dim.extensions.JavaSourceLoader|java-source=net.sf.MyClass}
50   * </code></div>
51   * or when using the alias:<div><code>
52   * %{include|source=template.vm|source-java=net.sf.MyClass}
53   * </code></div>
54   * <p/>
55   * <b>Notes:</b><ul>
56   * <li>
57   * All lists used by this extension derive from {@link org.tinyjee.maven.dim.utils.AbstractSelectableJavaEntitiesList} which means
58   * they allow chained calls to {@code selectMatching(..)}, {@code selectNonMatching(..)}, {@code selectAnnotated(..)}, etc. in order to
59   * further limit the amount of returned java entities. Example:
60   * <code><pre>##Select all properties starting with "my" but not containing "Internal":
61   * #set($myPublicProperties = $class.properties.selectMatching("..my*").selectNonMatching("..*Internal*"))
62   * </pre></code>
63   * </li>
64   * <li>
65   * All maps used by this extension derive from {@link PrintableMap} which logs any failed key access including the
66   * existing map content and defines additional methods like "{@link PrintableMap#getContentAsString()}" and
67   * "{@link PrintableMap#printContent()}" that may be used print the map structure.
68   * </li>
69   * <li>
70   * Applied doclet tags are parsed and translated to XHTML markup. Non-inline tags are stripped from the comments and copied
71   * into a map of the format {@code Map<String,(String|Map<String,String>)>}, where all tags are mapped as {@code String}
72   * key and value pair with one exception of "{@code param}" being mapped to a Map of {@code paramName} and {@code paramDescription}.
73   * </li>
74   * <li>
75   * Applied annotations are pre-processed for simplified access within a velocity template and are stored within list rows
76   * using a map (usually referenced via "annotations"). The simple name of the mapped annotation starting with "{@code @}" is used as
77   * map key and the values are maps of annotation parameter name and value pairs. (values are always serialized to String)
78   * </li>
79   * </ul>
80   *
81   * @author Juergen_Kellerer, 2010-09-08
82   */
83  public class JavaSourceLoader extends HashMap<String, Object> {
84  
85  	private static final long serialVersionUID = -4069212921446859127L;
86  
87  	private static final String JAVA_EXT = ".java";
88  
89  	/**
90  	 * Implements the "{@link JavaSourceLoader#PARAM_ALIAS}" alias functionality.
91  	 */
92  	public static class AliasHandler extends AbstractAliasHandler {
93  		/**
94  		 * Constructs the handler (Note: Is called by
95  		 * {@link org.tinyjee.maven.dim.spi.RequestParameterTransformer#TRANSFORMERS}).
96  		 */
97  		public AliasHandler() {
98  			super(PARAM_ALIAS, PARAM_JAVA_SOURCE, JavaSourceLoader.class.getName());
99  		}
100 	}
101 
102 	/**
103 	 * Defines a shortcut for the request properties 'source-class' and 'java-source'.
104 	 * <p/>
105 	 * If this parameter is present inside the request parameters list, the system
106 	 * will effectively behave as if 'source-class' and 'java-source' where set
107 	 * separately.
108 	 */
109 	public static final String PARAM_ALIAS = "source-java";
110 
111 	/**
112 	 * Is the expected input parameter that points to the class or interface to scan.
113 	 * <p/>
114 	 * The class can be specified as file (with .java as extension) or as fully qualified class name.
115 	 * Valid examples:<ul>
116 	 * <li>your.package.YourJavaClass</li>
117 	 * <li>your/package/YourJavaClass.java</li>
118 	 * <li>src/main/java/your/package/YourJavaClass.java</li>
119 	 * </ul>
120 	 * <p/>
121 	 * If only a package name or package file path is given, the extension loads the package information
122 	 * instead. When this feature is used most output parameters will be empty.
123 	 * Valid examples:<ul>
124 	 * <li>your.package</li>
125 	 * <li>your.package</li>
126 	 * <li>your/package</li>
127 	 * <li>src/main/java/your/package</li>
128 	 * </ul>
129 	 */
130 	public static final String PARAM_JAVA_SOURCE = "java-source";
131 
132 	/**
133 	 * Optional input parameter specifying the standard path to the apidocs to use when linking via
134 	 * {@literal {@link}} or {@literal {@linkplain}}.
135 	 * <p/>
136 	 * Defaults to "./apidocs/", package specific, alternate link paths can be specified using
137 	 * "{@code apidocs-my.package=http://some-host/apidocs/}".
138 	 */
139 	public static final String PARAM_API_DOCS = "apidocs";
140 
141 	/**
142 	 * Optional boolean input parameter specifying whether the extension overwrites other input parameters.
143 	 * <br/>
144 	 * If not set (=default) nothing is overwritten except "{@code $class}".
145 	 */
146 	public static final String PARAM_OVERWRITE = "overwrite";
147 
148 
149 	/**
150 	 * Is filled with the fully qualified name of the specified class.
151 	 */
152 	public static final String OUT_PARAM_NAME = "name";
153 
154 	/**
155 	 * Is filled with the simple name of the specified class.
156 	 */
157 	public static final String OUT_PARAM_SIMPLE_NAME = "simpleName";
158 
159 	/**
160 	 * Is filled with a map containing all other output parameters that are set by this extension.
161 	 * Therefore calls like {@code $class.simpleName} and {@code $simpleName} lead to exactly the same results.
162 	 */
163 	public static final String OUT_PARAM_CLASS = "class";
164 
165 	/**
166 	 * Is filled with the {@link com.thoughtworks.qdox.model.JavaClass} instance of the specified class and allows
167 	 * accessing the QDox reflection model directly.
168 	 */
169 	public static final String OUT_PARAM_JAVA_CLASS = "javaClass";
170 
171 	/**
172 	 * Is filled with an implementation of {@link Selector} that may be used to access nested, super, implemented and derived classes.
173 	 * Example Usage with Velocity:<code><pre>
174 	 * #foreach($derivedClass in $class.select.derivedClasses)
175 	 *     "$derivedClass.name" derives from $class.name
176 	 * #end
177 	 * </pre></code>
178 	 */
179 	public static final String OUT_PARAM_SELECTOR = "select";
180 
181 	/**
182 	 * Is filled with a the JavaDoc comment of the specified class.
183 	 */
184 	public static final String OUT_PARAM_COMMENT = "comment";
185 
186 	/**
187 	 * Is filled with a the JavaDoc Doclet-Tags of the specified class.
188 	 * <p/>
189 	 * The tags map has a format like <code><pre>[
190 	 *     "tagName": "tagValue",
191 	 *     ...
192 	 *     !! Special treatment for '@param' !!
193 	 *     "param": [
194 	 *         "parameterName": "JavaDoc comment",
195 	 *         ...
196 	 *     ]
197 	 * ]</pre></code>
198 	 * Please note that any leading "{@code @}" characters are removed from the map keys, thus "{@code @since}"
199 	 * becomes {@code $class.tags.since}.
200 	 */
201 	public static final String OUT_PARAM_TAGS = "tags";
202 
203 	/**
204 	 * Is filled with a map of annotations that are applied to the class.
205 	 * <p/>
206 	 * Applied annotations are pre-processed for simplified access within a velocity template by translating the QDox model into a map.
207 	 * The simple name of the mapped annotation starting with "{@code @}" is used as map key and the values are maps of annotation
208 	 * parameter name and value pairs. All values are serialized to string, which includes arrays &amp; annotation hierarchies that were
209 	 * set as values.
210 	 * <p/>
211 	 * The annotation map has a format like <code><pre>[
212 	 *     "@SimpleAnnotationName": [
213 	 *         "parameterName": "value",
214 	 *         ...
215 	 *     ],
216 	 *     ...
217 	 * ]</pre></code>
218 	 * Accessing the default value of an annotation works as expected, e.g.:
219 	 * <code><pre>
220 	 * Method suppresses: $class.methods.get(0).annotations.get("@SuppressWarnings").value</pre></code>
221 	 */
222 	public static final String OUT_PARAM_ANNOTATIONS = "annotations";
223 
224 	/**
225 	 * Is filled with a selectable list of public or protected class fields.
226 	 * Every list row is a map that follows the format:
227 	 * <code><pre>[
228 	 *     "type": type (signature-string),
229 	 *     "name": simple field name,
230 	 *     "value": initialization expression,
231 	 *     "comment": JavaDoc comment,
232 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
233 	 *     "field": {@link com.thoughtworks.qdox.model.JavaField},
234 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;
235 	 * ]</pre></code>
236 	 */
237 	public static final String OUT_PARAM_FIELDS = "fields";
238 
239 	/**
240 	 * Contains the same content as <code>"fields"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
241 	 */
242 	public static final String OUT_PARAM_FIELDS_MAP = "fieldsMap";
243 
244 	/**
245 	 * Is filled with a selectable list of all class fields that are not static and final.
246 	 * Every list row is a map that follows the format:
247 	 * <code><pre>[
248 	 *     "type": type (signature-string),
249 	 *     "name": simple field name,
250 	 *     "value": initialization expression,
251 	 *     "comment": JavaDoc comment,
252 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
253 	 *     "field": {@link com.thoughtworks.qdox.model.JavaField},
254 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;
255 	 * ]</pre></code>
256 	 */
257 	public static final String OUT_PARAM_DECLARED_FIELDS = "declaredFields";
258 
259 	/**
260 	 * Contains the same content as <code>"declaredFields"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
261 	 */
262 	public static final String OUT_PARAM_DECLARED_FIELDS_MAP = "declaredFieldsMap";
263 
264 	/**
265 	 * Is filled with a selectable list of public or protected class constants (static final fields).
266 	 * Every list row is a map that follows the format:
267 	 * <code><pre>[
268 	 *     "type": type (signature-string),
269 	 *     "name": simple field name,
270 	 *     "value": initialization expression,
271 	 *     "comment": JavaDoc comment,
272 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
273 	 *     "field": {@link com.thoughtworks.qdox.model.JavaField},
274 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;
275 	 * ]</pre></code>
276 	 */
277 	public static final String OUT_PARAM_CONSTANTS = "constants";
278 
279 	/**
280 	 * Contains the same content as <code>"constants"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
281 	 */
282 	public static final String OUT_PARAM_CONSTANTS_MAP = "constantsMap";
283 
284 	/**
285 	 * Is filled with a selectable list of all class constants (static final fields).
286 	 * Every list row is a map that follows the format:
287 	 * <code><pre>[
288 	 *     "type": type (signature-string),
289 	 *     "name": simple field name,
290 	 *     "value": initialization expression,
291 	 *     "comment": JavaDoc comment,
292 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
293 	 *     "field": {@link com.thoughtworks.qdox.model.JavaField},
294 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;
295 	 * ]</pre></code>
296 	 */
297 	public static final String OUT_PARAM_DECLARED_CONSTANTS = "declaredConstants";
298 
299 	/**
300 	 * Contains the same content as <code>"declaredConstants"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
301 	 */
302 	public static final String OUT_PARAM_DECLARED_CONSTANTS_MAP = "declaredConstantsMap";
303 
304 	/**
305 	 * Is filled with a selectable list of bean properties.
306 	 * Every list row is a map that follows the format:
307 	 * <code><pre>[
308 	 *     "type": property type (signature-string),
309 	 *     "name": simple property name (equals the name of the field),
310 	 *     "comment": getter or field JavaDoc comment (field comment is used when getter contains no comment),
311 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
312 	 *     "setterComment": setter JavaDoc comment,
313 	 *     "setterTags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
314 	 *     "property": {@link com.thoughtworks.qdox.model.BeanProperty},
315 	 *     "getter": {@link com.thoughtworks.qdox.model.JavaMethod},
316 	 *     "setter": {@link com.thoughtworks.qdox.model.JavaMethod},
317 	 *     "field": {@link com.thoughtworks.qdox.model.JavaField},
318 	 *     "value": field initialization expression,
319 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;
320 	 * ]</pre></code>
321 	 * <p/>
322 	 * <b>Note:</b> The annotations map contains a combined map of all annotations that were either specified on field, setter,
323 	 * or getter (using this precedence on conflicts).
324 	 */
325 	public static final String OUT_PARAM_PROPERTIES = "properties";
326 
327 	/**
328 	 * Contains the same content as <code>"properties"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
329 	 */
330 	public static final String OUT_PARAM_PROPERTIES_MAP = "propertiesMap";
331 
332 	/**
333 	 * Is filled with a selectable list of abstract or interface methods.
334 	 * Every list row is a map that follows the format:
335 	 * <code><pre>[
336 	 *     "type": return type (signature-string),
337 	 *     "name": simple method name,
338 	 *     "signature": method call signature (signature-string),
339 	 *     "comment": JavaDoc comment,
340 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
341 	 *     "method": {@link com.thoughtworks.qdox.model.JavaMethod},
342 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;,
343 	 *     "parameterAnnotations": Map&lt;String,Map&lt;String,Map&lt;String,String&gt;&gt;&gt;
344 	 * ]</pre></code>
345 	 */
346 	public static final String OUT_PARAM_INTERFACE_METHODS = "interfaceMethods";
347 
348 	/**
349 	 * Contains the same content as <code>"interfaceMethods"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
350 	 */
351 	public static final String OUT_PARAM_INTERFACE_METHODS_MAP = "interfaceMethodsMap";
352 
353 	/**
354 	 * Is filled with a selectable list of public or protected methods.
355 	 * Every list row is a map that follows the format:
356 	 * <code><pre>[
357 	 *     "type": return type (signature-string),
358 	 *     "name": simple method name,
359 	 *     "signature": method call signature (signature-string),
360 	 *     "comment": JavaDoc comment,
361 	 *     "tags": Map&lt;String,(String|Map&lt;String,String&gt;)&gt;
362 	 *     "method": {@link com.thoughtworks.qdox.model.JavaMethod},
363 	 *     "annotations": Map&lt;String,Map&lt;String,String&gt;&gt;,
364 	 *     "parameterAnnotations": Map&lt;String,Map&lt;String,Map&lt;String,String&gt;&gt;&gt;
365 	 * ]</pre></code>
366 	 */
367 	public static final String OUT_PARAM_METHODS = "methods";
368 
369 	/**
370 	 * Contains the same content as <code>"methods"</code> presented as Map&lt;String,Map&lt;?,?&gt;&gt; (mapping "rows" by name).
371 	 */
372 	public static final String OUT_PARAM_METHODS_MAP = "methodsMap";
373 
374 
375 	/**
376 	 * Defines a selector that can be used to access additional entities that relate to the loaded java class.
377 	 */
378 	public interface Selector {
379 		/**
380 		 * Returns a selectable list of all classes in the same package.
381 		 *
382 		 * @return a selectable list of all classes in the same package.
383 		 */
384 		SelectableMappedJavaEntitiesList getPackageClasses();
385 
386 		/**
387 		 * Returns a selectable list of all classes that are below or in the same package.
388 		 *
389 		 * @return a selectable list of all classes that are below or in the same package.
390 		 */
391 		SelectableMappedJavaEntitiesList getClassesBelowPackage();
392 
393 		/**
394 		 * Returns a selectable list of nested classes (inner classes).
395 		 *
396 		 * @return a selectable list of nested classes (inner classes).
397 		 */
398 		SelectableMappedJavaEntitiesList getNestedClasses();
399 
400 		/**
401 		 * Returns a selectable list of all super classes (vector up to java.lang.Object).
402 		 *
403 		 * @return a selectable list of all super classes (vector up to java.lang.Object).
404 		 */
405 		SelectableMappedJavaEntitiesList getSuperClasses();
406 
407 		/**
408 		 * Returns a selectable list of all implemented interfaces.
409 		 * <p/>
410 		 * Note: Interfaces that extend other interfaces actually "implement" them and will
411 		 * be listed in this list instead of superClasses (even if the keyword inside the code is extends).
412 		 *
413 		 * @return a selectable list of all implemented interfaces.
414 		 */
415 		SelectableMappedJavaEntitiesList getImplementedInterfaces();
416 
417 		/**
418 		 * Returns a selectable list of all known classes that derive either directly or in-directly from this class.
419 		 *
420 		 * @return a selectable list of all known classes that derive either directly or in-directly from this class.
421 		 */
422 		SelectableMappedJavaEntitiesList getDerivedClasses();
423 
424 		/**
425 		 * If this class is an annotation, it returns a selectable list of all known classes that are directly annotated
426 		 * with this class.
427 		 * An {@link IllegalStateException} will be thrown if the property is accessed on non-Annotation classes.
428 		 *
429 		 * @return a selectable list of all known classes that are directly annotated with this class..
430 		 */
431 		SelectableMappedJavaEntitiesList getAnnotatedClasses();
432 	}
433 
434 	JavaClass javaClass;
435 	String javaDocRootPath = "./apidocs/";
436 	JavaDocTagsHandler javaDocTagsHandler;
437 
438 	/**
439 	 * Constructs a new interface scanner.
440 	 *
441 	 * @param baseDir       the base dir to of the maven module.
442 	 * @param requestParams the request params of the macro call.
443 	 */
444 	public JavaSourceLoader(File baseDir, Map<String, Object> requestParams) {
445 		try {
446 			final String classOrPackageName = (String) requestParams.get(PARAM_JAVA_SOURCE);
447 			try {
448 				final JavaSource source = getSource(baseDir, classOrPackageName);
449 				init(source, requestParams);
450 			} catch (IllegalArgumentException e) {
451 				final JavaPackage javaPackage = getPackage(baseDir, classOrPackageName);
452 				if (javaPackage == null) {
453 					throw new IllegalArgumentException("'" + classOrPackageName +
454 							"' doesn't seem to be a valid package name nor is it a resolveable java source.", e);
455 				} else
456 					init(javaPackage, requestParams);
457 			}
458 		} catch (IOException e) {
459 			throw new RuntimeException(e);
460 		}
461 	}
462 
463 	/**
464 	 * Constructs a new interface scanner for the given interface or abstract class.
465 	 *
466 	 * @param baseDir   the base dir to of the maven module.
467 	 * @param className the class describing the interface.
468 	 */
469 	public JavaSourceLoader(File baseDir, String className) {
470 		this(baseDir, Collections.singletonMap(PARAM_JAVA_SOURCE, (Object) className));
471 	}
472 
473 	/**
474 	 * Constructs a new interface scanner for the given java class.
475 	 *
476 	 * @param javaClass     the java class to scan.
477 	 * @param requestParams optional request params.
478 	 */
479 	protected JavaSourceLoader(JavaClass javaClass, Map<String, Object> requestParams) {
480 		init(javaClass, requestParams);
481 	}
482 
483 	/**
484 	 * Initializes the map values.
485 	 *
486 	 * @param source        the source to scan.
487 	 * @param requestParams optional additional request params.
488 	 */
489 	protected final void init(JavaSource source, Map<String, Object> requestParams) {
490 		Globals.getLog().debug("About to load first java class of source '" + source.getURL() + "'.");
491 
492 		final JavaClass[] classes = source.getClasses();
493 		if (classes.length == 0)
494 			throw new IllegalArgumentException("The given source '" + source.getURL() + "' did not contain a single class.");
495 		JavaClass javaClass = classes[0];
496 
497 		// Support direct inner class references (using the $ delimiter).
498 		String className = requestParams == null ? null : (String) requestParams.get(PARAM_JAVA_SOURCE);
499 		if (className != null && className.contains("$")) {
500 			className = className.substring(className.indexOf('$') + 1).replace('$', '.');
501 			javaClass = javaClass.getNestedClassByName(className);
502 			if (javaClass == null)
503 				throw new IllegalArgumentException("Did not find nested class: " + className + " in " + classes[0]);
504 		}
505 
506 		init(javaClass, requestParams);
507 	}
508 
509 	/**
510 	 * Initializes the map values.
511 	 *
512 	 * @param javaClass     the java class to scan.
513 	 * @param requestParams optional additional request params.
514 	 */
515 	protected final void init(final JavaClass javaClass, Map<String, Object> requestParams) {
516 		final Log log = Globals.getLog();
517 		final boolean debug = log.isDebugEnabled();
518 		this.javaClass = javaClass;
519 
520 		Object overwrite = null;
521 
522 		if (requestParams != null) {
523 			overwrite = requestParams.get(PARAM_OVERWRITE);
524 			putAll(requestParams);
525 			String apiDocsPath = (String) requestParams.get(PARAM_API_DOCS);
526 			if (apiDocsPath != null && apiDocsPath.trim().length() != 0) javaDocRootPath = apiDocsPath;
527 		}
528 
529 		javaDocTagsHandler = new JavaDocTagsHandler(javaClass, javaDocRootPath, requestParams);
530 
531 		if (debug) log.debug("About to load reflection details on class '" + javaClass.getFullyQualifiedName() + "'");
532 
533 		scanFieldsAndConstants(javaClass);
534 		scanProperties(javaClass);
535 		scanMethods(javaClass);
536 
537 		put(OUT_PARAM_NAME, javaClass.getFullyQualifiedName());
538 		put(OUT_PARAM_SIMPLE_NAME, javaClass.getName());
539 		put(OUT_PARAM_JAVA_CLASS, javaClass);
540 		put(OUT_PARAM_COMMENT, javaDocTagsHandler.processJavaDocComment(javaClass.getComment()));
541 		put(OUT_PARAM_ANNOTATIONS, asMap(javaClass.getAnnotations()));
542 		put(OUT_PARAM_TAGS, asMap(javaClass.getTags()));
543 		put(OUT_PARAM_SELECTOR, new SelectorImplementation(javaClass, javaClass.getPackage()));
544 
545 		if (overwrite == null) putAll(requestParams);
546 
547 		PrintableMap<String, Object> selfIntrospection = new PrintableMap<String, Object>(
548 				this, "Reflection details on class '" + javaClass.getFullyQualifiedName() + '\'');
549 		put(OUT_PARAM_CLASS, selfIntrospection);
550 
551 		if (debug) log.debug("Loaded: $" + OUT_PARAM_CLASS + '=' + selfIntrospection.getContentAsString());
552 
553 		if (overwrite != null && !parseBoolean(valueOf(overwrite))) putAll(requestParams);
554 	}
555 
556 	/**
557 	 * Initializes the map values when loading a package.
558 	 *
559 	 * @param javaPackage   the package that is loaded.
560 	 * @param requestParams the request params that were specified with the macro call.
561 	 */
562 	protected final void init(JavaPackage javaPackage, Map<String, Object> requestParams) {
563 		put(OUT_PARAM_NAME, javaPackage.getName());
564 		put(OUT_PARAM_SIMPLE_NAME, javaPackage.getName());
565 		put(OUT_PARAM_ANNOTATIONS, asMap(javaPackage.getAnnotations()));
566 		put(OUT_PARAM_SELECTOR, new SelectorImplementation(null, javaPackage));
567 	}
568 
569 	/**
570 	 * Loads the given class from the module's source directory.
571 	 *
572 	 * @param baseDir   the module's base dir (containing the 'src/main/java' folder).
573 	 * @param className the name of the class.
574 	 * @return The parsed JavaSource instance.
575 	 * @throws IOException in case of the java source cannot be found or read.
576 	 */
577 	protected final JavaSource getSource(File baseDir, String className) throws IOException {
578 		className = className.replace('.', '/');
579 		if (className.contains("$"))
580 			className = className.substring(0, className.indexOf('$'));
581 
582 		if (className.endsWith("/java"))
583 			className = className.substring(0, className.length() - 5) + JAVA_EXT;
584 		else if (!className.endsWith(JAVA_EXT))
585 			className += JAVA_EXT;
586 
587 		return JavaClassLoader.getInstance().addSource(baseDir, className);
588 	}
589 
590 	/**
591 	 * Loads the given package from the module's source directory.
592 	 *
593 	 * @param baseDir     the module's base dir (containing the 'src/main/java' folder).
594 	 * @param packageName the name of the package.
595 	 * @return The parsed JavaPackage instance or 'null' if no java package was found.
596 	 * @throws IOException in case of the java source cannot be read.
597 	 */
598 	protected final JavaPackage getPackage(File baseDir, String packageName) throws IOException {
599 		JavaPackage aPackage = null;
600 		try {
601 			for (URL packageUrl : findAllSources(baseDir, packageName.replace('.', File.separatorChar))) {
602 				File packagePath = "file".equalsIgnoreCase(packageUrl.getProtocol()) ? new File(packageUrl.toURI()) : null;
603 
604 				// Note: We load all possible paths as we might end in a dead source tree if we break
605 				//       the loading loop with the first match.
606 				if (packagePath != null && packagePath.isDirectory()) {
607 					@SuppressWarnings("unchecked")
608 					final List<File> files = FileUtils.getFiles(packagePath, "**/*.java", "**/package-info.java", true);
609 					if (files.isEmpty()) continue;
610 
611 					aPackage = JavaClassLoader.getInstance().addSource(files.get(0).toURI().toURL(), false).getPackage();
612 					while (aPackage != null && aPackage.getName().startsWith(packageName) && !aPackage.getName().equals(packageName))
613 						aPackage = aPackage.getParentPackage();
614 				}
615 			}
616 		} catch (IllegalArgumentException ignore) {
617 		} catch (URISyntaxException e) {
618 			throw new RuntimeException(e);
619 		}
620 		return aPackage;
621 	}
622 
623 	/**
624 	 * Scans all relevant methods of the specified class and sets the results as map values.
625 	 *
626 	 * @param javaClass the java class to scan.
627 	 */
628 	protected void scanMethods(JavaClass javaClass) {
629 		final List<Map<?, ?>> interfaceMethods = new SelectableMappedJavaEntitiesList("method");
630 		put(OUT_PARAM_INTERFACE_METHODS, interfaceMethods);
631 		final List<Map<?, ?>> methods = new SelectableMappedJavaEntitiesList("method");
632 		put(OUT_PARAM_METHODS, methods);
633 
634 		for (JavaMethod method : javaClass.getMethods()) {
635 			boolean nonVisibleMethod = !method.isPublic() && !method.isProtected();
636 			if (nonVisibleMethod || method.isConstructor() || method.isPropertyAccessor() || method.isPropertyMutator())
637 				continue;
638 
639 			Map<String, Map> parameterAnnotations = new PrintableMap<String, Map>("Parameter-Annotations");
640 			if (method.getParameters() != null) {
641 				for (JavaParameter parameter : method.getParameters())
642 					parameterAnnotations.put(parameter.getName(), asMap(parameter.getAnnotations()));
643 			}
644 
645 			Map<?, ?> row = asMap(
646 					"type", typeToString(method.getReturnType()),
647 					"name", method.getName(),
648 					"signature", method.getCallSignature(),
649 					OUT_PARAM_COMMENT, javaDocTagsHandler.processJavaDocComment(method.getComment()),
650 					OUT_PARAM_TAGS, asMap(method.getTags()),
651 					"method", method,
652 					OUT_PARAM_ANNOTATIONS, asMap(method.getAnnotations()),
653 					"parameterAnnotations", parameterAnnotations);
654 
655 			if (method.isAbstract())
656 				interfaceMethods.add(row);
657 			else
658 				methods.add(row);
659 		}
660 
661 		put(OUT_PARAM_INTERFACE_METHODS_MAP, asMap(interfaceMethods, "name"));
662 		put(OUT_PARAM_METHODS_MAP, asMap(methods, "name"));
663 	}
664 
665 	/**
666 	 * Scans all relevant bean properties of the specified class and sets the results as map values.
667 	 *
668 	 * @param javaClass the java class to scan.
669 	 */
670 	protected void scanProperties(JavaClass javaClass) {
671 		final List<Map<?, ?>> properties = new SelectableMappedJavaEntitiesList("property");
672 		put(OUT_PARAM_PROPERTIES, properties);
673 
674 		for (BeanProperty property : javaClass.getBeanProperties()) {
675 			final JavaMethod getter = property.getAccessor();
676 			final JavaMethod setter = property.getMutator();
677 			final JavaField field = javaClass.getFieldByName(property.getName());
678 
679 			final Map<String, Map> annotations = new PrintableMap<String, Map>("Annotations");
680 			if (getter != null) annotations.putAll(asMap(getter.getAnnotations()));
681 			if (setter != null) annotations.putAll(asMap(setter.getAnnotations()));
682 			if (field != null) annotations.putAll(asMap(field.getAnnotations()));
683 
684 			final Map<?, ?> row = asMap(
685 					"type", typeToString(property.getType()),
686 					"name", property.getName(),
687 					OUT_PARAM_COMMENT, javaDocTagsHandler.processJavaDocComment(getter == null ? "" : getter.getComment() == null ?
688 					(field == null ? "" : field.getComment()) : getter.getComment()),
689 					OUT_PARAM_TAGS, getter == null ? field == null ? emptyMap() : asMap(field.getTags()) : asMap(getter.getTags()),
690 					"setterComment", javaDocTagsHandler.processJavaDocComment(setter == null ? "" : setter.getComment()),
691 					"setterTags", setter == null ? emptyMap() : asMap(setter.getTags()),
692 					"property", property,
693 					"getter", getter,
694 					"setter", setter,
695 					"field", field,
696 					"value", field == null ? "" : field.getInitializationExpression(),
697 					OUT_PARAM_ANNOTATIONS, annotations);
698 
699 			properties.add(row);
700 		}
701 
702 		put(OUT_PARAM_PROPERTIES_MAP, asMap(properties, "name"));
703 	}
704 
705 	/**
706 	 * Scans all relevant fields and constants of the specified class and sets the results as map values.
707 	 *
708 	 * @param javaClass the java class to scan.
709 	 */
710 	protected void scanFieldsAndConstants(JavaClass javaClass) {
711 		final List<Map<?, ?>> constants = new SelectableMappedJavaEntitiesList("field"),
712 				declaredConstants = new SelectableMappedJavaEntitiesList("field");
713 		put(OUT_PARAM_CONSTANTS, constants);
714 		put(OUT_PARAM_DECLARED_CONSTANTS, declaredConstants);
715 		final List<Map<?, ?>> fields = new SelectableMappedJavaEntitiesList("field"),
716 				declaredFields = new SelectableMappedJavaEntitiesList("field");
717 		put(OUT_PARAM_FIELDS, fields);
718 		put(OUT_PARAM_DECLARED_FIELDS, declaredFields);
719 
720 		boolean classIsInterface = javaClass.isInterface();
721 
722 		for (JavaField field : javaClass.getFields()) {
723 			boolean fieldIsConstant = classIsInterface || (field.isFinal() && field.isStatic());
724 
725 			Map<?, ?> row = asMap(
726 					"type", typeToString(field.getType()),
727 					"name", field.getName(),
728 					"value", field.getInitializationExpression(),
729 					OUT_PARAM_COMMENT, javaDocTagsHandler.processJavaDocComment(field.getComment()),
730 					OUT_PARAM_TAGS, asMap(field.getTags()),
731 					"field", field,
732 					OUT_PARAM_ANNOTATIONS, asMap(field.getAnnotations()));
733 
734 			(fieldIsConstant ? declaredConstants : declaredFields).add(row);
735 
736 			if (!field.isPublic() && !field.isProtected() && !classIsInterface)
737 				continue;
738 
739 			(fieldIsConstant ? constants : fields).add(row);
740 		}
741 
742 		put(OUT_PARAM_CONSTANTS_MAP, asMap(constants, "name"));
743 		put(OUT_PARAM_DECLARED_CONSTANTS_MAP, asMap(declaredConstants, "name"));
744 		put(OUT_PARAM_FIELDS_MAP, asMap(fields, "name"));
745 		put(OUT_PARAM_DECLARED_FIELDS_MAP, asMap(declaredFields, "name"));
746 	}
747 
748 	/**
749 	 * Converts the given doclet tags to a map of key->value pairs with any inline doclet tags being converted to XHTML markup.
750 	 *
751 	 * @param tags the tags to convert.
752 	 * @return a key & value
753 	 */
754 	@SuppressWarnings("unchecked")
755 	protected Map<String, Object> asMap(DocletTag... tags) {
756 		Map<String, Object> result = new PrintableMap<String, Object>("Doclet-Tags");
757 		for (DocletTag tag : tags) {
758 			Map targetMap = result;
759 			String name = tag.getName(), value = tag.getValue();
760 
761 			if ("see".equalsIgnoreCase(name)) {
762 				value = "{@link " + value + '}';
763 			} else if ("param".equalsIgnoreCase(name)) {
764 				if (tag.getParameters().length > 0) {
765 					name = tag.getParameters()[0];
766 					if (result.containsKey("param")) targetMap = (Map) result.get("param");
767 					else result.put("param", targetMap = new PrintableMap<String, Object>("Method-Params-Tags"));
768 				} else
769 					continue;
770 			}
771 
772 			targetMap.put(name, javaDocTagsHandler.processJavaDocComment(value));
773 		}
774 		return result;
775 	}
776 
777 	/**
778 	 * Converts the given type to string.
779 	 *
780 	 * @param type the type to convert.
781 	 * @return the string representation or an empty string.
782 	 */
783 	protected static String typeToString(Type type) {
784 		return type == null ? "" : type.toGenericString();
785 	}
786 
787 	/**
788 	 * Converts the given set of annotations to a map, mapping the simple name of the annotation
789 	 * against a key=>value map of the specified parameters (the simple name starts with '@').
790 	 *
791 	 * @param annotations the annotations to convert to a map.
792 	 * @return a map mapping the simple name of the annotation to its parameters.
793 	 */
794 	@SuppressWarnings("unchecked")
795 	protected static Map<String, Map> asMap(Annotation... annotations) {
796 		Map<String, Map> result = new PrintableMap<String, Map>("Annotations");
797 		for (Annotation annotation : annotations) {
798 			String name = annotation.getType().getFullyQualifiedName();
799 			name = "@".concat(name.substring(name.lastIndexOf('.') + 1));
800 
801 			Map<Object, Object> map = new PrintableMap<Object, Object>("Annotation-Parameters");
802 			map.putAll(annotation.getNamedParameterMap());
803 			for (Map.Entry<Object, Object> entry : map.entrySet())
804 				if (entry.getValue() != null)
805 					entry.setValue(entry.getValue().toString().trim());
806 
807 			result.put(name, map);
808 		}
809 
810 		return result;
811 	}
812 
813 	/**
814 	 * Converts the given key and value pairs to a Map that is linked by key and index (=> associative array).
815 	 *
816 	 * @param keyValuePairs the key and value pairs to convert.
817 	 * @return a Map that is linked by key and index.
818 	 */
819 	protected static Map<Object, Object> asMap(Object... keyValuePairs) {
820 		if (keyValuePairs.length % 2 != 0)
821 			throw new IllegalArgumentException("The key and value input array length must be a multiple of 2.");
822 
823 		final Map<Object, Object> map = new PrintableMap<Object, Object>("Key-Value-Pairs"),
824 				assocMap = new LinkedHashMap<Object, Object>(keyValuePairs.length / 2);
825 
826 		for (int i = 0; i < keyValuePairs.length; i += 2) {
827 			Object key = valueOf(keyValuePairs[i]), value = keyValuePairs[i + 1];
828 			map.put(i / 2, value);
829 			assocMap.put(key, value);
830 		}
831 
832 		map.putAll(assocMap);
833 
834 		return map;
835 	}
836 
837 	/**
838 	 * Converts the given named rows to a map by name.
839 	 *
840 	 * @param namedRows the named rows to map.
841 	 * @param nameKey   the key to use to extract the name.
842 	 * @return the mapped rows.
843 	 */
844 	protected static Map<String, Map<?, ?>> asMap(Iterable<Map<?, ?>> namedRows, String nameKey) {
845 		final Map<String, Map<?, ?>> map = new PrintableMap<String, Map<?, ?>>("Named-Class-Elements");
846 		for (Map<?, ?> namedRow : namedRows)
847 			map.put((String) namedRow.get(nameKey), namedRow);
848 
849 		return map;
850 	}
851 
852 	private class SelectorImplementation implements Selector {
853 
854 		private final JavaClass javaClass;
855 		private final JavaPackage javaPackage;
856 
857 		SelectorImplementation(JavaClass javaClass, JavaPackage javaPackage) {
858 			this.javaClass = javaClass;
859 			this.javaPackage = javaPackage;
860 		}
861 
862 		Map<String, Object> extractRequestParams() {
863 			Map<String, Object> requestParams = new HashMap<String, Object>();
864 			for (Map.Entry<String, Object> entry : entrySet()) {
865 				if (entry.getKey().startsWith(PARAM_API_DOCS))
866 					requestParams.put(entry.getKey(), entry.getValue());
867 			}
868 			return requestParams;
869 		}
870 
871 		List<Map<String, ?>> convert(Collection<JavaClass> classes) {
872 			return convert(classes.toArray(new JavaClass[classes.size()]));
873 		}
874 
875 		List<Map<String, ?>> convert(JavaClass[] classes) {
876 			Map<String, Object> requestParams = extractRequestParams();
877 			final List<Map<String, ?>> sourceLoaders = new ArrayList<Map<String, ?>>(classes.length);
878 			for (JavaClass aClass : classes)
879 				sourceLoaders.add(new PrintableMap<String, Object>(new JavaSourceLoader(aClass, requestParams), aClass.toString()));
880 			return sourceLoaders;
881 		}
882 
883 		public SelectableMappedJavaEntitiesList getPackageClasses() {
884 			final JavaClass[] classes = javaPackage == null ? new JavaClass[0] : javaPackage.getClasses();
885 			return new SelectableMappedJavaEntitiesList(convert(classes), OUT_PARAM_JAVA_CLASS);
886 		}
887 
888 		public SelectableMappedJavaEntitiesList getClassesBelowPackage() {
889 			final List<JavaClass> classes = new ArrayList<JavaClass>();
890 			if (javaPackage == null)
891 				classes.addAll(JavaClassLoader.getInstance().getClasses());
892 			else {
893 				String prefix = javaPackage.getName() + '.';
894 				for (JavaPackage candidate : JavaClassLoader.getInstance().getPackages()) {
895 					if (candidate.equals(javaPackage) || candidate.getName().startsWith(prefix))
896 						classes.addAll(Arrays.asList(candidate.getClasses()));
897 				}
898 			}
899 
900 			return new SelectableMappedJavaEntitiesList(convert(classes), OUT_PARAM_JAVA_CLASS);
901 		}
902 
903 		public SelectableMappedJavaEntitiesList getNestedClasses() {
904 			final JavaClass[] nestedClasses = javaClass == null ? new JavaClass[0] : javaClass.getNestedClasses();
905 			return new SelectableMappedJavaEntitiesList(convert(nestedClasses), OUT_PARAM_JAVA_CLASS);
906 		}
907 
908 		public SelectableMappedJavaEntitiesList getSuperClasses() {
909 			final List<JavaClass> knownSuperClasses = new ArrayList<JavaClass>();
910 			if (javaClass != null) {
911 				for (JavaClass superClass = javaClass.getSuperJavaClass(); superClass != null; superClass = superClass.getSuperJavaClass())
912 					knownSuperClasses.add(superClass);
913 			}
914 
915 			final List<Map<String, ?>> mapList = convert(knownSuperClasses.toArray(new JavaClass[knownSuperClasses.size()]));
916 			return new SelectableMappedJavaEntitiesList(mapList, OUT_PARAM_JAVA_CLASS);
917 		}
918 
919 		public SelectableMappedJavaEntitiesList getImplementedInterfaces() {
920 			final JavaClass[] interfaces = javaClass == null ? new JavaClass[0] : javaClass.getImplementedInterfaces();
921 			return new SelectableMappedJavaEntitiesList(convert(interfaces), OUT_PARAM_JAVA_CLASS);
922 		}
923 
924 		public SelectableMappedJavaEntitiesList getDerivedClasses() {
925 			final JavaClass[] classes = javaClass == null ? new JavaClass[0] : javaClass.getDerivedClasses();
926 			return new SelectableMappedJavaEntitiesList(convert(classes), OUT_PARAM_JAVA_CLASS);
927 		}
928 
929 		public SelectableMappedJavaEntitiesList getAnnotatedClasses() {
930 			if (!SelectableMappedJavaEntitiesList.isAnnotation(javaClass)) {
931 				throw new IllegalStateException("Cannot look after classes that are annotated with '" + javaClass +
932 						"' as this class is no annotation.");
933 			}
934 
935 			final SortedSet<JavaClass> allClasses = JavaClassLoader.getInstance().getClasses();
936 			final SelectableJavaEntitiesList<JavaClass> entitiesList = new SelectableJavaEntitiesList<JavaClass>(allClasses);
937 			final List<Map<String, ?>> converted = convert(entitiesList.selectAnnotated('@' + javaClass.getFullyQualifiedName()));
938 
939 			return new SelectableMappedJavaEntitiesList(converted, OUT_PARAM_JAVA_CLASS);
940 		}
941 	}
942 }