changeset 955:9a0cd2dcf73c

Add tools and templates to generate plugin documentation Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-January/005434.html PR 1255
author Omair Majid <omajid@redhat.com>
date Thu, 07 Feb 2013 12:05:12 -0500
parents 7c0a72c7c2c9
children 539c0e95b660
files annotations/pom.xml annotations/src/main/java/com/redhat/thermostat/annotations/internal/AnnotationProcessor.java annotations/src/main/java/com/redhat/thermostat/annotations/internal/JavadocToXmlConverter.java annotations/src/main/java/com/redhat/thermostat/annotations/internal/PluginAnnotationProcessor.java annotations/src/main/java/com/redhat/thermostat/annotations/internal/SelfAnnotationProcessor.java annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor annotations/src/test/java/com/redhat/thermostat/annotations/internal/AnnotationProcessorTest.java annotations/src/test/java/com/redhat/thermostat/annotations/internal/JavadocToXmlConverterTest.java distribution/tools/MergePluginDocs.java distribution/tools/plugin-docs-html.xslt
diffstat 10 files changed, 699 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/annotations/pom.xml	Wed Feb 06 11:40:45 2013 -0500
+++ b/annotations/pom.xml	Thu Feb 07 12:05:12 2013 -0500
@@ -76,12 +76,34 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
-        <configuration>
-          <!-- dont try to run the annotation process on this module
-               it will pick up the service file and try to run the (not yet
-               compiled) annotation processor and fail mysteriously -->
-          <compilerArgument>-proc:none</compilerArgument>
-        </configuration>
+        <!-- compile in 2 steps -->
+        <executions>
+          <execution>
+            <!-- Step 1: only compile annotation processors and do not run any annotation processors -->
+            <id>default-compile</id>
+            <configuration>
+              <compilerArgument>-proc:none</compilerArgument>
+              <includes>
+                <include>**/internal/**</include>
+              </includes>
+            </configuration>
+          </execution>
+          <execution>
+            <!-- Step 2: after compiling annotation processors, compile everything else using them -->
+            <id>compile-everything-else</id>
+            <phase>compile</phase>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+            <configuration>
+              <compilerArguments>
+                <!-- only use the self annotation processor here instead of the PluginAnnotationProcessor
+                     only meta-docs for the annotations should be generated -->
+                <processor>com.redhat.thermostat.annotations.internal.SelfAnnotationProcessor</processor>
+              </compilerArguments>
+            </configuration>
+          </execution>
+        </executions>
       </plugin>
 
     </plugins>
--- a/annotations/src/main/java/com/redhat/thermostat/annotations/internal/AnnotationProcessor.java	Wed Feb 06 11:40:45 2013 -0500
+++ b/annotations/src/main/java/com/redhat/thermostat/annotations/internal/AnnotationProcessor.java	Thu Feb 07 12:05:12 2013 -0500
@@ -47,9 +47,6 @@
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
-import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
-import javax.lang.model.SourceVersion;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.TypeElement;
 import javax.tools.Diagnostic.Kind;
@@ -57,15 +54,29 @@
 import javax.tools.StandardLocation;
 
 /**
- * An annotation processor that runs and finds @Service and @ExtensionPoint
- * annotations. A list  of classes using these annotations are written to
- * a <code>META-INF/thermostat/plugin-docs.xml<code> file.
+ * An annotation processor that runs and finds {@code @Service} and
+ * {@code @ExtensionPoint} annotations. A list of classes using these
+ * annotations are written to a
+ * {@code META-INF/thermostat/plugin-docs.xml} file.
  */
-@SupportedAnnotationTypes("com.redhat.thermostat.*")
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
 public class AnnotationProcessor extends AbstractProcessor {
 
-    private enum ExposedAs { EXTENSION_POINT, SERVICE }
+    private enum ExposedAs {
+        META("meta"),
+        EXTENSION_POINT("extension-point"),
+        SERVICE("service"),
+        ;
+
+        private String elementName;
+
+        private ExposedAs(String elementName) {
+            this.elementName = elementName;
+        }
+
+        private String getElementName() {
+            return elementName;
+        }
+    }
 
     private boolean firstRound = false;
 
@@ -120,6 +131,8 @@
                 exposedType = ExposedAs.SERVICE;
             } else if (annotation.getSimpleName().toString().contains("ExtensionPoint")) {
                 exposedType = ExposedAs.EXTENSION_POINT;
+            } else if (annotation.getSimpleName().toString().contains("Documented")) {
+                exposedType = ExposedAs.META;
             } else {
                 processingEnv.getMessager().printMessage(Kind.WARNING, "Unrecognized annotation: " + annotation.getSimpleName());
                 continue;
@@ -135,22 +148,27 @@
     }
 
     private void writeXml(PrintWriter writer, List<PluginPointInformation> points) {
-        writer.println("<?xml?>");
+        JavadocToXmlConverter converter = new JavadocToXmlConverter();
+
+        writer.println("<?xml version=\"1.0\"?>");
 
         writer.println("<!-- autogenerated by " + this.getClass().getName() + " -->");
 
+        writer.println("  <plugin-docs>");
         for (PluginPointInformation info: points) {
-            String tag = info.exposedAs == ExposedAs.SERVICE ? "service" : "extension-point";
+            String tag = info.exposedAs.getElementName();
 
-            writer.println("  <" + tag + ">");
-            writer.println("    <name>" + info.annotatedClass.getQualifiedName() + "</name>");
+            writer.println("    <" + tag + ">");
+            writer.println("      <name>" + info.annotatedClass.getQualifiedName() + "</name>");
             if (info.javadoc != null) {
-                writer.println("    <doc><![CDATA[");
-                writer.println(info.javadoc);
-                writer.println("]]></doc>");
+                writer.println("      <doc>");
+                writer.println(converter.convert(info.javadoc));
+                writer.println("      </doc>");
             }
-            writer.println("  </" + tag + ">");
+            writer.println("    </" + tag + ">");
         }
+
+        writer.println("  </plugin-docs>");
     }
 
     private static class PluginPointInformation {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annotations/src/main/java/com/redhat/thermostat/annotations/internal/JavadocToXmlConverter.java	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.annotations.internal;
+
+/**
+ * Converts javadoc comments into valid html-like xml.
+ * <p>
+ * Used to generate documentation.
+ */
+class JavadocToXmlConverter {
+
+    // TODO get rid of this once we use JEP 106
+
+    /**
+     * Perform an ad-hoc set of conversions to convert the javadoc input string
+     * into something that is valid xml and can be used in html documents
+     * safely.
+     */
+    public String convert(String input) {
+        String result = input;
+
+        // @see Foo -> See Also: <code>Foo</code>
+        result = result.replaceAll("@see (.+?)(\n|$)", "See Also: <code>$1</code><p>\n");
+
+        // <p> is valid html but bad xml (without a </p>)
+        // convert it to 2 <br />'s so we get one blank line
+        result = result.replace("<p>", "<br /> <br />");
+        result = result.replace("</p>", "");
+
+        // {@code foobar} -> <code>foobar</code>
+        result = result.replaceAll("\\{@code (.*?)\\}", "<code>$1</code>");
+
+        // {@link foobar} -> <code>foobar</code>
+        result = result.replaceAll("\\{@link (.*?)\\}", "<code>$1</code>");
+
+        // Foo#bar(Baz) -> Foo.bar(Baz)
+        result = result.replaceAll("(\\w+)#(\\w+)", "$1.$2");
+
+        // <h1>Foo</h1> -> <h4>Foo</h4>
+        int offset = 4;
+        for (int i = 1; i < offset; i++) {
+            result = convertHeadingLevel(result, i, i + offset);
+        }
+
+
+        return result;
+    }
+
+    private String convertHeadingLevel(String replace, int source, int target) {
+        return replace
+                .replaceAll("<h" + source + ">", "<h" + target + ">")
+                .replaceAll("</h" + source + ">", "</h" + target + ">");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annotations/src/main/java/com/redhat/thermostat/annotations/internal/PluginAnnotationProcessor.java	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.annotations.internal;
+
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+
+/**
+ * An annotation processor that runs and finds {@code @Service} and
+ * {@code @ExtensionPoint} annotations. A list of classes using these
+ * annotations are written to a
+ * {@code META-INF/thermostat/plugin-docs.xml} file.
+ */
+@SupportedAnnotationTypes("com.redhat.thermostat.*")
+@SupportedSourceVersion(SourceVersion.RELEASE_7)
+public class PluginAnnotationProcessor extends AnnotationProcessor {
+
+    /*
+     * Inherit all the behaviour of AnnotationProcessor
+     *
+     * The SupportedAnnotationType annotation on this class ensures it will only
+     * execute on Service and ExtensionPoint annotations
+     */
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annotations/src/main/java/com/redhat/thermostat/annotations/internal/SelfAnnotationProcessor.java	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.annotations.internal;
+
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+
+/**
+ * An annotation processor that runs and finds {@code Documented} annotation. A
+ * list of classes using these
+ * annotations are written to a {@code META-INF/thermostat/plugin-docs.xml}
+ * file.
+ */
+@SupportedAnnotationTypes("java.lang.annotation.Documented")
+@SupportedSourceVersion(SourceVersion.RELEASE_7)
+public class SelfAnnotationProcessor extends AnnotationProcessor {
+
+    /*
+     * Inherit all the behaviour of AnnotationProcessor
+     *
+     * The SupportedAnnotationType annotation on this class ensures it will only
+     * execute on Documented annotations
+     */
+
+}
+
--- a/annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor	Wed Feb 06 11:40:45 2013 -0500
+++ b/annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor	Thu Feb 07 12:05:12 2013 -0500
@@ -1,2 +1,3 @@
-# This jar provides the following annotation processors:
-com.redhat.thermostat.annotations.internal.AnnotationProcessor
+# This jar provides the following annotation processors
+# these are for use by jars that depend on this jar
+com.redhat.thermostat.annotations.internal.PluginAnnotationProcessor
\ No newline at end of file
--- a/annotations/src/test/java/com/redhat/thermostat/annotations/internal/AnnotationProcessorTest.java	Wed Feb 06 11:40:45 2013 -0500
+++ b/annotations/src/test/java/com/redhat/thermostat/annotations/internal/AnnotationProcessorTest.java	Thu Feb 07 12:05:12 2013 -0500
@@ -143,14 +143,16 @@
         processor.process(annotations, roundEnv);
 
         String actualFileContents = out.toString("UTF-8");
-        String expectedFileContents = "<?xml?>\n"
+        String expectedFileContents = "<?xml version=\"1.0\"?>\n"
         		+ AUTO_GENERATED_COMMENT + "\n"
-                + "  <service>\n"
-                + "    <name>" + CLASS_NAME + "</name>\n"
-                + "    <doc><![CDATA[\n"
+        		+ "  <plugin-docs>\n"
+                + "    <service>\n"
+                + "      <name>" + CLASS_NAME + "</name>\n"
+                + "      <doc>\n"
                 + JAVADOC + "\n"
-                + "]]></doc>\n"
-                + "  </service>\n"
+                + "      </doc>\n"
+                + "    </service>\n"
+                + "  </plugin-docs>\n"
                 + "";
 
         assertEquals(expectedFileContents, actualFileContents);
@@ -189,14 +191,16 @@
         processor.process(annotations, roundEnv);
 
         String actualFileContents = out.toString("UTF-8");
-        String expectedFileContents = "<?xml?>\n"
+        String expectedFileContents = "<?xml version=\"1.0\"?>\n"
         		+ AUTO_GENERATED_COMMENT + "\n"
-                + "  <extension-point>\n"
-                + "    <name>" + CLASS_NAME + "</name>\n"
-                + "    <doc><![CDATA[\n"
+        		+ "  <plugin-docs>\n"
+                + "    <extension-point>\n"
+                + "      <name>" + CLASS_NAME + "</name>\n"
+                + "      <doc>\n"
                 + JAVADOC + "\n"
-                + "]]></doc>\n"
-                + "  </extension-point>\n"
+                + "      </doc>\n"
+                + "    </extension-point>\n"
+                + "  </plugin-docs>\n"
                 + "";
 
         assertEquals(expectedFileContents, actualFileContents);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/annotations/src/test/java/com/redhat/thermostat/annotations/internal/JavadocToXmlConverterTest.java	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.annotations.internal;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class JavadocToXmlConverterTest {
+
+    private JavadocToXmlConverter converter;
+
+    @Before
+    public void setUp() {
+        converter = new JavadocToXmlConverter();
+    }
+
+    @Test
+    public void testParagraphs() {
+        String input = "" +
+                "Foo bar baz\n" +
+                "<p>\n" +
+                "frob\n";
+
+        String expected = "" +
+                "Foo bar baz\n" +
+                "<br /> <br />\n" +
+                "frob\n";
+
+        assertEquals(expected, converter.convert(input));
+    }
+
+    @Test
+    public void testCodeSegments() {
+        String input = "" +
+                "Foo bar baz\n" +
+                "<p>\n" +
+                "frob\n";
+
+        String expected = "" +
+                "Foo bar baz\n" +
+                "<br /> <br />\n" +
+                "frob\n";
+
+        assertEquals(expected, converter.convert(input));
+    }
+
+    @Test
+    public void testHeadingConversion() {
+        String input = "" +
+                "<h1>test</h1>\n" +
+                "frob\n";
+
+        String expected = "" +
+                "<h5>test</h5>\n" +
+                "frob\n";
+
+        assertEquals(expected, converter.convert(input));
+    }
+
+    @Test
+    public void testSeeAlsoSections() {
+        String input = "" +
+                " foo\n" +
+                " @see Foo#bar(baz)\n" +
+                " @see Spam#EGGS";
+
+        String expected = "" +
+                " foo\n" +
+                " See Also: <code>Foo.bar(baz)</code><br /> <br />\n" +
+                " See Also: <code>Spam.EGGS</code><br /> <br />\n";
+
+        assertEquals(expected, converter.convert(input));
+    }
+
+    @Test
+    public void testComplexJavadoc() {
+        String input = "" +
+                " foo \n" +
+                " bar.\n" +
+                " <p>\n" +
+                " baz (eggs) spam \n" +
+                " {@link BundleContext#getService(ServiceReference)} or\n" +
+                " {@link OSGIUtils#getService(Class)}.\n" +
+                " <p>\n" +
+                " ham\n" +
+                " \n";
+
+        String expected = "" +
+                " foo \n" +
+                " bar.\n" +
+                " <br /> <br />\n" +
+                " baz (eggs) spam \n" +
+                " <code>BundleContext.getService(ServiceReference)</code> or\n" +
+                " <code>OSGIUtils.getService(Class)</code>.\n" +
+                " <br /> <br />\n" +
+                " ham\n" +
+                " \n";
+
+        assertEquals(expected, converter.convert(input));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/distribution/tools/MergePluginDocs.java	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLEventWriter;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+
+public class MergePluginDocs {
+
+    private static final String CORE = "plugin-docs";
+    private static final String DOCS_FILE_NAME = CORE + ".xml";
+    private static final String DOCS_ELEMENT = CORE;
+
+    public static void main(String[] args) throws IOException, XMLStreamException {
+        String startPath = ".";
+        if (args.length > 0) {
+            startPath = args[0];
+        }
+
+        List<Path> paths = findAllPluginDocs(startPath);
+        String mergedXml = mergePluginDocs(paths);
+        System.out.println(mergedXml);
+    }
+
+    private static List<Path> findAllPluginDocs(String startPath) throws IOException {
+        final List<Path> paths = new LinkedList<>();
+
+        Files.walkFileTree(Paths.get(startPath), new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                if (!Files.isRegularFile(file)) {
+                    throw new AssertionError("lrn2code, nub!");
+                }
+
+                if (file.getFileName().toString().equals(DOCS_FILE_NAME)) {
+                    paths.add(file);
+                }
+
+                return FileVisitResult.CONTINUE;
+            }
+        });
+        return paths;
+    }
+
+    private static String mergePluginDocs(List<Path> paths) throws IOException, XMLStreamException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        XMLEventFactory eventFactory = XMLEventFactory.newFactory();
+        XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
+        XMLEventWriter writer = outputFactory.createXMLEventWriter(outputStream);
+        XMLInputFactory inputFactory = XMLInputFactory.newFactory();
+
+        QName docsElement = new QName("", DOCS_ELEMENT, "");
+
+        writer.add(eventFactory.createStartDocument());
+        writer.add(eventFactory.createSpace("\n"));
+        writer.add(eventFactory.createStartElement(docsElement, null, null));
+
+        for (Path path : paths) {
+            XMLEventReader reader = inputFactory.createXMLEventReader(Files.newInputStream(path));
+            XMLEvent event;
+            while (reader.hasNext()) {
+                event = reader.nextEvent();
+                if (event.getEventType() != XMLEvent.START_DOCUMENT && event.getEventType() != XMLEvent.END_DOCUMENT) {
+                    if (event.isStartElement() && event.asStartElement().getName().equals(docsElement)) {
+                        // skip
+                    } else if (event.isEndElement() && event.asEndElement().getName().equals(docsElement)) {
+                        // skip
+                    } else if (event.getEventType() == XMLEvent.COMMENT) {
+                        // skip
+                    } else {
+                        writer.add(event);
+                    }
+                }
+            }
+        }
+
+        writer.add(eventFactory.createEndElement(docsElement, null));
+        writer.add(eventFactory.createEndDocument());
+        writer.close();
+
+        return outputStream.toString("UTF-8");
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/distribution/tools/plugin-docs-html.xslt	Thu Feb 07 12:05:12 2013 -0500
@@ -0,0 +1,137 @@
+<?xml version="1.0"?>
+
+<!--
+
+ Copyright 2012, 2013 Red Hat, Inc.
+
+ This file is part of Thermostat.
+
+ Thermostat is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published
+ by the Free Software Foundation; either version 2, or (at your
+ option) any later version.
+
+ Thermostat is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Thermostat; see the file COPYING.  If not see
+ <http://www.gnu.org/licenses />.
+
+ Linking this code with other modules is making a combined work
+ based on this code.  Thus, the terms and conditions of the GNU
+ General Public License cover the whole combination.
+
+ As a special exception, the copyright holders of this code give
+ you permission to link this code with independent modules to
+ produce an executable, regardless of the license terms of these
+ independent modules, and to copy and distribute the resulting
+ executable under terms of your choice, provided that you also
+ meet, for each linked independent module, the terms and conditions
+ of the license of that module.  An independent module is a module
+ which is not derived from or based on this code.  If you modify
+ this code, you may extend this exception to your version of the
+ library, but you are not obligated to do so.  If you do not wish
+ to do so, delete this exception statement from your version.
+
+-->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+  <xsl:output method="html" indent="yes" encoding="UTF-8"/>
+
+  <xsl:template match="/plugin-docs">
+    <xsl:text disable-output-escaping="yes">&lt;!DOCTYPE html&gt;</xsl:text>
+    <html>
+      <head>
+        <title>Plugin Documentation For Thermostat</title>
+
+        <style>
+            body { background: #f9f9f9; font-family: sans-serif; font-size: 10pt; }
+            table { width: 90%; border-width:0; border-collapse: collapse; }
+            thead { font-weight: bold }
+            thead tr:nth-child(odd) { background: #f9f9f9; }
+            tr:nth-child(odd) { background: #e0e0e0; }
+            td { vertical-align: top; padding: 0.5em 1em }
+            .description { margin: 1em; }
+            .javadoc { margin: 0; padding:0; }
+            .javadoc h5, .javadoc h6, .javadoc h7 { display:inline; }
+        </style>
+      </head>
+
+      <body>
+        <h1>Points of Interest for Plugin Developers</h1>
+
+        <p>Jump to</p>
+        <ol>
+          <li><a href="#extension-points">Extension Points</a></li>
+          <li><a href="#services">Services</a></li>
+        </ol>
+
+        <h2><a id="extension-points">Extension Points</a></h2>
+        <div class="description">
+          <p>
+          <xsl:apply-templates select="meta[contains(name/text(), 'ExtensionPoint')]"/>
+          </p>
+        </div>
+        <table>
+          <thead>
+            <tr>
+              <td>Extension Point Name</td>
+              <td>Documentation</td>
+            </tr>
+          </thead>
+          <tbody>
+            <xsl:apply-templates select="extension-point">
+              <xsl:sort select="name" />
+            </xsl:apply-templates>
+          </tbody>
+        </table>
+
+        <h2><a id="services">Services</a></h2>
+        <div class="description">
+          <p>
+          <xsl:apply-templates select="meta[contains(name/text(), 'Service')]"/>
+          </p>
+        </div>
+        <table>
+          <thead>
+            <tr>
+              <td>Service Name</td>
+              <td>Documentation</td>
+            </tr>
+          </thead>
+          <tbody>
+            <xsl:apply-templates select="service">
+              <xsl:sort select="name" />
+            </xsl:apply-templates>
+          </tbody>
+        </table>
+
+      </body>
+    </html>
+  </xsl:template>
+
+  <xsl:template match="extension-point|service">
+    <tr>
+      <td>
+        <code class="point"> <xsl:value-of select="name" /> </code>
+      </td>
+      <td>
+        <div class="javadoc">
+          <xsl:apply-templates select="doc" />
+        </div>
+      </td>
+    </tr>
+  </xsl:template>
+
+  <xsl:template match="meta">
+    <xsl:apply-templates select="doc" />
+  </xsl:template>
+
+  <xsl:template match="doc">
+    <xsl:copy-of select="."/>
+  </xsl:template>
+
+</xsl:stylesheet>
+