blob: 9f170f47783355291d6b7eab5e70b7488d0dc173 [file] [log] [blame]
/*
* 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.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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) {
Collections.sort(apisUsed);
String content = null;
try {
content = IO.slurp(example.getDestReadme());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
final String basePath = pathToContentRoot(example.getDestReadme());
final List<JavadocSource> list = apisUsed.stream()
.map(sources::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
for (JavadocSource javadocSource : list) {
final String link = String.format("%s%s/javadoc/%s.html",
basePath,
source.getName(),
javadocSource.getClassName().replace(".", "/"));
content = ApisUsed.insertHref(content, link, javadocSource.getClassName());
}
try {
IO.copy(IO.read(content), example.getDestReadme());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static String pathToContentRoot(final File file) {
final StringBuilder sb = new StringBuilder();
File parent = file;
while ((parent = parent.getParentFile()) != null && !parent.getName().equals("content")) {
sb.append("../");
}
return sb.toString();
}
static String pathFromContentRoot(final File file) {
final String absolutePath = file.getAbsolutePath();
final String content = "/content/";
final int indexOfContent = absolutePath.indexOf(content);
if (indexOfContent == -1) {
throw new IllegalStateException("Expected '/content/' section of path not found: " + absolutePath);
}
return absolutePath.substring(indexOfContent + content.length());
}
private void addSeeLink(final JavadocSource javadocSource, final Example example) {
try {
final String content = IO.slurp(javadocSource.getSourceFile());
final String toContentRoot = pathToContentRoot(javadocSource.getSourceFile());
final String fromContentRoot = pathFromContentRoot(example.getDestReadme())
.replace(".adoc", ".html")
.replace(".md", ".html");
final String link = toContentRoot + fromContentRoot;
final String name = example.getName();
// Update the source contents to include an href link
final String modified = ExampleLinks.insertHref(content, link, name);
// Overwrite the source with the newly linked version
IO.copy(IO.read(modified), javadocSource.getSourceFile());
} catch (Exception e) {
throw new IllegalStateException("Unable to add link to java source: " + javadocSource.getSourceFile().getAbsolutePath(), 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()));
}
}