Enable flow documentation in external build

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=241934689
This commit is contained in:
shicong 2019-04-04 08:29:03 -07:00 committed by jianglai
parent 9b80b31917
commit 387042bf3a
34 changed files with 1412 additions and 15 deletions

View file

@ -0,0 +1,70 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.documentation;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.RootDoc;
import google.registry.documentation.FlowDocumentation.ErrorCase;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* Main entry point class for documentation generation. An instance of this class reads data
* via the javadoc system upon creation and stores it for answering future queries for
* documentation information.
*/
public final class DocumentationGenerator {
private final RootDoc sourceRoot;
/** Returns a new DocumentationGenerator object with parsed information from javadoc. */
public DocumentationGenerator() throws IOException {
sourceRoot = JavadocWrapper.getRootDoc();
}
/** Returns generated Markdown output for the flows. Convenience method for clients. */
public String generateMarkdown() {
return MarkdownDocumentationFormatter.generateMarkdownOutput(getFlowDocs());
}
/** Returns a list of flow documentation objects derived from this generator's data. */
public ImmutableList<FlowDocumentation> getFlowDocs() {
// Relevant flows are leaf flows: precisely the concrete subclasses of Flow.
return getConcreteSubclassesStream(FlowDocumentation.BASE_FLOW_CLASS_NAME)
.sorted(comparing(ClassDoc::typeName))
.map(FlowDocumentation::new)
.collect(toImmutableList());
}
/** Returns a list of all possible error cases that might occur. */
public ImmutableList<ErrorCase> getAllErrors() {
// Relevant error cases are precisely the concrete subclasses of EppException.
return getConcreteSubclassesStream(FlowDocumentation.EXCEPTION_CLASS_NAME)
.map(ErrorCase::new)
.collect(toImmutableList());
}
/** Helper to return all concrete subclasses of a given named class. */
private Stream<ClassDoc> getConcreteSubclassesStream(String baseClassName) {
final ClassDoc baseFlowClassDoc = sourceRoot.classNamed(baseClassName);
return Arrays.stream(sourceRoot.classes())
.filter(classDoc -> classDoc.subclassOf(baseFlowClassDoc) && !classDoc.isAbstract());
}
}

View file

@ -0,0 +1,278 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.documentation;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.MoreCollectors.onlyElement;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;
import google.registry.model.eppoutput.Result.Code;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.TreeMap;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/**
* Class to represent documentation information for a single EPP flow.
*
* <p>The static method getFlowDocs() on this class returns a list of FlowDocumentation
* instances corresponding to the leaf flows in the flows package, constructing the instances
* from class information returned from the javadoc system. Each instance has methods for
* retrieving relevant information about the flow, such as a description, error conditions, etc.
*/
public class FlowDocumentation {
/** Constants for names of various relevant packages and classes. */
static final String FLOW_PACKAGE_NAME = "google.registry.flows";
static final String BASE_FLOW_CLASS_NAME = FLOW_PACKAGE_NAME + ".Flow";
static final String EXCEPTION_CLASS_NAME = FLOW_PACKAGE_NAME + ".EppException";
static final String CODE_ANNOTATION_NAME = EXCEPTION_CLASS_NAME + ".EppResultCode";
/** Name of the class for this flow. */
private final String name;
/** Fully qualified name of the class for this flow. */
private final String qualifiedName;
/** Name of the package in which this flow resides. */
private final String packageName;
/** Class docs for the flow. */
private final String classDocs;
/** Javadoc-tagged error conditions for this flow in list form. */
private final List<ErrorCase> errors;
/** Javadoc-tagged error conditions for this flow, organized by underlying error code. */
private final ListMultimap<Long, ErrorCase> errorsByCode;
/**
* Creates a FlowDocumentation for this flow class using data from javadoc tags. Not public
* because clients should get FlowDocumentation objects via the DocumentationGenerator class.
*/
protected FlowDocumentation(ClassDoc flowDoc) {
name = flowDoc.name();
qualifiedName = flowDoc.qualifiedName();
packageName = flowDoc.containingPackage().name();
classDocs = flowDoc.commentText();
errors = new ArrayList<>();
// Store error codes in sorted order, and leave reasons in insert order.
errorsByCode =
Multimaps.newListMultimap(new TreeMap<Long, Collection<ErrorCase>>(), ArrayList::new);
parseTags(flowDoc);
}
public String getName() {
return name;
}
public String getQualifiedName() {
return qualifiedName;
}
public String getPackageName() {
return packageName;
}
public String getClassDocs() {
return classDocs;
}
public ImmutableList<ErrorCase> getErrors() {
return ImmutableList.copyOf(errors);
}
public ImmutableMultimap<Long, ErrorCase> getErrorsByCode() {
return ImmutableMultimap.copyOf(errorsByCode);
}
/** Iterates through javadoc tags on the underlying class and calls specific parsing methods. */
private void parseTags(ClassDoc flowDoc) {
for (Tag tag : flowDoc.tags()) {
switch (tag.name()) {
case "@error":
parseErrorTag(tag);
break;
default:
// Not a relevant tag.
}
}
}
/** Exception to throw when an @error tag cannot be parsed correctly. */
private static class BadErrorTagFormatException extends IllegalStateException {
/** Makes a message to use as a prefix for the reason passed up to the superclass. */
private static String makeMessage(String reason, Tag tag) {
return String.format("Bad @error tag format at %s - %s", tag.position(), reason);
}
private BadErrorTagFormatException(String reason, Tag tag) {
super(makeMessage(reason, tag));
}
private BadErrorTagFormatException(String reason, Tag tag, Exception cause) {
super(makeMessage(reason, tag), cause);
}
}
/** Parses a javadoc tag corresponding to an error case and updates the error mapping. */
private void parseErrorTag(Tag tag) {
// Parse the @error tag text to find the @link inline tag.
SeeTag linkedTag;
try {
linkedTag =
Stream.of(tag.inlineTags())
.filter(SeeTag.class::isInstance)
.map(SeeTag.class::cast)
.collect(onlyElement());
} catch (NoSuchElementException | IllegalArgumentException e) {
throw new BadErrorTagFormatException(
String.format("expected one @link tag in tag text but found %s: %s",
(e instanceof NoSuchElementException ? "none" : "multiple"),
tag.text()),
tag, e);
}
// Check to see if the @link tag references a valid class.
ClassDoc exceptionRef = linkedTag.referencedClass();
if (exceptionRef == null) {
throw new BadErrorTagFormatException(
"could not resolve class from @link tag text: " + linkedTag.text(),
tag);
}
// Try to convert the referenced class into an ErrorCase; fail if it's not an EppException.
ErrorCase error;
try {
error = new ErrorCase(exceptionRef);
} catch (IllegalStateException | IllegalArgumentException e) {
throw new BadErrorTagFormatException(
"class referenced in @link is not a valid EppException: " + exceptionRef.qualifiedName(),
tag, e);
}
// Success; store this as a parsed error case.
errors.add(error);
errorsByCode.put(error.getCode(), error);
}
/**
* Represents an error case for a flow, with a reason for the error and the EPP error code.
*
* <p>This class is an immutable wrapper for the name of an EppException subclass that gets
* thrown to indicate an error condition. It overrides equals() and hashCode() so that
* instances of this class can be used in collections in the normal fashion.
*/
public static class ErrorCase {
/** The non-qualified name of the exception class. */
private final String name;
/** The fully-qualified name of the exception class. */
private final String className;
/** The reason this error was thrown, normally documented on the low-level exception class. */
private final String reason;
/** The EPP error code value corresponding to this error condition. */
private final long errorCode;
/** Constructs an ErrorCase from the corresponding class for a low-level flow exception. */
protected ErrorCase(ClassDoc exceptionDoc) {
name = exceptionDoc.name();
className = exceptionDoc.qualifiedName();
// The javadoc comment on the class explains the reason for the error condition.
reason = exceptionDoc.commentText();
ClassDoc highLevelExceptionDoc = getHighLevelExceptionFrom(exceptionDoc);
errorCode = extractErrorCode(highLevelExceptionDoc);
checkArgument(!exceptionDoc.isAbstract(),
"Cannot use an abstract subclass of EppException as an error case");
}
public String getName() {
return name;
}
protected String getClassName() {
return className;
}
public String getReason() {
return reason;
}
public long getCode() {
return errorCode;
}
/** Returns the direct subclass of EppException that this class is a subclass of (or is). */
private ClassDoc getHighLevelExceptionFrom(ClassDoc exceptionDoc) {
// While we're not yet at the root, move up the class hierarchy looking for EppException.
while (exceptionDoc.superclass() != null) {
if (exceptionDoc.superclass().qualifiedTypeName().equals(EXCEPTION_CLASS_NAME)) {
return exceptionDoc;
}
exceptionDoc = exceptionDoc.superclass();
}
// Failure; we reached the root without finding a subclass of EppException.
throw new IllegalArgumentException(
String.format("Class referenced is not a subclass of %s", EXCEPTION_CLASS_NAME));
}
/** Returns the corresponding EPP error code for an annotated subclass of EppException. */
private long extractErrorCode(ClassDoc exceptionDoc) {
try {
// We're looking for a specific annotation by name that should appear only once.
AnnotationDesc errorCodeAnnotation =
Arrays.stream(exceptionDoc.annotations())
.filter(
anno -> anno.annotationType().qualifiedTypeName().equals(CODE_ANNOTATION_NAME))
.findFirst()
.get();
// The annotation should have one element whose value converts to an EppResult.Code.
AnnotationDesc.ElementValuePair pair = errorCodeAnnotation.elementValues()[0];
String enumConstant = ((FieldDoc) pair.value().value()).name();
return Code.valueOf(enumConstant).code;
} catch (IllegalStateException e) {
throw new IllegalStateException(
"No error code annotation found on exception " + exceptionDoc.name(), e);
} catch (ArrayIndexOutOfBoundsException | ClassCastException | IllegalArgumentException e) {
throw new IllegalStateException("Bad annotation on exception " + exceptionDoc.name(), e);
}
}
@Override
public boolean equals(@Nullable Object object) {
// The className field canonically identifies the EppException wrapped by this class, and
// all other instance state is derived from that exception, so we only check className.
return object instanceof ErrorCase && this.className.equals(((ErrorCase) object).className);
}
@Override
public int hashCode() {
// See note for equals() - only className is needed for comparisons.
return className.hashCode();
}
}
}

View file

@ -0,0 +1,90 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.documentation;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
/**
* Tool to generate documentation for the EPP flows and corresponding external API.
*
* <p>Mostly responsible for producing standalone documentation files (HTML
* and Markdown) from flow information objects; those call into javadoc to
* extract documentation information from the flows package source files.
* See the {@link FlowDocumentation} class for more details.
*/
@Parameters(separators = " =", commandDescription = "Tool to generate EPP API documentation")
public class FlowDocumentationTool {
@Parameter(names = {"-o", "--output_file"},
description = "file where generated documentation will be written (use '-' for stdout)")
private String outputFileName;
@Parameter(names = {"--help", "--helpshort"}, description = "print this help", help = true)
private boolean displayHelp = false;
/** Parses command line flags and then runs the documentation tool. */
public static void main(String[] args) {
FlowDocumentationTool docTool = new FlowDocumentationTool();
JCommander jcommander = new JCommander(docTool);
jcommander.setProgramName("flow_docs_tool");
try {
jcommander.parse(args);
} catch (ParameterException e) {
jcommander.usage();
throw e;
}
if (docTool.displayHelp) {
jcommander.usage();
return;
}
docTool.run();
}
/** Generates flow documentation and then outputs it to the specified file. */
public void run() {
DocumentationGenerator docGenerator;
try {
docGenerator = new DocumentationGenerator();
} catch (IOException e) {
throw new RuntimeException("IO error while running Javadoc tool", e);
}
String output = docGenerator.generateMarkdown();
if (outputFileName.equals("-")) {
System.out.println(output);
} else {
if (outputFileName == null) {
outputFileName = "doclet.html";
}
try {
Files.asCharSink(new File(outputFileName), UTF_8).write(output);
} catch (IOException e) {
throw new RuntimeException("Could not write to specified output file", e);
}
}
}
}

View file

@ -0,0 +1,158 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.documentation;
import static google.registry.util.BuildPathUtils.getProjectRoot;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.CharStreams;
import com.sun.javadoc.RootDoc;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javadoc.JavadocTool;
import com.sun.tools.javadoc.Messager;
import com.sun.tools.javadoc.ModifierFilter;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nullable;
import javax.tools.StandardLocation;
/**
* Wrapper class to simplify calls to the javadoc system and hide internal details. An instance
* represents a set of parameters for calling out to javadoc; these parameters can be set via
* the appropriate methods, and determine what files and packages javadoc will process. The
* actual running of javadoc occurs when calling getRootDoc() to retrieve a javadoc RootDoc.
*/
public final class JavadocWrapper {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Shows any member visible at at least the default (package) level. */
private static final long VISIBILITY_MASK =
Modifier.PUBLIC | Modifier.PROTECTED | ModifierFilter.PACKAGE;
/** Root directory for source files. If null, will use the current directory. */
private static final String SOURCE_PATH = getProjectRoot().resolve("java").toString();
/** Specific source files to generate documentation for. */
private static final ImmutableSet<String> SOURCE_FILE_NAMES = ImmutableSet.of();
/** Specific packages to generate documentation for. */
private static final ImmutableSet<String> SOURCE_PACKAGE_NAMES =
ImmutableSet.of(FlowDocumentation.FLOW_PACKAGE_NAME);
/** Whether or not the Javadoc tool should eschew excessive log output. */
private static final boolean QUIET = true;
/**
* Obtains a Javadoc {@link RootDoc} object containing raw Javadoc documentation.
* Wraps a call to the static method createRootDoc() and passes in instance-specific settings.
*/
public static RootDoc getRootDoc() throws IOException {
logger.atInfo().log("Starting Javadoc tool");
File sourceFilePath = new File(SOURCE_PATH);
logger.atInfo().log("Using source directory: %s", sourceFilePath.getAbsolutePath());
try {
return createRootDoc(
SOURCE_PATH,
SOURCE_PACKAGE_NAMES,
SOURCE_FILE_NAMES,
VISIBILITY_MASK,
QUIET);
} finally {
logger.atInfo().log("Javadoc tool finished");
}
}
/**
* Obtains a Javadoc root document object for the specified source path and package/Java names.
* If the source path is null, then the working directory is assumed as the source path.
*
* <p>If a list of package names is provided, then Javadoc will run on these packages and all
* their subpackages, based out of the specified source path.
*
* <p>If a list of file names is provided, then Javadoc will also run on these Java source files.
* The specified source path is not considered in this case.
*
* @see <a href="http://relation.to/12969.lace">Testing Java doclets</a>
* @see <a href="http://www.docjar.com/docs/api/com/sun/tools/javadoc/JavadocTool.html">JavadocTool</a>
*/
private static RootDoc createRootDoc(
@Nullable String sourcePath,
Collection<String> packageNames,
Collection<String> fileNames,
long visibilityMask,
boolean quiet) throws IOException {
// Create a context to hold settings for Javadoc.
Context context = new Context();
// Redirect Javadoc stdout/stderr to null writers, since otherwise the Java compiler
// issues lots of errors for classes that are imported and visible to blaze but not
// visible locally to the compiler.
// TODO(b/19124943): Find a way to ignore those errors so we can show real ones?
Messager.preRegister(
context,
JavadocWrapper.class.getName(),
new PrintWriter(CharStreams.nullWriter()), // For errors.
new PrintWriter(CharStreams.nullWriter()), // For warnings.
new PrintWriter(CharStreams.nullWriter())); // For notices.
// Set source path option for Javadoc.
try (JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8)) {
List<File> sourcePathFiles = new ArrayList<>();
if (sourcePath != null) {
for (String sourcePathEntry : Splitter.on(':').split(sourcePath)) {
sourcePathFiles.add(new File(sourcePathEntry));
}
}
fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles);
// Create an instance of Javadoc.
JavadocTool javadocTool = JavadocTool.make0(context);
// Convert the package and file lists to a format Javadoc can understand.
ListBuffer<String> subPackages = new ListBuffer<>();
subPackages.addAll(packageNames);
ListBuffer<String> javaNames = new ListBuffer<>();
javaNames.addAll(fileNames);
// Invoke Javadoc and ask it for a RootDoc containing the specified packages.
return javadocTool.getRootDocImpl(
Locale.US.toString(), // Javadoc comment locale
UTF_8.name(), // Source character encoding
new ModifierFilter(visibilityMask), // Element visibility filter
javaNames.toList(), // Included Java file names
com.sun.tools.javac.util.List.nil(), // Doclet options
com.sun.tools.javac.util.List.nil(), // Source files
false, // Don't use BreakIterator
subPackages.toList(), // Included sub-package names
com.sun.tools.javac.util.List.nil(), // Excluded package names
false, // Read source files, not classes
false, // Don't run legacy doclet
quiet); // If asked, run Javadoc quietly
}
}
private JavadocWrapper() {}
}

View file

@ -0,0 +1,182 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.documentation;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import google.registry.documentation.FlowDocumentation.ErrorCase;
import java.util.ArrayList;
import java.util.List;
/**
* Formatter that converts flow documentation into Markdown.
*/
public final class MarkdownDocumentationFormatter {
/** Header for flow documentation HTML output. */
private static final String MARKDOWN_HEADER = "# Nomulus EPP Command API Documentation\n\n";
/** Pattern that naively matches HTML tags and entity references. */
private static final Pattern HTML_TAG_AND_ENTITY_PATTERN = Pattern.compile("<[^>]*>|&[^;]*;");
/** 8 character indentation. */
private static final String INDENT8 = Strings.repeat(" ", 8);
/** Max linewidth for our markdown docs. */
private static final int LINE_WIDTH = 80;
/**
* Returns the string with all HTML tags stripped. Also, removes a single space after any
* newlines that have one (we get a single space indent for all lines but the first because of
* the way that javadocs are written in comments).
*/
@VisibleForTesting
static String fixHtml(String value) {
Matcher matcher = HTML_TAG_AND_ENTITY_PATTERN.matcher(value);
int pos = 0;
StringBuilder result = new StringBuilder();
while (matcher.find(pos)) {
result.append(value, pos, matcher.start());
switch (matcher.group(0)) {
case "<p>":
// <p> is simply removed.
break;
case "&amp;":
result.append("&");
break;
case "&lt;":
result.append("<");
break;
case "&gt;":
result.append(">");
break;
case "&squot;":
result.append("'");
break;
case "&quot;":
result.append("\"");
break;
default:
throw new IllegalArgumentException("Unrecognized HTML sequence: " + matcher.group(0));
}
pos = matcher.end();
}
// Add the string after the last HTML sequence.
result.append(value.substring(pos));
return result.toString().replace("\n ", "\n");
}
/**
* Formats a list of words into a paragraph with less than maxWidth characters per line.
*/
@VisibleForTesting
static String formatParagraph(List<String> words, int maxWidth) {
int lineLength = 0;
StringBuilder output = new StringBuilder();
for (String word : words) {
// This check ensures that 1) don't add a space before the word and 2) always have at least
// one word per line, so that we don't mishandle a very long word at the end of a line by
// adding a blank line before the word.
if (lineLength > 0) {
// Do we have enough room for another word?
if (lineLength + 1 + word.length() > maxWidth) {
// No. End the line.
output.append('\n');
lineLength = 0;
} else {
// Yes: Insert a space before the word.
output.append(' ');
++lineLength;
}
}
output.append(word);
lineLength += word.length();
}
output.append('\n');
return output.toString();
}
/**
* Returns 'value' with words reflowed to maxWidth characters.
*/
@VisibleForTesting
static String reflow(String text, int maxWidth) {
// A list of words that will be constructed into the list of words in a paragraph.
ArrayList<String> words = new ArrayList<>();
// Read through the lines, process a paragraph every time we get a blank line.
StringBuilder resultBuilder = new StringBuilder();
for (String line : Splitter.on('\n').trimResults().split(text)) {
// If we got a blank line, format our current paragraph and start fresh.
if (line.trim().isEmpty()) {
resultBuilder.append(formatParagraph(words, maxWidth));
resultBuilder.append('\n');
words.clear();
continue;
}
// Split the line into words and add them to the current paragraph.
words.addAll(Splitter.on(
CharMatcher.breakingWhitespace()).omitEmptyStrings().splitToList(line));
}
// Format the last paragraph, if any.
if (!words.isEmpty()) {
resultBuilder.append(formatParagraph(words, maxWidth));
}
return resultBuilder.toString();
}
/** Returns a string of HTML representing the provided flow documentation objects. */
public static String generateMarkdownOutput(Iterable<FlowDocumentation> flowDocs) {
StringBuilder output = new StringBuilder();
output.append(MARKDOWN_HEADER);
for (FlowDocumentation flowDoc : flowDocs) {
output.append(String.format("## %s\n\n", flowDoc.getName()));
output.append("### Description\n\n");
output.append(String.format("%s\n\n", reflow(fixHtml(flowDoc.getClassDocs()), LINE_WIDTH)));
output.append("### Errors\n\n");
for (Long code : flowDoc.getErrorsByCode().keySet()) {
output.append(String.format("* %d\n", code));
for (ErrorCase error : flowDoc.getErrorsByCode().get(code)) {
output.append(" * ");
String wrappedReason = reflow(fixHtml(error.getReason()), LINE_WIDTH - 8);
// Replace internal newlines with indentation and strip the final newline.
output.append(wrappedReason.trim().replace("\n", "\n" + INDENT8));
output.append('\n');
}
}
output.append('\n');
}
return output.toString();
}
private MarkdownDocumentationFormatter() {}
}

View file

@ -0,0 +1,38 @@
#!/bin/bash
# Copyright 2017 The Nomulus Authors. All Rights Reserved.
#
# Licensed 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.
# Generate javadoc for the project
if (( $# != 3 )); then
echo "Usage: $0 JAVADOC ZIP OUT" 1>&2
exit 1
fi
JAVADOC_BINARY="$1"
ZIP_BINARY="$2"
TARGETFILE="$3"
TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/generate_javadoc.XXXXXXXX")"
PWDDIR="$(pwd)"
"${JAVADOC_BINARY}" -d "${TMPDIR}" \
$(find java -name \*.java) \
-tag error:t:'EPP Errors' \
-subpackages google.registry \
-exclude google.registry.dns:google.registry.proxy:google.registry.monitoring.blackbox
cd "${TMPDIR}"
"${PWDDIR}/${ZIP_BINARY}" -rXoq "${PWDDIR}/${TARGETFILE}" .
cd -
rm -rf "${TMPDIR}"