You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by db...@apache.org on 2020/06/29 05:38:47 UTC

[tomee-site-generator] branch master updated: Initial framing for feature TOMEE-2346 Generate Javadoc '@see' references to our examples and docs https://issues.apache.org/jira/browse/TOMEE-2346

This is an automated email from the ASF dual-hosted git repository.

dblevins pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tomee-site-generator.git


The following commit(s) were added to refs/heads/master by this push:
     new dec4194  Initial framing for feature TOMEE-2346 Generate Javadoc '@see' references to our examples and docs https://issues.apache.org/jira/browse/TOMEE-2346
dec4194 is described below

commit dec4194bc77ca5eb2a1dcf36bb43a51bc4b5cb84
Author: David Blevins <da...@gmail.com>
AuthorDate: Sun Jun 28 22:38:22 2020 -0700

    Initial framing for feature TOMEE-2346 Generate Javadoc '@see' references to our examples and docs
    https://issues.apache.org/jira/browse/TOMEE-2346
---
 pom.xml                                            |   7 +-
 .../org/apache/tomee/website/Configuration.java    |  10 ++
 .../java/org/apache/tomee/website/Examples.java    |   8 +-
 .../org/apache/tomee/website/JavadocSource.java    |  38 +++++
 .../org/apache/tomee/website/JavadocSources.java   |  33 +++++
 .../java/org/apache/tomee/website/Javadocs.java    |  88 ++++++-----
 .../org/apache/tomee/website/LearningLinks.java    | 165 +++++++++++++++++++++
 .../java/org/apache/tomee/website/SeeLinks.java    |  29 ++++
 src/main/java/org/apache/tomee/website/Source.java |  79 ++++++++++
 .../java/org/apache/tomee/website/Sources.java     |  27 +++-
 .../java/org/apache/tomee/website/Scenario.java    |  53 +++++++
 .../org/apache/tomee/website/SeeLinksTest.java     |  90 +++++++++++
 .../SeeLinksTest/hasAnnotations/after.java         |  40 +++++
 .../SeeLinksTest/hasAnnotations/before.java        |  37 +++++
 .../resources/SeeLinksTest/insertHref/after.java   |  41 +++++
 .../resources/SeeLinksTest/insertHref/before.java  |  40 +++++
 .../SeeLinksTest/multipleInserts/after1.java       |  26 ++++
 .../SeeLinksTest/multipleInserts/after2.java       |  27 ++++
 .../SeeLinksTest/multipleInserts/after3.java       |  28 ++++
 .../SeeLinksTest/multipleInserts/before.java       |  23 +++
 .../resources/SeeLinksTest/noJavadoc/after.java    |  31 ++++
 .../resources/SeeLinksTest/noJavadoc/before.java   |  28 ++++
 22 files changed, 900 insertions(+), 48 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4e6d466..99ec9c8 100755
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
     <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
-      <version>1.16.12</version>
+      <version>1.18.12</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -107,6 +107,11 @@
       <artifactId>args4j</artifactId>
       <version>2.33</version>
     </dependency>
+    <dependency>
+      <groupId>org.tomitribe</groupId>
+      <artifactId>tomitribe-tio</artifactId>
+      <version>0.5</version>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/src/main/java/org/apache/tomee/website/Configuration.java b/src/main/java/org/apache/tomee/website/Configuration.java
index 2716530..75d0af5 100644
--- a/src/main/java/org/apache/tomee/website/Configuration.java
+++ b/src/main/java/org/apache/tomee/website/Configuration.java
@@ -16,6 +16,16 @@
  */
 package org.apache.tomee.website;
 
+/**
+ * This very well could be a json file or some other externalized format.
+ *
+ * For the moment it is kept in code simply to keep complexity low.  That said,
+ * please do not add "logic" here (no loops, if statements, string concatenation, etc)
+ * and try to keep the code here limited to simple structure.
+ *
+ * We may very well turn this into a json or yaml file that defines an array of sources.
+ * The simpler we keep this code, the easier that will be (when or if the time is right).
+ */
 public class Configuration {
     public static Source[] getSources() {
         final Source[] microProfile2 = new Source[]{
diff --git a/src/main/java/org/apache/tomee/website/Examples.java b/src/main/java/org/apache/tomee/website/Examples.java
index e483387..f0e04da 100644
--- a/src/main/java/org/apache/tomee/website/Examples.java
+++ b/src/main/java/org/apache/tomee/website/Examples.java
@@ -22,12 +22,11 @@ import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 public class Examples {
 
     private final Sources sources;
+    private final List<Example> examples = new ArrayList<>();
 
     public Examples(final Sources sources) {
         this.sources = sources;
@@ -40,7 +39,6 @@ public class Examples {
         // If we don't have examples in this codebase, skip
         if (!srcDir.exists()) return;
 
-        final List<Example> examples = new ArrayList<>();
         for (File file : srcDir.listFiles()) {
             if (file.isDirectory()) {
                 if (hasReadme(file)) {
@@ -65,6 +63,10 @@ public class Examples {
 //        https://javaee.github.io/javaee-spec/javadocs/javax/servlet/http/HttpServletMapping.html
     }
 
+    public List<Example> getExamples() {
+        return examples;
+    }
+
     /**
      * Copy all the readme.mdtext to examples/foo-bar.mdtext
      */
diff --git a/src/main/java/org/apache/tomee/website/JavadocSource.java b/src/main/java/org/apache/tomee/website/JavadocSource.java
new file mode 100644
index 0000000..2045240
--- /dev/null
+++ b/src/main/java/org/apache/tomee/website/JavadocSource.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.File;
+
+@Data
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class JavadocSource {
+
+    @EqualsAndHashCode.Include
+    private final String className;
+    private final File sourceFile;
+
+    public JavadocSource(final String relativePath, final File sourceFile) {
+        this.sourceFile = sourceFile;
+        this.className = relativePath
+                .replaceAll("\\.java$", "")
+                .replace("/", ".");
+    }
+}
diff --git a/src/main/java/org/apache/tomee/website/JavadocSources.java b/src/main/java/org/apache/tomee/website/JavadocSources.java
new file mode 100644
index 0000000..8abd085
--- /dev/null
+++ b/src/main/java/org/apache/tomee/website/JavadocSources.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JavadocSources {
+
+    private final List<JavadocSource> sources = new ArrayList<>();
+
+    public boolean add(final JavadocSource javadocSource) {
+        return sources.add(javadocSource);
+    }
+
+    public List<JavadocSource> getSources() {
+        return sources;
+    }
+}
diff --git a/src/main/java/org/apache/tomee/website/Javadocs.java b/src/main/java/org/apache/tomee/website/Javadocs.java
index 89a3846..146da53 100644
--- a/src/main/java/org/apache/tomee/website/Javadocs.java
+++ b/src/main/java/org/apache/tomee/website/Javadocs.java
@@ -24,6 +24,7 @@ import org.apache.openejb.util.Pipe;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -42,6 +43,7 @@ import static org.apache.openejb.loader.Files.mkdirs;
 public class Javadocs {
 
     private final Sources sources;
+    private final List<JavadocSource> javadocSources = new ArrayList<>();
 
     public Javadocs(final Sources sources) {
         this.sources = sources;
@@ -84,45 +86,52 @@ public class Javadocs {
             copySource(related, javaSources);
         }
 
-        final File javadocOutput = sources.getGeneratedDestFor(source, "javadoc");
-        final ProcessBuilder cmd = new ProcessBuilder(
-                getJavadocCommand().getAbsolutePath(),
-                "-sourcepath",
-                javaSources.getAbsolutePath(),
-                "-d",
-                javadocOutput.getAbsolutePath()
-        );
-
-        Stream.of(javaSources.listFiles())
-                .filter(File::isDirectory)
-                .forEach(file -> {
-                    cmd.command().add("-subpackages");
-                    cmd.command().add(file.getName());
-                });
-
-        try {
-            final Process process = cmd.start();
-            Pipe.pipe(process);
-            process.waitFor();
-        } catch (IOException e) {
-            throw new IllegalStateException("Command failed");
-        } catch (InterruptedException e) {
-            Thread.interrupted();
-            throw new IllegalStateException("Command failed");
-        }
+        // This part will be completed later when the perform stage is executed
+        source.addPerform(() -> {
+            final File javadocOutput = sources.getGeneratedDestFor(source, "javadoc");
+            final ProcessBuilder cmd = new ProcessBuilder(
+                    getJavadocCommand().getAbsolutePath(),
+                    "-sourcepath",
+                    javaSources.getAbsolutePath(),
+                    "-d",
+                    javadocOutput.getAbsolutePath()
+            );
+
+            Stream.of(javaSources.listFiles())
+                    .filter(File::isDirectory)
+                    .forEach(file -> {
+                        cmd.command().add("-subpackages");
+                        cmd.command().add(file.getName());
+                    });
 
-        // Scrub generated timestamps as it causes 26k needless file updates
-        // on the svn commit for every time the generator runs
-        try {
-            java.nio.file.Files.walk(javadocOutput.toPath())
-                    .map(Path::toFile)
-                    .filter(File::isFile)
-                    .filter(this::isHtml)
-                    .forEach(Javadocs::removeGeneratedDate);
-        } catch (IOException e) {
-            throw new IllegalStateException("Failed to remove timestamp from generated javadoc html");
-        }
+            try {
+                final Process process = cmd.start();
+                Pipe.pipe(process);
+                process.waitFor();
+            } catch (IOException e) {
+                throw new IllegalStateException("Command failed");
+            } catch (InterruptedException e) {
+                Thread.interrupted();
+                throw new IllegalStateException("Command failed");
+            }
+
+            // Scrub generated timestamps as it causes 26k needless file updates
+            // on the svn commit for every time the generator runs
+            try {
+                java.nio.file.Files.walk(javadocOutput.toPath())
+                        .map(Path::toFile)
+                        .filter(File::isFile)
+                        .filter(this::isHtml)
+                        .forEach(Javadocs::removeGeneratedDate);
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to remove timestamp from generated javadoc html");
+            }
+
+        });
+    }
 
+    public List<JavadocSource> getJavadocSources() {
+        return javadocSources;
     }
 
     public static void removeGeneratedDate(final File file) {
@@ -142,6 +151,9 @@ public class Javadocs {
     }
 
     private void copySource(final Source source, final File javaSources) {
+        final JavadocSources javadocSources = new JavadocSources();
+        source.setComponent(JavadocSources.class, javadocSources);
+
         try {
             java.nio.file.Files.walk(source.getDir().toPath())
                     .map(Path::toFile)
@@ -155,6 +167,7 @@ public class Javadocs {
                             final File dest = new File(javaSources, relativePath);
                             Files.mkdirs(dest.getParentFile());
                             IO.copy(file, dest);
+                            javadocSources.add(new JavadocSource(relativePath, dest));
                         } catch (IOException e) {
                             throw new IllegalStateException(e);
                         }
@@ -196,6 +209,7 @@ public class Javadocs {
     private boolean isJava(final File file) {
         return file.getName().endsWith(".java");
     }
+
     private boolean isHtml(final File file) {
         return file.getName().endsWith(".html");
     }
diff --git a/src/main/java/org/apache/tomee/website/LearningLinks.java b/src/main/java/org/apache/tomee/website/LearningLinks.java
new file mode 100644
index 0000000..1f2614c
--- /dev/null
+++ b/src/main/java/org/apache/tomee/website/LearningLinks.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+import org.apache.openejb.loader.IO;
+import org.tomitribe.tio.Dir;
+import org.tomitribe.tio.Match;
+import org.tomitribe.tio.lang.JvmLang;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This class is responsible for creating cross-links between Javadoc and Examples
+ * as detailed in https://issues.apache.org/jira/browse/TOMEE-2346
+ *
+ * The name "LearningLinks" is perhaps a bit weak.  The spirit of the feature is to
+ * make it easier for people to click back and forth between the javadoc and examples
+ * which are two primary sources of learning a new API.
+ *
+ * Ideally someone who searches for an API on the internet find the javadoc and from
+ * there can jump to any number of examples that use it.  Once on an example they can
+ * jump into other points of Javadoc and continue their learning.
+ */
+public class LearningLinks {
+
+    private final Examples examples;
+
+    public LearningLinks(final Examples examples) {
+        this.examples = examples;
+    }
+
+    /**
+     * The primary method driving all code in this class. Prepare is called once per Source,
+     * after example and javadoc preparation has been done.
+     *
+     * At this moment all of our examples have been copied to their final locations.  As well,
+     * we have created several "buckets" of source code on which we will later run the Javadoc tool.
+     *
+     * Once we have those two lists we will link the javadocs into the right examples. We will also
+     * link the applicable examples into each Javadoc as a @see tag.
+     *
+     * TODO: Note that we have several versions of the same examples, one per TomEE version. We may
+     * want to only link the latest one so we don't appear to have duplicates.  There is a Source
+     * called 'latest' which can be helpful in this regard, however, there will be situations where
+     * the 'latest' Source no longer refers to a particular API anymore (e.g. javax once we switch
+     * to jakarta) so we will want to consult all versions of the examples when we pick the most
+     * current matching example.
+     *
+     * Actual running of the javadoc command does not happen here.
+     *
+     * @see Source for a deeper description that is very applicable here
+     * @see Configuration for the full list of Sources that will be seen here
+     */
+    public void prepare(final Source source) {
+        if (!source.getName().contains("jakarta")) return;
+        final Map<String, JavadocSource> sources = getJavadocSources(source.stream());
+
+        for (final Example example : examples.getExamples()) {
+            final List<String> apisUsed = getImports(example).stream()
+                    .filter(sources::containsKey)
+                    .collect(Collectors.toList());
+
+            // If the example does not use any of the APIs from
+            // this Source instance (e.g Jakarta EE, MicroProfile, etc)
+            // then there is nothing to do.
+            if (apisUsed.size() == 0) continue;
+
+            // Add @see link to Javadoc
+            for (final String api : apisUsed) {
+                addSeeLink(sources.get(api), example);
+            }
+
+            // Add APIs Used links to Example
+            addApisUsed(example, apisUsed, sources, source);
+
+        }
+    }
+
+    private void addApisUsed(final Example example, final List<String> apisUsed, final Map<String, JavadocSource> sources, final Source source) {
+        // TODO
+    }
+
+
+    private void addSeeLink(final JavadocSource javadocSource, final Example example) {
+        try {
+            final String content = IO.slurp(javadocSource.getSourceFile());
+
+            // TODO this link won't resolve as-is, it needs to be relative
+            final String link = example.getHref();
+
+            final String name = example.getName();
+
+            // Update the source contents to include an href link
+            final String modified = SeeLinks.insertHref(content, link, name);
+
+            // Overwrite the source with the newly linked version
+            IO.copy(IO.read(modified), javadocSource.getSourceFile());
+        } catch (IOException e) {
+            throw new UncheckedIOException("Unable to add link to example: " + example.getName(), e);
+        }
+    }
+
+    /**
+     * Walk over every file in the example directory and look for import statements.
+     *
+     * Collect each statement and return a unique list of the class names
+     * referenced by each import statement.
+     */
+    private List<String> getImports(final Example example) {
+        final Dir dir = Dir.from(example.getSrcReadme().getParentFile());
+
+        // Unfiltered list of imported classes used in this example
+        // This list will contain the class names themselves
+        return dir.searchFiles()
+                .flatMap(JvmLang.imports(dir))
+                .map(Match::getMatch)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Return a map of JavadocSource instances that are applicable to this Source
+     * and any related Source instances.  If the Javadocs::prepare runs after this
+     * method is called an empty map will be returned.
+     */
+    private Map<String, JavadocSource> getJavadocSources(final Stream<Source> sources) {
+        // The stream for the jakartaee-platform.git repo will contain ejb-api.git and all
+        // related git repos.  Each repo will be a `Source` instance.
+
+        // The Javadocs class will have set a JavadocSources in each Source instance that
+        // we can use to get a list of java files that will be fed to the javadoc processor
+        // at a later time.
+        return sources.map(source -> source.getComponent(JavadocSources.class))
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .map(JavadocSources::getSources)
+                .flatMap(Collection::stream)
+                .distinct()
+                .collect(Collectors.toMap(JavadocSource::getClassName, Function.identity()));
+
+    }
+
+}
diff --git a/src/main/java/org/apache/tomee/website/SeeLinks.java b/src/main/java/org/apache/tomee/website/SeeLinks.java
new file mode 100644
index 0000000..a7231c8
--- /dev/null
+++ b/src/main/java/org/apache/tomee/website/SeeLinks.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+/**
+ * Utility class to insert additional @see links into java source code.
+ * If no Javadoc exists at the class level, some will be added.
+ */
+public class SeeLinks {
+
+    public static String insertHref(final String source, final String link, final String linkText) {
+        // TODO
+        return source;
+    }
+}
diff --git a/src/main/java/org/apache/tomee/website/Source.java b/src/main/java/org/apache/tomee/website/Source.java
index 70e0adf..188a53e 100644
--- a/src/main/java/org/apache/tomee/website/Source.java
+++ b/src/main/java/org/apache/tomee/website/Source.java
@@ -19,9 +19,45 @@ package org.apache.tomee.website;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
+/**
+ * A Source largely maps to a git repo we will checkout and use to build content
+ * to publish to the website.
+ * 
+ * Currently we have the following notable sources:
+ *
+ *  - tomee-8.0
+ *  - tomee-7.1
+ *  - tomee-7.0
+ *  - microprofile-2.0
+ *  - jakartaee-8.0
+ *  - master
+ *  - latest
+ *
+ * Each of these sources are given its own section on the website, for example:
+ *
+ *  - http://tomee.apache.org/jakartaee-8.0/javadoc/
+ *  - http://tomee.apache.org/tomee-8.0/javadoc/
+ *  - http://tomee.apache.org/tomee-8.0/examples/
+ *
+ * Sources may include content from other git repos and this act like an aggregator.
+ * For example the microprofile-2.0 Source has 8 related Source trees, one for each
+ * API in MicroProfile 2.0.  Each git reference includes the right tag information
+ * so we are able to get the source (and therefore javadoc) for the exact version in
+ * MicroProfile 2.0.
+ *
+ * We intentionally want the javadoc on our site to keep our links stable and not
+ * depending on third-party sites that could change and break our links.  It also
+ * allows us to enhance the javadoc to include links to and from our examples.
+ * And finally many of these javadocs are not online anywhere, so it allows us to
+ * bring some unique value to the world and increase our website traffic.
+ */
 public class Source {
     private final String name;
     private final String scmUrl;
@@ -31,6 +67,26 @@ public class Source {
     private File dir;
     private Filter javadocFilter = new Filter(".*/src/main/java/.*", ".*/(tck|itests|examples|archetype-resources|.*-example)/.*");
 
+    /**
+     * This allows us to attach a handful of finishing actions to
+     * each Source that get executed after any 'prepare' methods
+     * are called.  This will most likely consist of Lambdas and
+     * is effectively a very simple way to split our logic into
+     * two phases: prepare, perform.
+     */
+    private final List<Runnable> perform = new ArrayList<>();
+
+    /**
+     * The components map is a simple technique to allow us to attach
+     * objects to the Source in various "prepare" phases.  It was initially
+     * added to allow {@link Javadocs} to pass data to  {@link LearningLinks}
+     * without making the two directly reference on each other.
+     *
+     * It also allows us to prepare some data that is specific to the Source
+     * instance and not see by other Source instances.
+     */
+    private final Map<Class, Object> components = new HashMap();
+
     public Source(final String scmUrl, final String branch, final String name) {
         this(scmUrl, branch, name, false);
     }
@@ -42,6 +98,29 @@ public class Source {
         this.latest = latest;
     }
 
+    public List<Runnable> getPerform() {
+        return perform;
+    }
+
+    public boolean addPerform(final Runnable runnable) {
+        return perform.add(runnable);
+    }
+
+    public <T> T setComponent(Class<T> type, T value) {
+        return (T) this.components.put(type, value);
+    }
+
+    public <T> Optional<T> getComponent(Class<T> type) {
+        return Optional.ofNullable((T) this.components.get(type));
+    }
+
+    /**
+     * Returns stream of this source and all related sources
+     */
+    public Stream<Source> stream() {
+        return Stream.concat(Stream.of(this), this.getRelated().stream());
+    }
+
     public boolean isLatest() {
         return latest;
     }
diff --git a/src/main/java/org/apache/tomee/website/Sources.java b/src/main/java/org/apache/tomee/website/Sources.java
index 13daf34..5ee6ce5 100644
--- a/src/main/java/org/apache/tomee/website/Sources.java
+++ b/src/main/java/org/apache/tomee/website/Sources.java
@@ -21,6 +21,7 @@ import org.apache.openejb.loader.IO;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -119,6 +120,7 @@ public class Sources {
         final Javadocs javadocs = new Javadocs(this);
         final Examples examples = new Examples(this);
         final VersionIndex versionIndex = new VersionIndex(this);
+        final LearningLinks learningLinks = new LearningLinks(examples);
 
         try {
             IO.copyDirectory(mainSource, jbake);
@@ -126,20 +128,30 @@ public class Sources {
             throw new RuntimeException(e);
         }
 
+        // Download the git repo associated with each Source
+        // including any related Source/git repos
         sources.stream()
-                .flatMap(source -> source.getRelated().stream())
+                .flatMap(Source::stream)
                 .peek(source -> source.setDir(new File(repos, source.getName())))
                 .forEach(Repos::download);
 
+        // Run any initial steps to process each
+        // source root (excluding the related repos)
         sources.stream()
-                .peek(source -> source.setDir(new File(repos, source.getName())))
-                .peek(Repos::download)
                 .peek(docs::prepare)
                 .peek(javadocs::prepare)
                 .peek(examples::prepare)
                 .peek(versionIndex::prepare)
-                .forEach(Sources::done);
+                .peek(learningLinks::prepare)
+                .forEach(Sources::prepared);
 
+        // Run any final tasks that have been registered
+        // with any Source instance during the prepare phase
+        sources.stream()
+                .flatMap(Source::stream)
+                .map(Source::getPerform)
+                .flatMap(Collection::stream)
+                .forEach(Runnable::run);
 
         VersionsIndex.prepare(this);
     }
@@ -153,7 +165,7 @@ public class Sources {
 
         sources.stream()
                 .peek(javadocs::prepare)
-                .forEach(Sources::done);
+                .forEach(Sources::prepared);
         ;
     }
 
@@ -184,7 +196,8 @@ public class Sources {
         return dir;
     }
 
-    private static void done(final Source source) {
-        System.out.println("Done " + source);
+    private static void prepared(final Source source) {
+        System.out.println("Prepared " + source);
     }
+
 }
diff --git a/src/test/java/org/apache/tomee/website/Scenario.java b/src/test/java/org/apache/tomee/website/Scenario.java
new file mode 100644
index 0000000..47f3f3f
--- /dev/null
+++ b/src/test/java/org/apache/tomee/website/Scenario.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+import org.apache.openejb.loader.IO;
+
+import java.io.IOException;
+import java.net.URL;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Simple utility class to make it easier to load test files
+ * for certain test methods
+ */
+public class Scenario {
+
+    private final ClassLoader loader;
+    private final String basePath;
+
+    public Scenario(final ClassLoader loader, final String basePath) {
+        this.loader = loader;
+        this.basePath = basePath;
+    }
+
+    public String get(final String resource) throws IOException {
+        final String path = basePath + resource;
+        final URL url = loader.getResource(path);
+        assertNotNull("Resource not found: " + path, url);
+        return IO.slurp(url);
+    }
+
+    public static Scenario scenario(final Class testClass, final String method) {
+        final ClassLoader loader = testClass.getClassLoader();
+        final String basePath = testClass.getSimpleName() + "/" + method + "/";
+
+        return new Scenario(loader, basePath);
+    }
+}
diff --git a/src/test/java/org/apache/tomee/website/SeeLinksTest.java b/src/test/java/org/apache/tomee/website/SeeLinksTest.java
new file mode 100644
index 0000000..64bc931
--- /dev/null
+++ b/src/test/java/org/apache/tomee/website/SeeLinksTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomee.website;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.apache.tomee.website.Scenario.scenario;
+import static org.junit.Assert.assertEquals;
+
+@Ignore
+public class SeeLinksTest {
+
+    /**
+     * Test we can insert an @see link into some code that already has some javadoc
+     */
+    @Test
+    public void insertHref() throws IOException {
+        final Scenario scenario = scenario(SeeLinksTest.class, "insertHref");
+
+        final String input = scenario.get("before.java");
+
+        final String actual = SeeLinks.insertHref(input, "http://example.org/orange.html", "Orange Example");
+
+        assertEquals(scenario.get("after.java"), actual);
+    }
+
+    /**
+     * Test we can insert an @see link into some code has no javadoc yet
+     */
+    @Test
+    public void noJavadoc() throws IOException {
+        final Scenario scenario = scenario(SeeLinksTest.class, "noJavadoc");
+
+        final String input = scenario.get("before.java");
+
+        final String actual = SeeLinks.insertHref(input, "http://example.org/orange.html", "Orange Example");
+
+        assertEquals(scenario.get("after.java"), actual);
+    }
+
+    /**
+     * Test we can insert several @see links into the same javadoc
+     */
+    @Test
+    public void multipleInserts() throws IOException {
+        final Scenario scenario = scenario(SeeLinksTest.class, "multipleInserts");
+
+        final String input = scenario.get("before.java");
+
+        final String after1 = SeeLinks.insertHref(input, "http://example.org/orange.html", "Orange Example");
+        assertEquals(scenario.get("after1.java"), after1);
+
+        final String after2 = SeeLinks.insertHref(after1, "http://example.org/red.html", "Red Sample");
+        assertEquals(scenario.get("after2.java"), after2);
+
+        final String after3 = SeeLinks.insertHref(after2, "http://example.org/yellow.html", "yellow");
+        assertEquals(scenario.get("after3.java"), after3);
+    }
+
+    /**
+     * Test we can insert several @see links into the same javadoc
+     */
+    @Test
+    public void hasAnnotations() throws IOException {
+        final Scenario scenario = scenario(SeeLinksTest.class, "hasAnnotations");
+
+        final String input = scenario.get("before.java");
+
+        final String actual = SeeLinks.insertHref(input, "http://example.org/orange.html", "Orange Example");
+        assertEquals(scenario.get("after.java"), actual);
+    }
+
+}
diff --git a/src/test/resources/SeeLinksTest/hasAnnotations/after.java b/src/test/resources/SeeLinksTest/hasAnnotations/after.java
new file mode 100644
index 0000000..f5a3706
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/hasAnnotations/after.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Documented;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ */
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+public @interface Entity {
+
+        /**
+         * (Optional) The entity name. Defaults to the unqualified
+         * name of the entity class. This name is used to refer to the
+         * entity in queries. The name must not be a reserved literal
+         * in the Java Persistence query language.
+         */
+        String name() default "";
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/hasAnnotations/before.java b/src/test/resources/SeeLinksTest/hasAnnotations/before.java
new file mode 100644
index 0000000..6d15e0e
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/hasAnnotations/before.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Documented;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+public @interface Entity {
+
+        /**
+         * (Optional) The entity name. Defaults to the unqualified
+         * name of the entity class. This name is used to refer to the
+         * entity in queries. The name must not be a reserved literal
+         * in the Java Persistence query language.
+         */
+        String name() default "";
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/insertHref/after.java b/src/test/resources/SeeLinksTest/insertHref/after.java
new file mode 100644
index 0000000..75f3b52
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/insertHref/after.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Documented;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Specifies that the class is an entity. This annotation is applied to the
+ * entity class.
+ *
+ * @since Java Persistence 1.0
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ */
+public interface Entity {
+
+        /**
+         * (Optional) The entity name. Defaults to the unqualified
+         * name of the entity class. This name is used to refer to the
+         * entity in queries. The name must not be a reserved literal
+         * in the Java Persistence query language.
+         */
+        String name();
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/insertHref/before.java b/src/test/resources/SeeLinksTest/insertHref/before.java
new file mode 100644
index 0000000..5ffc69c
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/insertHref/before.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Documented;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Specifies that the class is an entity. This annotation is applied to the
+ * entity class.
+ *
+ * @since Java Persistence 1.0
+ */
+public interface Entity {
+
+        /**
+         * (Optional) The entity name. Defaults to the unqualified
+         * name of the entity class. This name is used to refer to the
+         * entity in queries. The name must not be a reserved literal
+         * in the Java Persistence query language.
+         */
+        String name();
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/multipleInserts/after1.java b/src/test/resources/SeeLinksTest/multipleInserts/after1.java
new file mode 100644
index 0000000..13ae94a
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/multipleInserts/after1.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+/**
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ */
+public enum Shapes {
+    CIRCLE,
+    TRIANGLE,
+    SQUARE
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/multipleInserts/after2.java b/src/test/resources/SeeLinksTest/multipleInserts/after2.java
new file mode 100644
index 0000000..d673789
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/multipleInserts/after2.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+/**
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ * @see <a href="http://example.org/red.html">Red Sample</a>
+ */
+public enum Shapes {
+    CIRCLE,
+    TRIANGLE,
+    SQUARE
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/multipleInserts/after3.java b/src/test/resources/SeeLinksTest/multipleInserts/after3.java
new file mode 100644
index 0000000..932c0cc
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/multipleInserts/after3.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+/**
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ * @see <a href="http://example.org/red.html">Red Sample</a>
+ * @see <a href="http://example.org/red.html">yellow</a>
+ */
+public enum Shapes {
+    CIRCLE,
+    TRIANGLE,
+    SQUARE
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/multipleInserts/before.java b/src/test/resources/SeeLinksTest/multipleInserts/before.java
new file mode 100644
index 0000000..9321f32
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/multipleInserts/before.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+public enum Shapes {
+    CIRCLE,
+    TRIANGLE,
+    SQUARE
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/noJavadoc/after.java b/src/test/resources/SeeLinksTest/noJavadoc/after.java
new file mode 100644
index 0000000..da7abb7
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/noJavadoc/after.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+/**
+ * @see <a href="http://example.org/orange.html">Orange Example</a>
+ */
+public @interface Entity {
+
+    /**
+     * (Optional) The entity name. Defaults to the unqualified
+     * name of the entity class. This name is used to refer to the
+     * entity in queries. The name must not be a reserved literal
+     * in the Java Persistence query language.
+     */
+    String name() default "";
+}
\ No newline at end of file
diff --git a/src/test/resources/SeeLinksTest/noJavadoc/before.java b/src/test/resources/SeeLinksTest/noJavadoc/before.java
new file mode 100644
index 0000000..f6c2e2d
--- /dev/null
+++ b/src/test/resources/SeeLinksTest/noJavadoc/before.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package javax.persistence;
+
+public @interface Entity {
+
+    /**
+     * (Optional) The entity name. Defaults to the unqualified
+     * name of the entity class. This name is used to refer to the
+     * entity in queries. The name must not be a reserved literal
+     * in the Java Persistence query language.
+     */
+    String name() default "";
+}
\ No newline at end of file