001: // Copyright 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.mojo;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.util.Collections;
023: import java.util.List;
024: import java.util.Locale;
025: import java.util.Map;
026: import java.util.StringTokenizer;
027:
028: import nu.xom.Builder;
029: import nu.xom.Document;
030: import nu.xom.Element;
031: import nu.xom.Elements;
032:
033: import org.apache.commons.lang.SystemUtils;
034: import org.apache.maven.artifact.Artifact;
035: import org.apache.maven.artifact.repository.ArtifactRepository;
036: import org.apache.maven.project.MavenProject;
037: import org.apache.maven.reporting.AbstractMavenReport;
038: import org.apache.maven.reporting.MavenReportException;
039: import org.apache.tapestry.ioc.IOCUtilities;
040: import org.apache.tapestry.ioc.internal.util.InternalUtils;
041: import org.codehaus.doxia.sink.Sink;
042: import org.codehaus.doxia.site.renderer.SiteRenderer;
043: import org.codehaus.plexus.util.StringUtils;
044: import org.codehaus.plexus.util.cli.CommandLineException;
045: import org.codehaus.plexus.util.cli.CommandLineUtils;
046: import org.codehaus.plexus.util.cli.Commandline;
047: import org.codehaus.plexus.util.cli.DefaultConsumer;
048:
049: /**
050: * The component report generates documentation about components and parameters within the current
051: * project.
052: *
053: * @goal component-report
054: * @requiresDependencyResolution compile
055: * @execute phase="generate-sources"
056: */
057: public class ComponentReport extends AbstractMavenReport {
058: /**
059: * Identifies the application root package.
060: *
061: * @parameter
062: * @required
063: */
064: private String rootPackage;
065:
066: /**
067: * The Maven Project Object
068: *
069: * @parameter expression="${project}"
070: * @required
071: * @readonly
072: */
073: private MavenProject project;
074:
075: /**
076: * Generates the site report
077: *
078: * @component
079: */
080: private SiteRenderer siteRenderer;
081:
082: /**
083: * Location of the generated site.
084: *
085: * @parameter default-value="${project.reporting.outputDirectory}"
086: * @required
087: */
088: private String outputDirectory;
089:
090: /**
091: * Working directory for temporary files.
092: *
093: * @parameter default-value="target"
094: * @required
095: */
096: private String workDirectory;
097:
098: protected String getOutputDirectory() {
099: return outputDirectory;
100: }
101:
102: protected MavenProject getProject() {
103: return project;
104: }
105:
106: protected SiteRenderer getSiteRenderer() {
107: return siteRenderer;
108: }
109:
110: public String getDescription(Locale locale) {
111: return "Tapestry component parameter reference documentation";
112: }
113:
114: public String getName(Locale locale) {
115: return "Component Reference";
116: }
117:
118: public String getOutputName() {
119: return "component-parameters";
120: }
121:
122: @Override
123: protected void executeReport(Locale locale)
124: throws MavenReportException {
125: Map<String, ClassDescription> descriptions = runJavadoc();
126:
127: getLog().info("Executing ComponentReport ...");
128:
129: Sink sink = getSink();
130:
131: sink.section1();
132: sink.sectionTitle1();
133: sink.text("Component Index");
134: sink.sectionTitle1_();
135: sink.list();
136:
137: for (String className : InternalUtils.sortedKeys(descriptions)) {
138: String simpleName = InternalUtils.lastTerm(className);
139:
140: sink.listItem();
141:
142: // Something is convertin the name attribute of the anchors to lower case, so
143: // we'll follow suit.
144:
145: sink.link("#" + className.toLowerCase());
146: sink.text(simpleName);
147: sink.link_();
148:
149: sink.listItem_();
150: }
151:
152: sink.list_();
153:
154: for (String className : InternalUtils.sortedKeys(descriptions)) {
155: writeClassDescription(descriptions, sink, className);
156: }
157:
158: }
159:
160: private void writeClassDescription(
161: Map<String, ClassDescription> descriptions, Sink sink,
162: String className) {
163: ClassDescription cd = descriptions.get(className);
164:
165: Map<String, ParameterDescription> parameters = newMap(cd
166: .getParameters());
167: List<String> parents = newList();
168:
169: String current = cd.getSuperClassName();
170:
171: while (true) {
172: ClassDescription super Description = descriptions
173: .get(current);
174:
175: if (super Description == null)
176: break;
177:
178: parents.add(current);
179: parameters.putAll(super Description.getParameters());
180:
181: current = super Description.getSuperClassName();
182: }
183:
184: Collections.reverse(parents);
185:
186: sink.section2();
187:
188: sink.sectionTitle2();
189: sink.anchor(className);
190: sink.text(className);
191: sink.anchor_();
192:
193: sink.sectionTitle2_();
194:
195: sink.paragraph();
196: sink.text(cd.getDescription());
197: sink.paragraph_();
198:
199: sink.paragraph();
200:
201: String javadocURL = String.format("apidocs/%s.html", className
202: .replace('.', '/'));
203:
204: sink.link(javadocURL);
205: sink.text("[JavaDoc]");
206: sink.link_();
207:
208: sink.paragraph_();
209:
210: if (!parents.isEmpty()) {
211: sink.sectionTitle3();
212: sink.text("Component inheritance");
213: sink.sectionTitle3_();
214:
215: sink.list();
216: sink.listItem();
217:
218: for (String name : parents) {
219: sink.link("#" + name.toLowerCase());
220: sink.text(name);
221: sink.link_();
222:
223: sink.list();
224: sink.listItem();
225: }
226:
227: sink.text(className);
228:
229: for (int i = 0; i <= parents.size(); i++) {
230: sink.listItem_();
231: sink.list_();
232: }
233: }
234:
235: if (!parameters.isEmpty()) {
236: List<String> flags = newList();
237:
238: sink.sectionTitle3();
239: sink.text("Parameters");
240: sink.sectionTitle3_();
241:
242: sink.table();
243: sink.tableRow();
244:
245: for (String header : PARAMETER_HEADERS) {
246: sink.tableHeaderCell();
247: sink.text(header);
248: sink.tableHeaderCell_();
249: }
250:
251: sink.tableRow_();
252:
253: for (String name : InternalUtils.sortedKeys(parameters)) {
254: ParameterDescription pd = parameters.get(name);
255:
256: flags.clear();
257: if (pd.getRequired())
258: flags.add("Required");
259:
260: if (!pd.getCache())
261: flags.add("NOT Cached");
262:
263: sink.tableRow();
264:
265: cell(sink, pd.getName());
266: cell(sink, pd.getType());
267: cell(sink, InternalUtils.join(flags));
268: cell(sink, pd.getDefaultValue());
269: cell(sink, pd.getDefaultPrefix());
270: cell(sink, pd.getDescription());
271:
272: sink.tableRow_();
273:
274: }
275:
276: sink.table_();
277: }
278:
279: sink.section2_();
280: }
281:
282: private void cell(Sink sink, String value) {
283: sink.tableCell();
284: sink.text(value);
285: sink.tableCell_();
286: }
287:
288: private final static String[] PARAMETER_HEADERS = { "Name", "Type",
289: "Flags", "Default", "Default Prefix", "Description" };
290:
291: private Map<String, ClassDescription> runJavadoc()
292: throws MavenReportException {
293: getLog()
294: .info(
295: "Running JavaDoc to collection component parameter data ...");
296:
297: Commandline command = new Commandline();
298:
299: try {
300: command.setExecutable(pathToJavadoc());
301: } catch (IOException ex) {
302: throw new MavenReportException(
303: "Unable to locate javadoc command: "
304: + ex.getMessage(), ex);
305: }
306:
307: String parametersPath = workDirectory + File.separator
308: + "component-parameters.xml";
309:
310: String[] arguments = { "-private", "-o", parametersPath,
311:
312: "-subpackages", rootPackage,
313:
314: "-doclet", ParametersDoclet.class.getName(),
315:
316: "-docletpath", docletPath(),
317:
318: "-sourcepath", sourcePath(),
319:
320: "-classpath", classPath() };
321:
322: command.addArguments(arguments);
323:
324: executeCommand(command);
325:
326: return readXML(parametersPath);
327: }
328:
329: @SuppressWarnings("unchecked")
330: private String sourcePath() {
331: List<String> roots = (List<String>) project
332: .getCompileSourceRoots();
333:
334: return toArgumentPath(roots);
335: }
336:
337: /**
338: * Needed to help locate this plugin's local JAR file for the -doclet argument.
339: *
340: * @parameter default-value="${localRepository}"
341: * @read-only
342: */
343: private ArtifactRepository localRepository;
344:
345: /**
346: * Needed to help locate this plugin's local JAR file for the -doclet argument.
347: *
348: * @parameter default-value="${plugin.groupId}"
349: * @read-only
350: */
351: private String pluginGroupId;
352:
353: /**
354: * Needed to help locate this plugin's local JAR file for the -doclet argument.
355: *
356: * @parameter default-value="${plugin.artifactId}"
357: * @read-only
358: */
359: private String pluginArtifactId;
360:
361: /**
362: * Needed to help locate this plugin's local JAR file for the -doclet argument.
363: *
364: * @parameter default-value="${plugin.version}"
365: * @read-only
366: */
367: private String pluginVersion;
368:
369: @SuppressWarnings("unchecked")
370: private String docletPath() throws MavenReportException {
371: File file = new File(localRepository.getBasedir());
372:
373: for (String term : pluginGroupId.split("\\."))
374: file = new File(file, term);
375:
376: file = new File(file, pluginArtifactId);
377: file = new File(file, pluginVersion);
378:
379: file = new File(file, String.format("%s-%s.jar",
380: pluginArtifactId, pluginVersion));
381:
382: return file.getAbsolutePath();
383: }
384:
385: @SuppressWarnings("unchecked")
386: private String classPath() throws MavenReportException {
387: List<Artifact> artifacts = (List<Artifact>) project
388: .getCompileArtifacts();
389:
390: return artifactsToArgumentPath(artifacts);
391: }
392:
393: private String artifactsToArgumentPath(List<Artifact> artifacts)
394: throws MavenReportException {
395: List<String> paths = newList();
396:
397: for (Artifact artifact : artifacts) {
398: if (artifact.getScope().equals("test"))
399: continue;
400:
401: File file = artifact.getFile();
402:
403: if (file == null)
404: throw new MavenReportException(
405: "Unable to execute Javadoc: compile dependencies are not fully resolved.");
406:
407: paths.add(file.getAbsolutePath());
408: }
409:
410: return toArgumentPath(paths);
411: }
412:
413: private void executeCommand(Commandline command)
414: throws MavenReportException {
415: getLog().debug(command.toString());
416:
417: CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
418:
419: try {
420: int exitCode = CommandLineUtils.executeCommandLine(command,
421: new DefaultConsumer(), err);
422:
423: if (exitCode != 0) {
424: String message = String
425: .format(
426: "Javadoc exit code: %d - %s\nCommand line was: %s",
427: exitCode, err.getOutput(), command);
428:
429: throw new MavenReportException(message);
430: }
431: } catch (CommandLineException ex) {
432: throw new MavenReportException(
433: "Unable to execute javadoc command: "
434: + ex.getMessage(), ex);
435: }
436:
437: // ----------------------------------------------------------------------
438: // Handle Javadoc warnings
439: // ----------------------------------------------------------------------
440:
441: if (StringUtils.isNotEmpty(err.getOutput())) {
442: getLog().info("Javadoc Warnings");
443:
444: StringTokenizer token = new StringTokenizer(
445: err.getOutput(), "\n");
446: while (token.hasMoreTokens()) {
447: String current = token.nextToken().trim();
448:
449: getLog().warn(current);
450: }
451: }
452: }
453:
454: private String pathToJavadoc() throws IOException,
455: MavenReportException {
456: String executableName = SystemUtils.IS_OS_WINDOWS ? "javadoc.exe"
457: : "javadoc";
458:
459: File executable = initialGuessAtJavadocFile(executableName);
460:
461: if (!executable.exists() || !executable.isFile())
462: throw new MavenReportException(String.format(
463: "Path %s does not exist or is not a file.",
464: executable));
465:
466: return executable.getAbsolutePath();
467: }
468:
469: private File initialGuessAtJavadocFile(String executableName) {
470: if (SystemUtils.IS_OS_MAC_OSX)
471: return new File(SystemUtils.getJavaHome() + File.separator
472: + "bin", executableName);
473:
474: return new File(SystemUtils.getJavaHome() + File.separator
475: + ".." + File.separator + "bin", executableName);
476: }
477:
478: private String toArgumentPath(List<String> paths) {
479: StringBuilder builder = new StringBuilder();
480:
481: String sep = "";
482:
483: for (String path : paths) {
484: builder.append(sep);
485: builder.append(path);
486:
487: sep = SystemUtils.PATH_SEPARATOR;
488: }
489:
490: return builder.toString();
491: }
492:
493: public Map<String, ClassDescription> readXML(String path)
494: throws MavenReportException {
495: try {
496: Builder builder = new Builder(false);
497:
498: File input = new File(path);
499:
500: Document doc = builder.build(input);
501:
502: return buildMapFromDocument(doc);
503: } catch (Exception ex) {
504: throw new MavenReportException(String.format(
505: "Failure reading from %s: %s", path, ex
506: .getMessage()), ex);
507: }
508: }
509:
510: private Map<String, ClassDescription> buildMapFromDocument(
511: Document doc) {
512: Map<String, ClassDescription> result = newMap();
513:
514: Elements elements = doc.getRootElement().getChildElements(
515: "class");
516:
517: for (int i = 0; i < elements.size(); i++) {
518: Element element = elements.get(i);
519:
520: String description = element.getFirstChildElement(
521: "description").getValue();
522:
523: String className = element.getAttributeValue("name");
524: String super ClassName = element
525: .getAttributeValue("super-class");
526:
527: ClassDescription cd = new ClassDescription(className,
528: super ClassName, description);
529:
530: result.put(className, cd);
531:
532: readParameters(cd, element);
533: }
534:
535: return result;
536: }
537:
538: private void readParameters(ClassDescription cd,
539: Element classElement) {
540: Elements elements = classElement.getChildElements("parameter");
541:
542: for (int i = 0; i < elements.size(); i++) {
543: Element node = elements.get(i);
544:
545: String name = node.getAttributeValue("name");
546: String type = node.getAttributeValue("type");
547:
548: int dotx = type.lastIndexOf('.');
549: if (dotx > 0 && type.substring(0, dotx).equals("java.lang"))
550: type = type.substring(dotx + 1);
551:
552: String defaultValue = node.getAttributeValue("default");
553: boolean required = Boolean.parseBoolean(node
554: .getAttributeValue("required"));
555: boolean cache = Boolean.parseBoolean(node
556: .getAttributeValue("cache"));
557: String defaultPrefix = node
558: .getAttributeValue("default-prefix");
559: String description = node.getValue();
560:
561: ParameterDescription pd = new ParameterDescription(name,
562: type, defaultValue, defaultPrefix, required, cache,
563: description);
564:
565: cd.getParameters().put(name, pd);
566: }
567: }
568: }
|