mirror of
https://github.com/google/nomulus.git
synced 2025-05-14 00:17:20 +02:00
Enable flow documentation in external build
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=241934689
This commit is contained in:
parent
9b80b31917
commit
387042bf3a
34 changed files with 1412 additions and 15 deletions
|
@ -58,6 +58,7 @@ if (!project.hasProperty('showAllOutput')) {
|
||||||
gradleLint.autoLintAfterFailure = false
|
gradleLint.autoLintAfterFailure = false
|
||||||
|
|
||||||
// Paths to main and test sources.
|
// Paths to main and test sources.
|
||||||
|
ext.projectRootDir = "${rootDir}/.."
|
||||||
ext.javaDir = "${rootDir}/../java"
|
ext.javaDir = "${rootDir}/../java"
|
||||||
ext.javatestsDir = "${rootDir}/../javatests"
|
ext.javatestsDir = "${rootDir}/../javatests"
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,7 @@ dependencies {
|
||||||
maybeRuntime deps['com.squareup:javawriter']
|
maybeRuntime deps['com.squareup:javawriter']
|
||||||
maybeRuntime deps['com.sun.activation:javax.activation']
|
maybeRuntime deps['com.sun.activation:javax.activation']
|
||||||
maybeRuntime deps['com.thoughtworks.paranamer:paranamer']
|
maybeRuntime deps['com.thoughtworks.paranamer:paranamer']
|
||||||
|
testCompile deps['com.thoughtworks.qdox:qdox']
|
||||||
maybeRuntime deps['commons-codec:commons-codec']
|
maybeRuntime deps['commons-codec:commons-codec']
|
||||||
maybeRuntime deps['commons-logging:commons-logging']
|
maybeRuntime deps['commons-logging:commons-logging']
|
||||||
compile deps['dnsjava:dnsjava']
|
compile deps['dnsjava:dnsjava']
|
||||||
|
@ -613,6 +614,22 @@ task generateGoldenImages(type: Test) {
|
||||||
}
|
}
|
||||||
generateGoldenImages.finalizedBy(findGoldenImages)
|
generateGoldenImages.finalizedBy(findGoldenImages)
|
||||||
|
|
||||||
|
task flowDocsTool(type: JavaExec) {
|
||||||
|
systemProperty 'test.projectRoot', rootProject.projectRootDir
|
||||||
|
systemProperty 'test.resourcesDir', resourcesDir
|
||||||
|
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
main = 'google.registry.documentation.FlowDocumentationTool'
|
||||||
|
|
||||||
|
def arguments = []
|
||||||
|
if (rootProject.flowDocsFile) {
|
||||||
|
arguments << "--output_file=${rootProject.flowDocsFile}"
|
||||||
|
} else {
|
||||||
|
arguments << "--output_file=${rootProject.projectRootDir}/docs/flows.md"
|
||||||
|
}
|
||||||
|
args arguments
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
// Common exclude pattern. See README in parent directory for explanation.
|
// Common exclude pattern. See README in parent directory for explanation.
|
||||||
exclude "**/*TestCase.*", "**/*TestSuite.*"
|
exclude "**/*TestCase.*", "**/*TestSuite.*"
|
||||||
|
@ -627,6 +644,9 @@ test {
|
||||||
// Sets the maximum number of test executors that may exist at the same time.
|
// Sets the maximum number of test executors that may exist at the same time.
|
||||||
maxParallelForks 5
|
maxParallelForks 5
|
||||||
|
|
||||||
|
systemProperty 'test.projectRoot', rootProject.projectRootDir
|
||||||
|
systemProperty 'test.resourcesDir', resourcesDir
|
||||||
|
|
||||||
doFirst {
|
doFirst {
|
||||||
new File(screenshotsDir).deleteDir()
|
new File(screenshotsDir).deleteDir()
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,6 +122,7 @@ com.squareup.okhttp3:okhttp:3.11.0
|
||||||
com.squareup.okhttp:okhttp:2.5.0
|
com.squareup.okhttp:okhttp:2.5.0
|
||||||
com.squareup.okio:okio:1.14.0
|
com.squareup.okio:okio:1.14.0
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -121,6 +121,7 @@ com.squareup.okhttp3:okhttp:3.11.0
|
||||||
com.squareup.okhttp:okhttp:2.5.0
|
com.squareup.okhttp:okhttp:2.5.0
|
||||||
com.squareup.okio:okio:1.14.0
|
com.squareup.okio:okio:1.14.0
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -84,6 +84,7 @@ ext {
|
||||||
'com.sun.xml.bind:jaxb-osgi:2.2.11',
|
'com.sun.xml.bind:jaxb-osgi:2.2.11',
|
||||||
'com.sun.xml.bind:jaxb-xjc:2.2.11',
|
'com.sun.xml.bind:jaxb-xjc:2.2.11',
|
||||||
'com.thoughtworks.paranamer:paranamer:2.7',
|
'com.thoughtworks.paranamer:paranamer:2.7',
|
||||||
|
'com.thoughtworks.qdox:qdox:1.12.1',
|
||||||
'dnsjava:dnsjava:2.1.7',
|
'dnsjava:dnsjava:2.1.7',
|
||||||
'io.netty:netty-buffer:4.1.31.Final',
|
'io.netty:netty-buffer:4.1.31.Final',
|
||||||
'io.netty:netty-codec:4.1.31.Final',
|
'io.netty:netty-codec:4.1.31.Final',
|
||||||
|
|
|
@ -4,3 +4,4 @@ uploaderDestination=
|
||||||
uploaderCredentialsFile=
|
uploaderCredentialsFile=
|
||||||
uploaderMultithreadedUpload=
|
uploaderMultithreadedUpload=
|
||||||
mavenCoordinateFile=
|
mavenCoordinateFile=
|
||||||
|
flowDocsFile=
|
||||||
|
|
|
@ -13,8 +13,8 @@ com.jcraft:jzlib:1.1.1
|
||||||
com.moowork.gradle:gradle-node-plugin:1.2.0
|
com.moowork.gradle:gradle-node-plugin:1.2.0
|
||||||
com.moowork.node:com.moowork.node.gradle.plugin:1.2.0
|
com.moowork.node:com.moowork.node.gradle.plugin:1.2.0
|
||||||
com.netflix.nebula:gradle-lint-plugin:10.4.2
|
com.netflix.nebula:gradle-lint-plugin:10.4.2
|
||||||
com.netflix.nebula:nebula-gradle-interop:1.0.3
|
com.netflix.nebula:nebula-gradle-interop:1.0.6
|
||||||
com.netflix.nebula:nebula-test:7.2.1
|
com.netflix.nebula:nebula-test:7.2.3
|
||||||
commons-codec:commons-codec:1.9
|
commons-codec:commons-codec:1.9
|
||||||
commons-io:commons-io:2.5
|
commons-io:commons-io:2.5
|
||||||
commons-lang:commons-lang:2.6
|
commons-lang:commons-lang:2.6
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
3
gradle/third_party/gradle/dependency-locks/compile.lockfile
vendored
Normal file
3
gradle/third_party/gradle/dependency-locks/compile.lockfile
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a Gradle generated file for dependency locking.
|
||||||
|
# Manual edits can break the build and are not advised.
|
||||||
|
# This file is expected to be part of source control.
|
3
gradle/third_party/gradle/dependency-locks/compileClasspath.lockfile
vendored
Normal file
3
gradle/third_party/gradle/dependency-locks/compileClasspath.lockfile
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a Gradle generated file for dependency locking.
|
||||||
|
# Manual edits can break the build and are not advised.
|
||||||
|
# This file is expected to be part of source control.
|
3
gradle/third_party/gradle/dependency-locks/runtimeClasspath.lockfile
vendored
Normal file
3
gradle/third_party/gradle/dependency-locks/runtimeClasspath.lockfile
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a Gradle generated file for dependency locking.
|
||||||
|
# Manual edits can break the build and are not advised.
|
||||||
|
# This file is expected to be part of source control.
|
3
gradle/third_party/gradle/dependency-locks/testCompile.lockfile
vendored
Normal file
3
gradle/third_party/gradle/dependency-locks/testCompile.lockfile
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a Gradle generated file for dependency locking.
|
||||||
|
# Manual edits can break the build and are not advised.
|
||||||
|
# This file is expected to be part of source control.
|
3
gradle/third_party/gradle/dependency-locks/testRuntimeClasspath.lockfile
vendored
Normal file
3
gradle/third_party/gradle/dependency-locks/testRuntimeClasspath.lockfile
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a Gradle generated file for dependency locking.
|
||||||
|
# Manual edits can break the build and are not advised.
|
||||||
|
# This file is expected to be part of source control.
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -124,6 +124,7 @@ com.squareup.okio:okio:1.14.0
|
||||||
com.sun.istack:istack-commons-runtime:3.0.5
|
com.sun.istack:istack-commons-runtime:3.0.5
|
||||||
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
com.sun.xml.fastinfoset:FastInfoset:1.2.13
|
||||||
com.thoughtworks.paranamer:paranamer:2.7
|
com.thoughtworks.paranamer:paranamer:2.7
|
||||||
|
com.thoughtworks.qdox:qdox:1.12.1
|
||||||
commons-codec:commons-codec:1.10
|
commons-codec:commons-codec:1.10
|
||||||
commons-logging:commons-logging:1.2
|
commons-logging:commons-logging:1.2
|
||||||
dnsjava:dnsjava:2.1.7
|
dnsjava:dnsjava:2.1.7
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
278
java/google/registry/documentation/FlowDocumentation.java
Normal file
278
java/google/registry/documentation/FlowDocumentation.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
158
java/google/registry/documentation/JavadocWrapper.java
Normal file
158
java/google/registry/documentation/JavadocWrapper.java
Normal 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() {}
|
||||||
|
}
|
|
@ -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 "&":
|
||||||
|
result.append("&");
|
||||||
|
break;
|
||||||
|
case "<":
|
||||||
|
result.append("<");
|
||||||
|
break;
|
||||||
|
case ">":
|
||||||
|
result.append(">");
|
||||||
|
break;
|
||||||
|
case "&squot;":
|
||||||
|
result.append("'");
|
||||||
|
break;
|
||||||
|
case """:
|
||||||
|
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() {}
|
||||||
|
}
|
38
java/google/registry/documentation/generate_javadoc.sh
Executable file
38
java/google/registry/documentation/generate_javadoc.sh
Executable 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}"
|
40
java/google/registry/util/BuildPathUtils.java
Normal file
40
java/google/registry/util/BuildPathUtils.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2019 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.util;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
/** Utilities methods related to build path. */
|
||||||
|
public final class BuildPathUtils {
|
||||||
|
// When we run the build from gradlew's directory, the current working directory would be
|
||||||
|
// ${projectRoot}/gradle/${subproject}. So, the project root is the grand parent of it.
|
||||||
|
private static final Path PROJECT_ROOT =
|
||||||
|
Paths.get(System.getProperty("test.projectRoot", "../../")).normalize();
|
||||||
|
private static final Path RESOURCES_DIR =
|
||||||
|
Paths.get(System.getProperty("test.resourcesDir", "build/resources/main")).normalize();
|
||||||
|
|
||||||
|
/** Returns the {@link Path} to the project root directory. */
|
||||||
|
public static Path getProjectRoot() {
|
||||||
|
return PROJECT_ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link Path} to the resources directory. */
|
||||||
|
public static Path getResourcesDir() {
|
||||||
|
return RESOURCES_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BuildPathUtils() {}
|
||||||
|
}
|
268
javatests/google/registry/documentation/FlowContext.java
Normal file
268
javatests/google/registry/documentation/FlowContext.java
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
// 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.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||||
|
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||||
|
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.base.Ascii;
|
||||||
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.MultimapBuilder;
|
||||||
|
import com.google.common.collect.SetMultimap;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.common.collect.Streams;
|
||||||
|
import com.thoughtworks.qdox.JavaDocBuilder;
|
||||||
|
import com.thoughtworks.qdox.model.JavaSource;
|
||||||
|
import google.registry.documentation.FlowDocumentation.ErrorCase;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the context for a flow and computes exception mismatches between javadoc and tests.
|
||||||
|
*
|
||||||
|
* <p>This class uses the flow_docs library built for the documentation generator tool to pull out
|
||||||
|
* the set of flow exceptions documented by custom javadoc tags on the specified flow. It then
|
||||||
|
* derives the corresponding test files for that flow and pulls out the imported names from those
|
||||||
|
* files, checking against a set of all possible flow exceptions to determine those used by this
|
||||||
|
* particular test. The set of javadoc-based exceptions and the set of imported exceptions should
|
||||||
|
* be identical, ensuring a correspondence between error cases listed in the documentation and
|
||||||
|
* those tested in the flow unit tests.
|
||||||
|
*
|
||||||
|
* <p>If the two sets are not identical, the getMismatchedExceptions() method on this class will
|
||||||
|
* return a non-empty string containing messages about what the mismatches were and which lines
|
||||||
|
* need to be added or removed in which files in order to satisfy the correspondence condition.
|
||||||
|
*/
|
||||||
|
public class FlowContext {
|
||||||
|
/** Represents one of the two possible places an exception may be referenced from. */
|
||||||
|
// TODO(b/19124943): This enum is only used in ErrorCaseMismatch and ideally belongs there, but
|
||||||
|
// can't go in the inner class because it's not a static inner class. At some point it might
|
||||||
|
// be worth refactoring so that this enum can be properly scoped.
|
||||||
|
private enum SourceType { JAVADOC, IMPORT }
|
||||||
|
|
||||||
|
/** The package in which this flow resides. */
|
||||||
|
final String packageName;
|
||||||
|
|
||||||
|
/** The source file for this flow, used for help messages. */
|
||||||
|
final String sourceFilename;
|
||||||
|
|
||||||
|
/** The test files for this flow, used for help messages and extracting imported exceptions. */
|
||||||
|
final Set<String> testFilenames;
|
||||||
|
|
||||||
|
/** The set of all possible exceptions that could be error cases for a flow. */
|
||||||
|
final Set<ErrorCase> possibleExceptions;
|
||||||
|
|
||||||
|
/** The set of exceptions referenced from the javadoc on this flow. */
|
||||||
|
final Set<ErrorCase> javadocExceptions;
|
||||||
|
|
||||||
|
/** Maps exceptions imported by the test files for this flow to the files in which they occur. */
|
||||||
|
final SetMultimap<ErrorCase, String> importExceptionsToFilenames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a FlowContext from a FlowDocumentation object and a set of all possible exceptions.
|
||||||
|
* The latter parameter is needed in order to filter imported names in the flow test file.
|
||||||
|
*/
|
||||||
|
public FlowContext(FlowDocumentation flowDoc, Set<ErrorCase> possibleExceptions)
|
||||||
|
throws IOException {
|
||||||
|
packageName = flowDoc.getPackageName();
|
||||||
|
// Assume the standard filename conventions for locating the flow class's source file.
|
||||||
|
sourceFilename = "java/" + flowDoc.getQualifiedName().replace('.', '/') + ".java";
|
||||||
|
testFilenames = getTestFilenames(flowDoc.getQualifiedName());
|
||||||
|
checkState(testFilenames.size() >= 1, "No test files found for %s.", flowDoc.getName());
|
||||||
|
this.possibleExceptions = possibleExceptions;
|
||||||
|
javadocExceptions = Sets.newHashSet(flowDoc.getErrors());
|
||||||
|
importExceptionsToFilenames = getImportExceptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to locate test files for this flow by looking in javatests/ for all files with the
|
||||||
|
* exact same relative filename as the flow file, but with a "*Test{,Case}.java" suffix.
|
||||||
|
*/
|
||||||
|
private static Set<String> getTestFilenames(String flowName) throws IOException {
|
||||||
|
String commonPrefix =
|
||||||
|
getProjectRoot().resolve("javatests").resolve(flowName.replace('.', '/')).toString();
|
||||||
|
return Sets.union(
|
||||||
|
getFilenamesMatchingGlob(commonPrefix + "*Test.java"),
|
||||||
|
getFilenamesMatchingGlob(commonPrefix + "*TestCase.java"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to return the set of filenames matching the given glob. The glob should only have
|
||||||
|
* asterisks in the portion following the last slash (if there is one).
|
||||||
|
*/
|
||||||
|
private static Set<String> getFilenamesMatchingGlob(String fullGlob) throws IOException {
|
||||||
|
Path globPath = FileSystems.getDefault().getPath(fullGlob);
|
||||||
|
Path dirPath = globPath.getParent();
|
||||||
|
String glob = globPath.getFileName().toString();
|
||||||
|
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath, glob)) {
|
||||||
|
return Streams.stream(dirStream).map(Object::toString).collect(toImmutableSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a multimap mapping each exception imported in test files for this flow to the set of
|
||||||
|
* filenames for files that import that exception.
|
||||||
|
*/
|
||||||
|
private SetMultimap<ErrorCase, String> getImportExceptions() throws IOException {
|
||||||
|
ImmutableMultimap.Builder<String, ErrorCase> builder = new ImmutableMultimap.Builder<>();
|
||||||
|
for (String testFileName : testFilenames) {
|
||||||
|
builder.putAll(testFileName, getImportExceptionsFromFile(testFileName));
|
||||||
|
}
|
||||||
|
// Invert the mapping so that later we can easily map exceptions to where they were imported.
|
||||||
|
return MultimapBuilder.hashKeys().hashSetValues().build(builder.build().inverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of exceptions imported in this test file. First extracts the set of
|
||||||
|
* all names imported by the test file, and then uses these to filter a global list of possible
|
||||||
|
* exceptions, so that the additional exception information available via the global list objects
|
||||||
|
* (which are ErrorCases wrapping exception names) can be preserved.
|
||||||
|
*/
|
||||||
|
private Set<ErrorCase> getImportExceptionsFromFile(String filename) throws IOException {
|
||||||
|
JavaDocBuilder builder = new JavaDocBuilder();
|
||||||
|
JavaSource src = builder.addSource(new File(filename));
|
||||||
|
final Set<String> importedNames = Sets.newHashSet(src.getImports());
|
||||||
|
return possibleExceptions
|
||||||
|
.stream()
|
||||||
|
.filter(errorCase -> importedNames.contains(errorCase.getClassName()))
|
||||||
|
.collect(toImmutableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a mismatch in this flow for a specific error case and documents how to fix it.
|
||||||
|
* A mismatch occurs when the exception for this error case appears in either the source file
|
||||||
|
* javadoc or at least one matching test file, but not in both.
|
||||||
|
*/
|
||||||
|
private class ErrorCaseMismatch {
|
||||||
|
|
||||||
|
/** The format for an import statement for a given exception name. */
|
||||||
|
static final String IMPORT_FORMAT = "import %s;";
|
||||||
|
|
||||||
|
/** The format for a javadoc tag referencing a given exception name. */
|
||||||
|
static final String JAVADOC_FORMAT = "@error {@link %s}";
|
||||||
|
|
||||||
|
// Template strings for printing output.
|
||||||
|
static final String TEMPLATE_HEADER = "Extra %s for %s:\n";
|
||||||
|
static final String TEMPLATE_ADD = " Add %s to %s:\n + %s\n";
|
||||||
|
static final String TEMPLATE_ADD_MULTIPLE = " Add %s to one or more of:\n%s + %s\n";
|
||||||
|
static final String TEMPLATE_REMOVE = " Or remove %s in %s:\n - %s\n";
|
||||||
|
static final String TEMPLATE_REMOVE_MULTIPLE = " Or remove %ss in:\n%s - %s\n";
|
||||||
|
static final String TEMPLATE_MULTIPLE_FILES = " * %s\n";
|
||||||
|
|
||||||
|
/** The error case for which the mismatch was detected. */
|
||||||
|
final ErrorCase errorCase;
|
||||||
|
|
||||||
|
/** The source type where references could be added to fix the mismatch. */
|
||||||
|
final SourceType addType;
|
||||||
|
|
||||||
|
/** The source type where references could be removed to fix the mismatch. */
|
||||||
|
final SourceType removeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an ErrorCaseMismatch for the given ErrorCase and SourceType. The latter parameter
|
||||||
|
* indicates the source type this exception was referenced from.
|
||||||
|
*/
|
||||||
|
public ErrorCaseMismatch(ErrorCase errorCase, SourceType foundType) {
|
||||||
|
this.errorCase = errorCase;
|
||||||
|
// Effectively addType = !foundType.
|
||||||
|
addType = (foundType == SourceType.IMPORT ? SourceType.JAVADOC : SourceType.IMPORT);
|
||||||
|
removeType = foundType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the line of code needed to refer to this exception from the given source type. */
|
||||||
|
public String getCodeLineAs(SourceType sourceType) {
|
||||||
|
return sourceType == SourceType.JAVADOC
|
||||||
|
// Strip the flow package prefix from the exception class name if possible, for brevity.
|
||||||
|
? String.format(JAVADOC_FORMAT, errorCase.getClassName().replace(packageName + ".", ""))
|
||||||
|
: String.format(IMPORT_FORMAT, errorCase.getClassName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper to format a set of filenames for printing in a mismatch message. */
|
||||||
|
private String formatMultipleFiles(Set<String> filenames) {
|
||||||
|
checkArgument(filenames.size() >= 1, "Cannot format empty list of files.");
|
||||||
|
if (filenames.size() == 1) {
|
||||||
|
return filenames.stream().collect(onlyElement());
|
||||||
|
}
|
||||||
|
return filenames
|
||||||
|
.stream()
|
||||||
|
.map(filename -> String.format(TEMPLATE_MULTIPLE_FILES, filename))
|
||||||
|
.collect(joining(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper to format the section describing how to add references to fix the mismatch. */
|
||||||
|
private String makeAddSection() {
|
||||||
|
String addTypeString = Ascii.toLowerCase(addType.toString());
|
||||||
|
String codeLine = getCodeLineAs(addType);
|
||||||
|
Set<String> files = (addType == SourceType.JAVADOC
|
||||||
|
? ImmutableSet.of(sourceFilename)
|
||||||
|
: testFilenames);
|
||||||
|
return (files.size() == 1
|
||||||
|
? String.format(
|
||||||
|
TEMPLATE_ADD, addTypeString, formatMultipleFiles(files), codeLine)
|
||||||
|
: String.format(
|
||||||
|
TEMPLATE_ADD_MULTIPLE, addTypeString, formatMultipleFiles(files), codeLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper to format the section describing how to remove references to fix the mismatch. */
|
||||||
|
// TODO(b/19124943): Repeating structure from makeAddSection() - would be nice to clean up.
|
||||||
|
private String makeRemoveSection() {
|
||||||
|
String removeTypeString = Ascii.toLowerCase(removeType.toString());
|
||||||
|
String codeLine = getCodeLineAs(removeType);
|
||||||
|
Set<String> files = (removeType == SourceType.JAVADOC
|
||||||
|
? ImmutableSet.of(sourceFilename)
|
||||||
|
: importExceptionsToFilenames.get(errorCase));
|
||||||
|
return (files.size() == 1
|
||||||
|
? String.format(
|
||||||
|
TEMPLATE_REMOVE, removeTypeString, formatMultipleFiles(files), codeLine)
|
||||||
|
: String.format(
|
||||||
|
TEMPLATE_REMOVE_MULTIPLE, removeTypeString, formatMultipleFiles(files), codeLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a string describing the mismatch for this flow exception and how to fix it. */
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String headerSection = String.format(
|
||||||
|
TEMPLATE_HEADER, Ascii.toLowerCase(removeType.toString()), errorCase.getName());
|
||||||
|
return headerSection + makeAddSection() + makeRemoveSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single string describing all mismatched exceptions for this flow. An empty string
|
||||||
|
* means no mismatched exceptions were found.
|
||||||
|
*/
|
||||||
|
public String getMismatchedExceptions() {
|
||||||
|
Set<ErrorCase> importExceptions = importExceptionsToFilenames.keySet();
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (ErrorCase errorCase : Sets.difference(javadocExceptions, importExceptions)) {
|
||||||
|
builder.append(new ErrorCaseMismatch(errorCase, SourceType.JAVADOC)).append("\n");
|
||||||
|
}
|
||||||
|
for (ErrorCase errorCase : Sets.difference(importExceptions, javadocExceptions)) {
|
||||||
|
builder.append(new ErrorCaseMismatch(errorCase, SourceType.IMPORT)).append("\n");
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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.truth.Truth.assert_;
|
||||||
|
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Tests to ensure that generated flow documentation matches the expected documentation. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class FlowDocumentationTest {
|
||||||
|
private static final Path GOLDEN_MARKDOWN_FILEPATH = getProjectRoot().resolve("docs/flows.md");
|
||||||
|
|
||||||
|
private static final String UPDATE_COMMAND = "./gradlew :core:flowDocsTool";
|
||||||
|
private static final String UPDATE_INSTRUCTIONS =
|
||||||
|
Joiner.on('\n')
|
||||||
|
.join(
|
||||||
|
"",
|
||||||
|
"-----------------------------------------------------------------------------------",
|
||||||
|
"Your changes affect the flow API documentation output. To update the golden version "
|
||||||
|
+ "of the documentation, run:",
|
||||||
|
UPDATE_COMMAND,
|
||||||
|
"");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeneratedMatchesGolden() throws IOException {
|
||||||
|
// Read the markdown file.
|
||||||
|
Path goldenMarkdownPath =
|
||||||
|
GOLDEN_MARKDOWN_FILEPATH;
|
||||||
|
|
||||||
|
String goldenMarkdown = new String(Files.readAllBytes(goldenMarkdownPath), "UTF-8");
|
||||||
|
|
||||||
|
// Don't use Truth's isEqualTo() because the output is huge and unreadable for large files.
|
||||||
|
DocumentationGenerator generator = new DocumentationGenerator();
|
||||||
|
if (!generator.generateMarkdown().equals(goldenMarkdown)) {
|
||||||
|
assert_().fail(UPDATE_INSTRUCTIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.documentation.FlowDocumentation.ErrorCase;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to ensure accurate documentation of flow exceptions.
|
||||||
|
*
|
||||||
|
* <p>This test goes through each flow and ensures that the exceptions listed in custom javadoc
|
||||||
|
* tags on the flow class match the import statements in the test case for that flow. Thus it
|
||||||
|
* catches the case where someone adds a test case for an exception without updating the javadoc,
|
||||||
|
* and the case where someone adds a javadoc tag to a flow without writing a test for this error
|
||||||
|
* condition. For example, there should always be a matching pair of lines such as the following:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java/.../flows/session/LoginFlow.java:
|
||||||
|
* @error {@link AlreadyLoggedInException}
|
||||||
|
*
|
||||||
|
* javatests/.../flows/session/LoginFlowTest.java:
|
||||||
|
* import .....flows.session.LoginFlow.AlreadyLoggedInException;
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* If the first line is missing, this test fails and suggests adding the javadoc tag or removing
|
||||||
|
* the import. If the second line is missing, this test fails and suggests adding the import or
|
||||||
|
* removing the javadoc tag.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("javadoc")
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class FlowExceptionsTest {
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExceptionCorrespondence() throws IOException {
|
||||||
|
DocumentationGenerator docGenerator = new DocumentationGenerator();
|
||||||
|
Set<ErrorCase> possibleErrors = Sets.newHashSet(docGenerator.getAllErrors());
|
||||||
|
Set<String> mismatchingFlows = Sets.newHashSet();
|
||||||
|
for (FlowDocumentation flow : docGenerator.getFlowDocs()) {
|
||||||
|
FlowContext context = new FlowContext(flow, possibleErrors);
|
||||||
|
String mismatches = context.getMismatchedExceptions();
|
||||||
|
if (!mismatches.isEmpty()) {
|
||||||
|
logger.atWarning().log("%-40s FAIL\n\n%s", flow.getName(), mismatches);
|
||||||
|
mismatchingFlows.add(flow.getName());
|
||||||
|
} else {
|
||||||
|
logger.atInfo().log("%-40s OK", flow.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertWithMessage(
|
||||||
|
"Mismatched exceptions between flow documentation and tests. See test log for full "
|
||||||
|
+ "details. The set of failing flows follows.")
|
||||||
|
.that(mismatchingFlows)
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
// 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.truth.Truth.assertThat;
|
||||||
|
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Test conversion of javadocs to markdown. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class MarkdownDocumentationFormatterTest {
|
||||||
|
@Test
|
||||||
|
public void testHtmlSanitization() {
|
||||||
|
assertThat(
|
||||||
|
MarkdownDocumentationFormatter.fixHtml(
|
||||||
|
"First. <p>Second. < > & &squot; ""))
|
||||||
|
.isEqualTo("First. Second. < > & ' \"");
|
||||||
|
assertThat(MarkdownDocumentationFormatter.fixHtml("<p>Leading substitution."))
|
||||||
|
.isEqualTo("Leading substitution.");
|
||||||
|
assertThat(MarkdownDocumentationFormatter.fixHtml("No substitution."))
|
||||||
|
.isEqualTo("No substitution.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDedents() {
|
||||||
|
assertThat(MarkdownDocumentationFormatter.fixHtml(
|
||||||
|
"First line\n\n <p>Second line.\n Third line."))
|
||||||
|
.isEqualTo("First line\n\nSecond line.\nThird line.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnknownSequences() {
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class, () -> MarkdownDocumentationFormatter.fixHtml("&blech;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParagraphFormatting() {
|
||||||
|
String[] words = {"first", "second", "third", "really-really-long-word", "more", "stuff"};
|
||||||
|
String formatted = MarkdownDocumentationFormatter.formatParagraph(Arrays.asList(words), 16);
|
||||||
|
assertThat(formatted).isEqualTo("first second\nthird\nreally-really-long-word\nmore stuff\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflow() {
|
||||||
|
String input =
|
||||||
|
"This is the very first line.\n"
|
||||||
|
+ " \n" // add a little blank space to this line just to make things interesting.
|
||||||
|
+ "This is the second paragraph. Aint\n"
|
||||||
|
+ "it sweet?\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "This is our third and final paragraph.\n"
|
||||||
|
+ "It is multi-line and ends with no blank\n"
|
||||||
|
+ "line.";
|
||||||
|
|
||||||
|
String expected =
|
||||||
|
"This is the very\n"
|
||||||
|
+ "first line.\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "This is the\n"
|
||||||
|
+ "second\n"
|
||||||
|
+ "paragraph. Aint\n"
|
||||||
|
+ "it sweet?\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "This is our\n"
|
||||||
|
+ "third and final\n"
|
||||||
|
+ "paragraph. It is\n"
|
||||||
|
+ "multi-line and\n"
|
||||||
|
+ "ends with no\n"
|
||||||
|
+ "blank line.\n";
|
||||||
|
|
||||||
|
assertThat(MarkdownDocumentationFormatter.reflow(input, 16)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarkdownDocumentationFormatterTest() {}
|
||||||
|
}
|
|
@ -15,6 +15,8 @@
|
||||||
package google.registry.server;
|
package google.registry.server;
|
||||||
|
|
||||||
import static google.registry.server.Route.route;
|
import static google.registry.server.Route.route;
|
||||||
|
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||||
|
import static google.registry.util.BuildPathUtils.getResourcesDir;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
@ -25,27 +27,22 @@ import google.registry.module.backend.BackendServlet;
|
||||||
import google.registry.module.frontend.FrontendServlet;
|
import google.registry.module.frontend.FrontendServlet;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Optional;
|
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
|
|
||||||
/** Lightweight HTTP server for testing the Nomulus Admin and Registrar consoles. */
|
/** Lightweight HTTP server for testing the Nomulus Admin and Registrar consoles. */
|
||||||
public final class RegistryTestServer {
|
public final class RegistryTestServer {
|
||||||
|
|
||||||
// In most cases, the current working dir is ${projectRoot}/gradle/${subproject}
|
private static final Path PROJECT_ROOT = getProjectRoot();
|
||||||
private static final String PROJECT_ROOT =
|
private static final Path RESOURCES_DIR = getResourcesDir();
|
||||||
Optional.ofNullable(System.getProperty("test.projectRoot")).orElse("../../");
|
|
||||||
private static final String RESOURCES_DIR =
|
|
||||||
Optional.ofNullable(System.getProperty("test.resourcesDir")).orElse("build/resources/main");
|
|
||||||
|
|
||||||
public static final ImmutableMap<String, Path> RUNFILES =
|
public static final ImmutableMap<String, Path> RUNFILES =
|
||||||
new ImmutableMap.Builder<String, Path>()
|
new ImmutableMap.Builder<String, Path>()
|
||||||
.put("/index.html", Paths.get(PROJECT_ROOT, "/java/google/registry/ui/html/index.html"))
|
.put("/index.html", PROJECT_ROOT.resolve("java/google/registry/ui/html/index.html"))
|
||||||
.put("/error.html", Paths.get(PROJECT_ROOT, "/java/google/registry/ui/html/error.html"))
|
.put("/error.html", PROJECT_ROOT.resolve("java/google/registry/ui/html/error.html"))
|
||||||
.put("/assets/js/*", Paths.get(RESOURCES_DIR, "/google/registry/ui"))
|
.put("/assets/js/*", RESOURCES_DIR.resolve("google/registry/ui"))
|
||||||
.put("/assets/css/*", Paths.get(RESOURCES_DIR, "/google/registry/ui/css"))
|
.put("/assets/css/*", RESOURCES_DIR.resolve("google/registry/ui/css"))
|
||||||
.put("/assets/sources/*", Paths.get(PROJECT_ROOT))
|
.put("/assets/sources/*", PROJECT_ROOT)
|
||||||
.put("/assets/*", Paths.get(PROJECT_ROOT, "/java/google/registry/ui/assets"))
|
.put("/assets/*", PROJECT_ROOT.resolve("java/google/registry/ui/assets"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private static final ImmutableList<Route> ROUTES =
|
private static final ImmutableList<Route> ROUTES =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue