Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ void commandHelpDescribesServerOptions() {
.contains("--port <port>"),
"help omits the port option");
assertTrue(output.toString()
.contains("--browse"),
"help omits the browse option");
.contains("--browse[=<type>]"),
"help omits the browse target option");
assertTrue(output.toString()
.contains("--version"),
"help omits the version option");
Expand All @@ -143,6 +143,17 @@ void commandHelpDescribesServerOptions() {
"help advertises classpath documentation");
}

@Test
void commandRejectsAnEmptyBrowseTarget() {
var output = new StringWriter();
var error = new StringWriter();

int result = new JdocServer().run(new PrintWriter(output), new PrintWriter(error), "--help", "--browse=");

assertEquals(2, result);
assertTrue(error.toString().contains("--browse requires a type"), error.toString());
}

@Test
void commandReportsStartupDetails() throws Exception {
Path temporary = Files.createTempDirectory("jdocserver-command-test-");
Expand Down Expand Up @@ -336,7 +347,7 @@ public final class Api {}
.followRedirects(Redirect.NORMAL)
.build();
HttpResponse<String> type = get(client, server.uri(handler.typeUri("example.module.Api")));
assertEquals(200, type.statusCode(), "module type request failed");
assertEquals(200, type.statusCode(), "module type request failed: " + type.body());
assertTrue(type.body()
.contains("Class Api"),
"module documentation is missing");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipException;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
Expand All @@ -56,9 +59,9 @@
import com.sun.source.tree.ProvidesTree;
import com.sun.source.tree.RequiresTree;
import com.sun.source.util.JavacTask;
import com.sun.tools.javac.api.JavacTool;

final class CompilationContext {
private final Provider<JavaCompiler> compiler;
private final List<String> toolArguments;
private final List<Path> modulePath;
private final List<Path> sourcePath;
Expand All @@ -69,6 +72,7 @@ final class CompilationContext {
private final boolean defaultJdk;

private CompilationContext(
Provider<JavaCompiler> compiler,
List<String> toolArguments,
List<Path> modulePath,
List<Path> sourcePath,
Expand All @@ -77,6 +81,7 @@ private CompilationContext(
Path system,
Path systemSources,
boolean defaultJdk) {
this.compiler = compiler;
this.toolArguments = List.copyOf(toolArguments);
this.modulePath = List.copyOf(modulePath);
this.sourcePath = List.copyOf(sourcePath);
Expand All @@ -87,7 +92,11 @@ private CompilationContext(
this.defaultJdk = defaultJdk;
}

static CompilationContext parse(List<String> arguments) {
static CompilationContext parse(List<String> arguments) throws IOException {
Provider<JavaCompiler> compiler = ServiceLoader.load(JavaCompiler.class)
.stream()
.findFirst()
.orElseThrow(() -> new IOException("The running JDK does not provide javac"));
var retained = new ArrayList<String>();
var classPath = new ArrayList<Path>();
var modulePath = new ArrayList<Path>();
Expand Down Expand Up @@ -162,8 +171,8 @@ static CompilationContext parse(List<String> arguments) {
if (!defaultJdk && moduleSourcePath.isEmpty()) {
throw new IllegalArgumentException("no modular documentation sources were provided");
}
return new CompilationContext(retained, modulePath, sourcePath, moduleSourcePath, requestedModules, system,
systemSources, defaultJdk);
return new CompilationContext(compiler, retained, modulePath, sourcePath, moduleSourcePath, requestedModules,
system, systemSources, defaultJdk);
}

boolean defaultJdk() {
Expand Down Expand Up @@ -344,7 +353,7 @@ private List<ModuleDescriptor> discoverSourceModules() throws IOException {
return List.copyOf(modules.values());
}

private static void addSourceModule(Path root, Map<String, ModuleDescriptor> modules) throws IOException {
private void addSourceModule(Path root, Map<String, ModuleDescriptor> modules) throws IOException {
byte[] source = read(root, "module-info.java");
if (source == null) {
return;
Expand Down Expand Up @@ -412,15 +421,15 @@ private Optional<ModuleDescriptor> sourceModule(String name) throws IOException
return Optional.empty();
}

private static ModuleDescriptor parseModuleDescriptor(byte[] bytes) throws IOException {
private ModuleDescriptor parseModuleDescriptor(byte[] bytes) throws IOException {
String content = new String(bytes, StandardCharsets.UTF_8);
JavaFileObject source = new SimpleJavaFileObject(URI.create("memory:///module-info.java"), Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return content;
}
};
JavacTask task = JavacTool.create().getTask(null, null, null, List.of("-proc:none"), null, List.of(source));
JavacTask task = (JavacTask) compiler.get().getTask(null, null, null, List.of("-proc:none"), null, List.of(source));
ModuleTree module = null;
for (var unit : task.parse()) {
if (unit.getModule() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -77,7 +79,12 @@
*/
public final class DocumentationHandler implements HttpHandler, AutoCloseable {
private static final OptionChecker OPTION_CHECKER = option -> {
DocumentationTool documentation = new JavadocTool();
DocumentationTool documentation = ServiceLoader.load(DocumentationTool.class)
.findFirst()
.orElse(null);
if (documentation == null) {
return -1;
}
int operands = documentation.isSupportedOption(option);
if (operands >= 0) {
return operands;
Expand All @@ -90,6 +97,7 @@ public final class DocumentationHandler implements HttpHandler, AutoCloseable {
};

private final CompilationContext context;
private final Provider<DocumentationTool> documentationTool;
private final List<String> inputArguments;
private final Path workspace;
private final Path generatedRoot;
Expand All @@ -101,6 +109,10 @@ public final class DocumentationHandler implements HttpHandler, AutoCloseable {
private volatile boolean closed;

private DocumentationHandler(List<String> arguments) throws IOException {
documentationTool = ServiceLoader.load(DocumentationTool.class)
.stream()
.findFirst()
.orElseThrow(() -> new IOException("The running JDK does not provide javadoc"));
inputArguments = expandArgumentFiles(arguments);
context = CompilationContext.parse(inputArguments);
workspace = Files.createTempDirectory("jdocserver-");
Expand Down Expand Up @@ -198,6 +210,17 @@ CompletableFuture<Void> startOverview() {
*/
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
handleRequest(exchange);
} catch (IOException failure) {
sendHtml(exchange, 500, errorPage(failure), exchange.getRequestMethod().equals("HEAD"));
} catch (RuntimeException | Error failure) {
failure.printStackTrace();
throw failure;
}
}

private void handleRequest(HttpExchange exchange) throws IOException {
if (closed) {
sendText(exchange, 503, "Documentation handler is closed");
return;
Expand All @@ -221,22 +244,18 @@ public void handle(HttpExchange exchange) throws IOException {
handleType(exchange, method.equals("HEAD"));
return;
}
try {
ensureOverview();
String relative = path.equals("/") ? "index.html" : path.substring(1);
if (serveFile(exchange, overviewRoot, relative, method.equals("HEAD"))) {
ensureOverview();
String relative = path.equals("/") ? "index.html" : path.substring(1);
if (serveFile(exchange, overviewRoot, relative, method.equals("HEAD"))) {
return;
}
if (relative.endsWith(".html")) {
String typeName = typeNameFromOverviewPath(relative);
if (typeName != null && materializeOverviewClass(typeName, relative) && serveFile(exchange, overviewRoot, relative, method.equals("HEAD"))) {
return;
}
if (relative.endsWith(".html")) {
String typeName = typeNameFromOverviewPath(relative);
if (typeName != null && materializeOverviewClass(typeName, relative) && serveFile(exchange, overviewRoot, relative, method.equals("HEAD"))) {
return;
}
}
sendText(exchange, 404, "Not found");
} catch (IOException failure) {
sendHtml(exchange, 500, errorPage(failure), method.equals("HEAD"));
}
sendText(exchange, 404, "Not found");
}

/**
Expand Down Expand Up @@ -274,9 +293,18 @@ private void ensureOverview() throws IOException {
overviewFuture().join();
} catch (CompletionException failure) {
Throwable cause = failure.getCause();
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
if (cause instanceof IOException io) {
throw io;
}
if (cause instanceof RuntimeException runtime) {
throw runtime;
}
if (cause instanceof Error error) {
throw error;
}
throw new IOException("Overview generation failed", cause);
}
}
Expand Down Expand Up @@ -365,7 +393,7 @@ private void generateOverview() throws IOException {
arguments.add("-d");
arguments.add(overviewRoot.toString());

var javadoc = new JavadocTool();
DocumentationTool javadoc = documentationTool.get();
var diagnostics = new StringWriter();
try (var systemSources = FileSystems.newFileSystem(context.systemSources());
var fileManager = javadoc.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
Expand Down Expand Up @@ -606,9 +634,18 @@ private GeneratedDocumentation documentation(String name) throws IOException {
.join();
} catch (CompletionException failure) {
Throwable cause = failure.getCause();
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
if (cause instanceof IOException io) {
throw io;
}
if (cause instanceof RuntimeException runtime) {
throw runtime;
}
if (cause instanceof Error error) {
throw error;
}
throw new IOException("Documentation generation failed", cause);
}
}
Expand Down Expand Up @@ -663,7 +700,7 @@ private GeneratedDocumentation generate(String requestedName) throws IOException
Files.createDirectories(output);
Files.write(sourceFile, source.bytes());

var javadoc = new JavadocTool();
DocumentationTool javadoc = documentationTool.get();
var arguments = new ArrayList<>(context.javadocArguments(type, sourceRoot));
if (documentationBase != null && Files.isRegularFile(overviewRoot.resolve("element-list"))) {
arguments.add("-linkoffline");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -106,7 +107,11 @@ public int run(PrintWriter out, PrintWriter err, String... arguments) {
out.println("URL " + uri);
out.flush();
if (options.browse()) {
browse(uri);
URI target = options.browseType()
.map(handler::typeUri)
.map(uri::resolve)
.orElse(uri);
browse(target);
}

var shutdownHook = new Thread(running::close, "jdocserver-shutdown");
Expand Down Expand Up @@ -194,7 +199,7 @@ private static void printHelp(PrintWriter out) {
Server options:
-b, --bind-address <address> Address to bind (default: 127.0.0.1)
--port <port> Port to listen on (default: 8000)
--browse Browse the documentation index
--browse[=<type>] Browse the index or a qualified type
-h, --help Print this help message
--version Print version information

Expand All @@ -216,11 +221,12 @@ private static void printHelp(PrintWriter out) {
}

private record Options(InetSocketAddress address, boolean defaultBinding, boolean browse,
boolean help, List<String> documentationArguments) {
Optional<String> browseType, boolean help, List<String> documentationArguments) {
static Options parse(String[] arguments) {
String bindAddress = null;
String port = null;
boolean browse = false;
String browseType = null;
boolean help = false;
var documentation = new ArrayList<String>();
for (int i = 0; i < arguments.length; i++) {
Expand All @@ -245,14 +251,20 @@ static Options parse(String[] arguments) {
bindAddress = argument.substring("--bind-address=".length());
} else if (argument.startsWith("--port=")) {
port = argument.substring("--port=".length());
} else if (argument.startsWith("--browse=")) {
browseType = argument.substring("--browse=".length());
if (browseType.isBlank()) {
throw new IllegalArgumentException("--browse requires a type after =");
}
browse = true;
} else {
documentation.add(argument);
}
}
}
}
return new Options(parseAddress(bindAddress, port), bindAddress == null, browse, help,
List.copyOf(documentation));
return new Options(parseAddress(bindAddress, port), bindAddress == null, browse,
Optional.ofNullable(browseType), help, List.copyOf(documentation));
}

private static InetSocketAddress parseAddress(String address, String value) {
Expand Down
3 changes: 3 additions & 0 deletions src/com.netflix.tools.jdocserver/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@
exports com.netflix.tools.jdocserver;
exports com.netflix.tools.jdocserver.internal to jdk.javadoc;

uses javax.tools.DocumentationTool;
uses javax.tools.JavaCompiler;

provides java.util.spi.ToolProvider with com.netflix.tools.jdocserver.JdocServer;
}