Merging javadoc feature branch changes to main (#715)

* Adds a gradle plugin to validate missing javadocs

Use `./gradlew missingJavadoc` to validate missing javadocs.
Currently this task fails because several modules are missing
appropriate javadocs. Once added, this should pass.
Also, precommit PomValidation check currently fails with missing Javadoc
plugin, that needs to be fixed -
https://github.com/opensearch-project/OpenSearch/issues/449
Thus keeping this in a separate feature branch.

Signed-off-by: Himanshu Setia <setiah@amazon.com>

* Fix Javadoc errors in module `client/rest` (#685)

* Fix Javadoc errors in client/rest module

Signed-off-by: Gregor Zurowski <gregor@zurowski.org>

* Add package info file in client/rest module

Signed-off-by: Gregor Zurowski <gregor@zurowski.org>

* Fix typos

Signed-off-by: Gregor Zurowski <gregor@zurowski.org>

* Add exception documentation to Javadoc

Signed-off-by: Gregor Zurowski <gregor@zurowski.org>

* Fixes precommit task configuration failures due to newly added missin… (#707)

* Fixes precommit task configuration failures due to newly added missingJavadoc task

Signed-off-by: Himanshu Setia <setiah@amazon.com>

* Fixes javadoc task errors due to PR#685

Signed-off-by: Himanshu Setia <setiah@amazon.com>

* Updated CONTRIBUTING.md for info on javadocs

Signed-off-by: Himanshu Setia <setiah@amazon.com>

* Correcting licenses and naming

Signed-off-by: Himanshu Setia <setiah@amazon.com>

* Correcting version info

Signed-off-by: Himanshu Setia <setiah@amazon.com>

Co-authored-by: Gregor Zurowski <gregor@zurowski.org>
This commit is contained in:
Himanshu Setia 2021-05-18 13:21:41 -07:00 committed by GitHub
parent 50abf6d066
commit 6f893ed1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 892 additions and 3 deletions

View File

@ -53,6 +53,7 @@ If you've thought of a way that OpenSearch could be better, we want to hear abou
### Documentation Changes
If you would like to contribute to the documentation, please see [OpenSearch Docs repo](https://github.com/opensearch-project/documentation-website/blob/main/README.md).
To contribute javadocs, please see [this open issue on github](https://github.com/opensearch-project/OpenSearch/issues/221).
### Contributing Code

View File

@ -58,6 +58,7 @@ apply from: 'gradle/formatting.gradle'
apply from: 'gradle/local-distribution.gradle'
apply from: 'gradle/fips.gradle'
apply from: 'gradle/run.gradle'
apply from: 'gradle/missing-javadoc.gradle'
// common maven publishing configuration
allprojects {

View File

@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/**
* A standalone process that will reap external services after a build dies.
*/
package org.opensearch.gradle.reaper;

View File

@ -45,6 +45,13 @@ public final class HasAttributeNodeSelector implements NodeSelector {
private final String key;
private final String value;
/**
* Create a {link HasAttributeNodeSelector} instance using the provided
* attribute key value pair.
*
* @param key The attribute name.
* @param value The attribute value.
*/
public HasAttributeNodeSelector(String key, String value) {
this.key = key;
this.value = value;

View File

@ -61,7 +61,10 @@ public class HeapBufferedAsyncResponseConsumer extends AbstractAsyncResponseCons
private volatile SimpleInputBuffer buf;
/**
* Creates a new instance of this consumer with the provided buffer limit
* Creates a new instance of this consumer with the provided buffer limit.
*
* @param bufferLimit the buffer limit. Must be greater than 0.
* @throws IllegalArgumentException if {@code bufferLimit} is less than or equal to 0.
*/
public HeapBufferedAsyncResponseConsumer(int bufferLimit) {
if (bufferLimit <= 0) {

View File

@ -66,6 +66,11 @@ public interface HttpAsyncResponseConsumerFactory {
private final int bufferLimit;
/**
* Creates a {@link HeapBufferedResponseConsumerFactory} instance with the given buffer limit.
*
* @param bufferLimitBytes the buffer limit to be applied to this instance
*/
public HeapBufferedResponseConsumerFactory(int bufferLimitBytes) {
this.bufferLimit = bufferLimitBytes;
}

View File

@ -77,6 +77,13 @@ public class Node {
* Create a {@linkplain Node} with metadata. All parameters except
* {@code host} are nullable and implementations of {@link NodeSelector}
* need to decide what to do in their absence.
*
* @param host primary host address
* @param boundHosts addresses on which the host is listening
* @param name name of the node
* @param version version of OpenSearch
* @param roles roles that the OpenSearch process has on the host
* @param attributes attributes declared on the node
*/
public Node(HttpHost host, Set<HttpHost> boundHosts, String name, String version,
Roles roles, Map<String, List<String>> attributes) {
@ -93,6 +100,8 @@ public class Node {
/**
* Create a {@linkplain Node} without any metadata.
*
* @param host primary host address
*/
public Node(HttpHost host) {
this(host, null, null, null, null, null);
@ -192,6 +201,11 @@ public class Node {
private final Set<String> roles;
/**
* Create a {@link Roles} instance of the given string set.
*
* @param roles set of role names.
*/
public Roles(final Set<String> roles) {
this.roles = new TreeSet<>(roles);
}

View File

@ -53,6 +53,8 @@ public interface NodeSelector {
* {@link RestClient} will call this method with a list of "dead" nodes.
* <p>
* Implementers should not rely on the ordering of the nodes.
*
* @param nodes the {@link Node}s targeted for the sending requests
*/
void select(Iterable<Node> nodes);
/*

View File

@ -47,6 +47,12 @@ public final class PreferHasAttributeNodeSelector implements NodeSelector {
private final String key;
private final String value;
/**
* Creates a {@link PreferHasAttributeNodeSelector} instance with the given key value pair.
*
* @param key attribute key
* @param value attribute value
*/
public PreferHasAttributeNodeSelector(String key, String value) {
this.key = key;
this.value = value;

View File

@ -94,6 +94,12 @@ public final class Request {
}
}
/**
* Add query parameters using the provided map of key value pairs.
*
* @param paramSource a map of key value pairs where the key is the url parameter.
* @throws IllegalArgumentException if a parameter with that name has already been set.
*/
public void addParameters(Map<String, String> paramSource){
paramSource.forEach(this::addParameter);
}
@ -110,6 +116,8 @@ public final class Request {
/**
* Set the body of the request. If not set or set to {@code null} then no
* body is sent with the request.
*
* @param entity the {@link HttpEntity} to be set as the body of the request.
*/
public void setEntity(HttpEntity entity) {
this.entity = entity;
@ -121,6 +129,8 @@ public final class Request {
* {@code Content-Type} will be sent as {@code application/json}.
* If you need a different content type then use
* {@link #setEntity(HttpEntity)}.
*
* @param entity JSON string to be set as the entity body of the request.
*/
public void setJsonEntity(String entity) {
setEntity(entity == null ? null : new NStringEntity(entity, ContentType.APPLICATION_JSON));
@ -137,6 +147,9 @@ public final class Request {
/**
* Set the portion of an HTTP request to OpenSearch that can be
* manipulated without changing OpenSearch's behavior.
*
* @param options the options to be set.
* @throws NullPointerException if {@code options} is null.
*/
public void setOptions(RequestOptions options) {
Objects.requireNonNull(options, "options cannot be null");
@ -146,6 +159,9 @@ public final class Request {
/**
* Set the portion of an HTTP request to OpenSearch that can be
* manipulated without changing OpenSearch's behavior.
*
* @param options the options to be set.
* @throws NullPointerException if {@code options} is null.
*/
public void setOptions(RequestOptions.Builder options) {
Objects.requireNonNull(options, "options cannot be null");

View File

@ -196,6 +196,10 @@ public final class RequestOptions {
/**
* Add the provided header to the request.
*
* @param name the header name
* @param value the header value
* @throws NullPointerException if {@code name} or {@code value} is null.
*/
public Builder addHeader(String name, String value) {
Objects.requireNonNull(name, "header name cannot be null");
@ -209,6 +213,9 @@ public final class RequestOptions {
* {@link HttpAsyncResponseConsumer} callback per retry. Controls how the
* response body gets streamed from a non-blocking HTTP connection on the
* client side.
*
* @param httpAsyncResponseConsumerFactory factory for creating {@link HttpAsyncResponseConsumer}.
* @throws NullPointerException if {@code httpAsyncResponseConsumerFactory} is null.
*/
public void setHttpAsyncResponseConsumerFactory(HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory) {
this.httpAsyncResponseConsumerFactory =
@ -231,6 +238,8 @@ public final class RequestOptions {
* {@linkplain WarningsHandler} to permit only certain warnings or to
* fail the request if the warnings returned don't
* <strong>exactly</strong> match some set.
*
* @param warningsHandler the {@link WarningsHandler} to be used
*/
public void setWarningsHandler(WarningsHandler warningsHandler) {
this.warningsHandler = warningsHandler;

View File

@ -96,6 +96,8 @@ public class Response {
* Returns the value of the first header with a specified name of this message.
* If there is more than one matching header in the message the first element is returned.
* If there is no matching header in the message <code>null</code> is returned.
*
* @param name header name
*/
public String getHeader(String name) {
Header header = response.getFirstHeader(name);

View File

@ -47,6 +47,11 @@ public final class ResponseException extends IOException {
private final Response response;
/**
* Creates a ResponseException containing the given {@code Response}.
*
* @param response The error response.
*/
public ResponseException(Response response) throws IOException {
super(buildMessage(response));
this.response = response;

View File

@ -44,7 +44,9 @@ package org.opensearch.client;
public interface ResponseListener {
/**
* Method invoked if the request yielded a successful response
* Method invoked if the request yielded a successful response.
*
* @param response success response
*/
void onSuccess(Response response);
@ -52,6 +54,8 @@ public interface ResponseListener {
* Method invoked if the request failed. There are two main categories of failures: connection failures (usually
* {@link java.io.IOException}s, or responses that were treated as errors based on their error response code
* ({@link ResponseException}s).
*
* @param exception the failure exception
*/
void onFailure(Exception exception);
}

View File

@ -195,6 +195,8 @@ public class RestClient implements Closeable {
* <p>
* Prefer this to {@link #builder(HttpHost...)} if you have metadata up front about the nodes.
* If you don't either one is fine.
*
* @param nodes The nodes that the client will send requests to.
*/
public static RestClientBuilder builder(Node... nodes) {
return new RestClientBuilder(nodes == null ? null : Arrays.asList(nodes));
@ -207,6 +209,8 @@ public class RestClient implements Closeable {
* You can use this if you do not have metadata up front about the nodes. If you do, prefer
* {@link #builder(Node...)}.
* @see Node#Node(HttpHost)
*
* @param hosts The hosts that the client will send requests to.
*/
public static RestClientBuilder builder(HttpHost... hosts) {
if (hosts == null || hosts.length == 0) {
@ -218,6 +222,8 @@ public class RestClient implements Closeable {
/**
* Replaces the nodes with which the client communicates.
*
* @param nodes the new nodes to communicate with.
*/
public synchronized void setNodes(Collection<Node> nodes) {
if (nodes == null || nodes.isEmpty()) {
@ -672,7 +678,14 @@ public class RestClient implements Closeable {
*/
public static class FailureListener {
/**
* Notifies that the node provided as argument has just failed
* Create a {@link FailureListener} instance.
*/
public FailureListener() {}
/**
* Notifies that the node provided as argument has just failed.
*
* @param node The node which has failed.
*/
public void onFailure(Node node) {}
}
@ -907,6 +920,11 @@ public class RestClient implements Closeable {
*/
public static class ContentCompressingEntity extends GzipCompressingEntity {
/**
* Creates a {@link ContentCompressingEntity} instance with the provided HTTP entity.
*
* @param entity the HTTP entity.
*/
public ContentCompressingEntity(HttpEntity entity) {
super(entity);
}

View File

@ -53,9 +53,24 @@ import java.util.Objects;
* {@link org.apache.http.nio.client.HttpAsyncClient} in case additional customization is needed.
*/
public final class RestClientBuilder {
/**
* The default connection timout in milliseconds.
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 1000;
/**
* The default socket timeout in milliseconds.
*/
public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 30000;
/**
* The default maximum of connections per route.
*/
public static final int DEFAULT_MAX_CONN_PER_ROUTE = 10;
/**
* The default maximum total connections.
*/
public static final int DEFAULT_MAX_CONN_TOTAL = 30;
private static final Header[] EMPTY_HEADERS = new Header[0];
@ -92,6 +107,7 @@ public final class RestClientBuilder {
* <p>
* Request-time headers will always overwrite any default headers.
*
* @param defaultHeaders array of default header
* @throws NullPointerException if {@code defaultHeaders} or any header is {@code null}.
*/
public RestClientBuilder setDefaultHeaders(Header[] defaultHeaders) {
@ -106,6 +122,7 @@ public final class RestClientBuilder {
/**
* Sets the {@link RestClient.FailureListener} to be notified for each request failure
*
* @param failureListener the {@link RestClient.FailureListener} for each failure
* @throws NullPointerException if {@code failureListener} is {@code null}.
*/
public RestClientBuilder setFailureListener(RestClient.FailureListener failureListener) {
@ -117,6 +134,7 @@ public final class RestClientBuilder {
/**
* Sets the {@link HttpClientConfigCallback} to be used to customize http client configuration
*
* @param httpClientConfigCallback the {@link HttpClientConfigCallback} to be used
* @throws NullPointerException if {@code httpClientConfigCallback} is {@code null}.
*/
public RestClientBuilder setHttpClientConfigCallback(HttpClientConfigCallback httpClientConfigCallback) {
@ -128,6 +146,7 @@ public final class RestClientBuilder {
/**
* Sets the {@link RequestConfigCallback} to be used to customize http client configuration
*
* @param requestConfigCallback the {@link RequestConfigCallback} to be used
* @throws NullPointerException if {@code requestConfigCallback} is {@code null}.
*/
public RestClientBuilder setRequestConfigCallback(RequestConfigCallback requestConfigCallback) {
@ -145,6 +164,7 @@ public final class RestClientBuilder {
* OpenSearch is behind a proxy that provides a base path or a proxy that requires all paths to start with '/';
* it is not intended for other purposes and it should not be supplied in other scenarios.
*
* @param pathPrefix the path prefix for every request.
* @throws NullPointerException if {@code pathPrefix} is {@code null}.
* @throws IllegalArgumentException if {@code pathPrefix} is empty, or ends with more than one '/'.
*/
@ -153,6 +173,14 @@ public final class RestClientBuilder {
return this;
}
/**
* Cleans up the given path prefix to ensure that looks like "/base/path".
*
* @param pathPrefix the path prefix to be cleaned up.
* @return the cleaned up path prefix.
* @throws NullPointerException if {@code pathPrefix} is {@code null}.
* @throws IllegalArgumentException if {@code pathPrefix} is empty, or ends with more than one '/'.
*/
public static String cleanPathPrefix(String pathPrefix) {
Objects.requireNonNull(pathPrefix, "pathPrefix must not be null");
@ -178,6 +206,8 @@ public final class RestClientBuilder {
/**
* Sets the {@link NodeSelector} to be used for all requests.
*
* @param nodeSelector the {@link NodeSelector} to be used
* @throws NullPointerException if the provided nodeSelector is null
*/
public RestClientBuilder setNodeSelector(NodeSelector nodeSelector) {
@ -189,6 +219,8 @@ public final class RestClientBuilder {
/**
* Whether the REST client should return any response containing at least
* one warning header as a failure.
*
* @param strictDeprecationMode flag for enabling strict deprecation mode
*/
public RestClientBuilder setStrictDeprecationMode(boolean strictDeprecationMode) {
this.strictDeprecationMode = strictDeprecationMode;
@ -198,6 +230,8 @@ public final class RestClientBuilder {
/**
* Whether the REST client should compress requests using gzip content encoding and add the "Accept-Encoding: gzip"
* header to receive compressed responses.
*
* @param compressionEnabled flag for enabling compression
*/
public RestClientBuilder setCompressionEnabled(boolean compressionEnabled) {
this.compressionEnabled = compressionEnabled;
@ -254,6 +288,8 @@ public final class RestClientBuilder {
* Allows to customize the {@link RequestConfig} that will be used with each request.
* It is common to customize the different timeout values through this method without losing any other useful default
* value that the {@link RestClientBuilder} internally sets.
*
* @param requestConfigBuilder the {@link RestClientBuilder} for customizing the request configuration.
*/
RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder);
}
@ -269,6 +305,8 @@ public final class RestClientBuilder {
* Commonly used to customize the default {@link org.apache.http.client.CredentialsProvider} for authentication
* or the {@link SchemeIOSessionStrategy} for communication through ssl without losing any other useful default
* value that the {@link RestClientBuilder} internally sets, like connection pooling.
*
* @param httpClientBuilder the {@link HttpClientBuilder} for customizing the client instance.
*/
HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder);
}

View File

@ -47,6 +47,12 @@ public final class WarningFailureException extends RuntimeException {
private final Response response;
/**
* Creates a {@link WarningFailureException} instance.
*
* @param response the response that contains warnings.
* @throws IOException if there is a problem building the exception message.
*/
public WarningFailureException(Response response) throws IOException {
super(buildMessage(response));
this.response = response;
@ -56,6 +62,8 @@ public final class WarningFailureException extends RuntimeException {
* Wrap a {@linkplain WarningFailureException} with another one with the current
* stack trace. This is used during synchronous calls so that the caller
* ends up in the stack trace of the exception thrown.
*
* @param e the exception to be wrapped.
*/
WarningFailureException(WarningFailureException e) {
super(e.getMessage(), e);

View File

@ -39,8 +39,18 @@ import java.util.List;
* request.
*/
public interface WarningsHandler {
/**
* Determines whether the given list of warnings should fail the request.
*
* @param warnings a list of warnings.
* @return boolean indicating if the request should fail.
*/
boolean warningsShouldFailRequest(List<String> warnings);
/**
* The permissive warnings handler. Warnings will not fail the request.
*/
WarningsHandler PERMISSIVE = new WarningsHandler() {
@Override
public boolean warningsShouldFailRequest(List<String> warnings) {
@ -52,6 +62,10 @@ public interface WarningsHandler {
return "permissive";
}
};
/**
* The strict warnings handler. Warnings will fail the request.
*/
WarningsHandler STRICT = new WarningsHandler() {
@Override
public boolean warningsShouldFailRequest(List<String> warnings) {

View File

@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/**
* REST client that connects to an OpenSearch cluster.
*/
package org.opensearch.client;

View File

@ -54,5 +54,6 @@ testingConventions {
}
javadoc.enabled = false
missingJavadoc.enabled = false
loggerUsageCheck.enabled = false
jarHell.enabled = false

11
doc-tools/build.gradle Normal file
View File

@ -0,0 +1,11 @@
plugins {
id 'java'
}
group 'org.opensearch'
version '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
}

View File

@ -0,0 +1,15 @@
plugins {
id 'java-library'
}
group 'org.opensearch'
version '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
}
tasks.withType(JavaCompile) {
options.compilerArgs += ["--release", targetCompatibility.toString()]
options.encoding = "UTF-8"
}

View File

@ -0,0 +1,432 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
package org.opensearch.missingdoclet;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.ParamTree;
import com.sun.source.util.DocTrees;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import jdk.javadoc.doclet.StandardDoclet;
/**
* Checks for missing javadocs, where missing also means "only whitespace" or "license header".
* Has option --missing-level (package, class, method, parameter) so that we can improve over time.
* Has option --missing-ignore to ignore individual elements (such as split packages).
* It isn't recursive, just ignores exactly the elements you tell it.
* Has option --missing-method to apply "method" level to selected packages (fix one at a time).
* Matches package names exactly: so you'll need to list subpackages separately.
*
* Note: This by default ignores javadoc validation on overridden methods.
*/
// Original version of this class is ported from MissingDoclet code in Lucene,
// which is under the Apache Software Foundation under Apache 2.0 license
// See - https://github.com/apache/lucene-solr/tree/master/dev-tools/missing-doclet
public class MissingDoclet extends StandardDoclet {
// checks that modules and packages have documentation
private static final int PACKAGE = 0;
// checks that classes, interfaces, enums, and annotation types have documentation
private static final int CLASS = 1;
// checks that methods, constructors, fields, and enumerated constants have documentation
private static final int METHOD = 2;
// checks that @param tags are present for any method/constructor parameters
private static final int PARAMETER = 3;
int level = PARAMETER;
Reporter reporter;
DocletEnvironment docEnv;
DocTrees docTrees;
Elements elementUtils;
Set<String> ignored = Collections.emptySet();
Set<String> methodPackages = Collections.emptySet();
@Override
public Set<Doclet.Option> getSupportedOptions() {
Set<Doclet.Option> options = new HashSet<>();
options.addAll(super.getSupportedOptions());
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "level to enforce for missing javadocs: [package, class, method, parameter]";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-level");
}
@Override
public String getParameters() {
return "level";
}
@Override
public boolean process(String option, List<String> arguments) {
switch (arguments.get(0)) {
case "package":
level = PACKAGE;
return true;
case "class":
level = CLASS;
return true;
case "method":
level = METHOD;
return true;
case "parameter":
level = PARAMETER;
return true;
default:
return false;
}
}
});
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "comma separated list of element names to ignore (e.g. as a workaround for split packages)";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-ignore");
}
@Override
public String getParameters() {
return "ignoredNames";
}
@Override
public boolean process(String option, List<String> arguments) {
ignored = new HashSet<>(Arrays.asList(arguments.get(0).split(",")));
return true;
}
});
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "comma separated list of packages to check at 'method' level";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-method");
}
@Override
public String getParameters() {
return "packages";
}
@Override
public boolean process(String option, List<String> arguments) {
methodPackages = new HashSet<>(Arrays.asList(arguments.get(0).split(",")));
return true;
}
});
return options;
}
@Override
public void init(Locale locale, Reporter reporter) {
this.reporter = reporter;
super.init(locale, reporter);
}
@Override
public boolean run(DocletEnvironment docEnv) {
this.docEnv = docEnv;
this.docTrees = docEnv.getDocTrees();
this.elementUtils = docEnv.getElementUtils();
for (var element : docEnv.getIncludedElements()) {
check(element);
}
return true;
}
/**
* Returns effective check level for this element
*/
private int level(Element element) {
String pkg = elementUtils.getPackageOf(element).getQualifiedName().toString();
if (methodPackages.contains(pkg)) {
return METHOD;
} else {
return level;
}
}
/**
* Check an individual element.
* This checks packages and types from the doctrees.
* It will recursively check methods/fields from encountered types when the level is "method"
*/
private void check(Element element) {
switch(element.getKind()) {
case MODULE:
// don't check the unnamed module, it won't have javadocs
if (!((ModuleElement)element).isUnnamed()) {
checkComment(element);
}
break;
case PACKAGE:
checkComment(element);
break;
// class-like elements, check them, then recursively check their children (fields and methods)
case CLASS:
case INTERFACE:
case ENUM:
case ANNOTATION_TYPE:
if (level(element) >= CLASS) {
checkComment(element);
for (var subElement : element.getEnclosedElements()) {
// don't recurse into enclosed types, otherwise we'll double-check since they are already in the included docTree
if (subElement.getKind() == ElementKind.METHOD ||
subElement.getKind() == ElementKind.CONSTRUCTOR ||
subElement.getKind() == ElementKind.FIELD ||
subElement.getKind() == ElementKind.ENUM_CONSTANT) {
check(subElement);
}
}
}
break;
// method-like elements, check them if we are configured to do so
case METHOD:
case CONSTRUCTOR:
case FIELD:
case ENUM_CONSTANT:
if (level(element) >= METHOD && !isSyntheticEnumMethod(element)) {
checkComment(element);
}
break;
default:
error(element, "I don't know how to analyze " + element.getKind() + " yet.");
}
}
/**
* Return true if the method is synthetic enum method (values/valueOf).
* According to the doctree documentation, the "included" set never includes synthetic elements.
* UweSays: It should not happen but it happens!
*/
private boolean isSyntheticEnumMethod(Element element) {
String simpleName = element.getSimpleName().toString();
if (simpleName.equals("values") || simpleName.equals("valueOf")) {
if (element.getEnclosingElement().getKind() == ElementKind.ENUM) {
return true;
}
}
return false;
}
/**
* Checks that an element doesn't have missing javadocs.
* In addition to truly "missing", check that comments aren't solely whitespace (generated by some IDEs)
*/
private void checkComment(Element element) {
// sanity check that the element is really "included", because we do some recursion into types
if (!docEnv.isIncluded(element)) {
return;
}
// check that this element isn't on our ignore list. This is only used as a workaround for "split packages".
// ignoring a package isn't recursive (on purpose), we still check all the classes, etc. inside it.
// we just need to cope with the fact package-info.java isn't there because it is split across multiple jars.
if (ignored.contains(element.toString())) {
return;
}
var tree = docTrees.getDocCommentTree(element);
if (tree == null || tree.getFirstSentence().isEmpty()) {
// Check for methods that override other stuff and perhaps inherit their Javadocs.
if (hasInheritedJavadocs(element)) {
return;
} else {
error(element, "javadocs are missing");
}
} else {
var normalized = tree.getFirstSentence().get(0).toString()
.replace('\u00A0', ' ')
.trim()
.toLowerCase(Locale.ROOT);
if (normalized.isEmpty()) {
error(element, "blank javadoc comment");
}
}
if (level >= PARAMETER) {
checkParameters(element, tree);
}
}
private boolean hasInheritedJavadocs(Element element) {
boolean hasOverrides = element.getAnnotationMirrors().stream()
.anyMatch(ann -> ann.getAnnotationType().toString().equals(Override.class.getName()));
if (hasOverrides) {
// If an element has explicit @Overrides annotation, assume it does
// have inherited javadocs somewhere.
reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but @Override declared, skipping.");
return true;
}
// Check for methods up the types tree.
if (element instanceof ExecutableElement) {
ExecutableElement thisMethod = (ExecutableElement) element;
Iterable<Element> superTypes =
() -> superTypeForInheritDoc(thisMethod.getEnclosingElement()).iterator();
for (Element sup : superTypes) {
for (ExecutableElement supMethod : ElementFilter.methodsIn(sup.getEnclosedElements())) {
TypeElement clazz = (TypeElement) thisMethod.getEnclosingElement();
if (elementUtils.overrides(thisMethod, supMethod, clazz)) {
// We could check supMethod for non-empty javadoc here. Don't know if this makes
// sense though as all methods will be verified in the end so it'd fail on the
// top of the hierarchy (if empty) anyway.
reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but method overrides another, skipping.");
return true;
}
}
}
}
return false;
}
/* Find types from which methods in type may inherit javadoc, in the proper order.*/
private Stream<Element> superTypeForInheritDoc(Element type) {
TypeElement clazz = (TypeElement) type;
List<Element> interfaces = clazz.getInterfaces()
.stream()
.filter(tm -> tm.getKind() == TypeKind.DECLARED)
.map(tm -> ((DeclaredType) tm).asElement())
.collect(Collectors.toList());
Stream<Element> result = interfaces.stream();
result = Stream.concat(result, interfaces.stream().flatMap(this::superTypeForInheritDoc));
if (clazz.getSuperclass().getKind() == TypeKind.DECLARED) {
Element superClass = ((DeclaredType) clazz.getSuperclass()).asElement();
result = Stream.concat(result, Stream.of(superClass));
result = Stream.concat(result, superTypeForInheritDoc(superClass));
}
return result;
}
/** Checks there is a corresponding "param" tag for each method parameter */
private void checkParameters(Element element, DocCommentTree tree) {
if (element instanceof ExecutableElement) {
// record each @param that we see
Set<String> seenParameters = new HashSet<>();
if (tree != null) {
for (var tag : tree.getBlockTags()) {
if (tag instanceof ParamTree) {
var name = ((ParamTree)tag).getName().getName().toString();
seenParameters.add(name);
}
}
}
// now compare the method's formal parameter list against it
for (var param : ((ExecutableElement)element).getParameters()) {
var name = param.getSimpleName().toString();
if (!seenParameters.contains(name)) {
error(element, "missing javadoc @param for parameter '" + name + "'");
}
}
}
}
/** logs a new error for the particular element */
private void error(Element element, String message) {
var fullMessage = new StringBuilder();
switch (element.getKind()) {
case MODULE:
case PACKAGE:
// for modules/packages, we don't have filename + line number, fully qualify
fullMessage.append(element.toString());
break;
case METHOD:
case CONSTRUCTOR:
case FIELD:
case ENUM_CONSTANT:
// for method-like elements, include the enclosing type to make it easier
fullMessage.append(element.getEnclosingElement().getSimpleName());
fullMessage.append(".");
fullMessage.append(element.getSimpleName());
break;
default:
// for anything else, use a simple name
fullMessage.append(element.getSimpleName());
break;
}
fullMessage.append(" (");
fullMessage.append(element.getKind().toString().toLowerCase(Locale.ROOT));
fullMessage.append("): ");
fullMessage.append(message);
if (Runtime.version().feature() == 11 && element.getKind() == ElementKind.PACKAGE) {
// Avoid JDK 11 bug:
// https://bugs.openjdk.java.net/browse/JDK-8224082
reporter.print(Diagnostic.Kind.ERROR, fullMessage.toString());
} else {
reporter.print(Diagnostic.Kind.ERROR, element, fullMessage.toString());
}
}
}

View File

@ -0,0 +1,242 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
import javax.annotation.Nullable
/**
* Checks for missing javadocs.
*/
// Original version of this file is ported from the render-javadoc code in Lucene,
// original code under the Apache Software Foundation under Apache 2.0 license
// See - https://github.com/apache/lucene-solr/tree/master/gradle/documentation/render-javadoc
allprojects {
ext {
scriptResources = { buildscript ->
return file(buildscript.sourceFile.absolutePath.replaceAll('.gradle$', ""))
}
}
}
def resources = scriptResources(buildscript)
allprojects {
plugins.withType(JavaPlugin) {
configurations {
missingdoclet
}
dependencies {
missingdoclet "org.opensearch:missing-doclet"
}
ext {
relativeDocPath = project.path.replaceFirst(/:\w+:/, "").replace(':', '/')
docroot = file("${buildDir}/site")
}
// TODO: Add missingJavadoc checks to precommit plugin
// See https://github.com/opensearch-project/OpenSearch/issues/221
// Currently the missingJavadoc task fails due to missing documentation
// across multiple modules. Once javadocs are added, we can
// add this task to precommit plugin.
tasks.withType(MissingJavadocTask).configureEach {
enabled = true
title = "${project.rootProject.name} ${project.name} API"
// Set up custom doclet.
dependsOn configurations.missingdoclet
docletpath = configurations.missingdoclet
}
tasks.register('missingJavadoc', MissingJavadocTask) {
description "This task validates and generates Javadoc API documentation for the main source code."
group "documentation"
taskResources = resources
dependsOn sourceSets.main.compileClasspath
classpath = sourceSets.main.compileClasspath
srcDirSet = sourceSets.main.java
outputDir = project.javadoc.destinationDir
}
}
}
class MissingJavadocTask extends DefaultTask {
@InputFiles
@SkipWhenEmpty
SourceDirectorySet srcDirSet;
@OutputDirectory
File outputDir
@CompileClasspath
FileCollection classpath
@CompileClasspath
FileCollection docletpath
@Input
String title
@Input
boolean linksource = false
@Input
boolean relativeProjectLinks = false
@Input
String javadocMissingLevel = "parameter"
// anything in these packages is checked with level=method. This allows iteratively fixing one package at a time.
@Input
List<String> javadocMissingMethod = []
// default is not to ignore any elements, should only be used to workaround split packages
@Input
List<String> javadocMissingIgnore = []
@Nullable
@Optional
@Input
def executable
@Input
def taskResources
/** Utility method to recursively collect all tasks with same name like this one that we depend on */
private Set findRenderTasksInDependencies() {
Set found = []
def collectDeps
collectDeps = { task -> task.taskDependencies.getDependencies(task).findAll{ it.name == this.name && it.enabled && !found.contains(it) }.each{
found << it
collectDeps(it)
}}
collectDeps(this)
return found
}
@TaskAction
void render() {
def srcDirs = srcDirSet.srcDirs.findAll { dir -> dir.exists() }
def optionsFile = project.file("${getTemporaryDir()}/javadoc-options.txt")
// create the directory, so relative link calculation knows that it's a directory:
outputDir.mkdirs();
def opts = []
opts << [ '-overview', project.file("${srcDirs[0]}/overview.html") ]
opts << [ '-sourcepath', srcDirs.join(File.pathSeparator) ]
if (project.getGroup().toString().startsWith('org.opensearch')) {
opts << [ '-subpackages', 'org.opensearch']
} else {
opts << [ '-subpackages', project.getGroup().toString()]
}
opts << [ '-d', outputDir ]
opts << '-protected'
opts << [ '-encoding', 'UTF-8' ]
opts << [ '-charset', 'UTF-8' ]
opts << [ '-docencoding', 'UTF-8' ]
opts << '-noindex'
opts << '-author'
opts << '-version'
if (linksource) {
opts << '-linksource'
}
opts << '-use'
opts << [ '-locale', 'en_US' ]
opts << [ '-windowtitle', title ]
opts << [ '-doctitle', title ]
if (!classpath.isEmpty()) {
opts << [ '-classpath', classpath.asPath ]
}
opts << [ '-doclet', "org.opensearch.missingdoclet.MissingDoclet" ]
opts << [ '-docletpath', docletpath.asPath ]
opts << [ '--missing-level', javadocMissingLevel ]
if (javadocMissingIgnore) {
opts << [ '--missing-ignore', String.join(',', javadocMissingIgnore) ]
}
if (javadocMissingMethod) {
opts << [ '--missing-method', String.join(',', javadocMissingMethod) ]
}
opts << [ '-quiet' ]
opts << [ '--release', 11 ]
opts << '-Xdoclint:all,-missing'
// Temporary file that holds all javadoc options for the current task.
optionsFile.withWriter("UTF-8", { writer ->
// escapes an option with single quotes or whitespace to be passed in the options.txt file for
def escapeJavadocOption = { String s -> (s =~ /[ '"]/) ? ("'" + s.replaceAll(/[\\'"]/, /\\$0/) + "'") : s }
opts.each { entry ->
if (entry instanceof List) {
writer.write(entry.collect { escapeJavadocOption(it as String) }.join(" "))
} else {
writer.write(escapeJavadocOption(entry as String))
}
writer.write('\n')
}
})
def javadocCmd = {
if (executable == null) {
JavaInstallationRegistry registry = project.extensions.getByType(JavaInstallationRegistry)
JavaInstallation currentJvm = registry.installationForCurrentVirtualMachine.get()
return currentJvm.jdk.get().javadocExecutable.asFile
} else {
return project.file(executable)
}
}()
def outputFile = project.file("${getTemporaryDir()}/javadoc-output.txt")
def result
outputFile.withOutputStream { output ->
result = project.exec {
executable javadocCmd
// we want to capture both stdout and stderr to the same
// stream but gradle attempts to close these separately
// (it has two independent pumping threads) and it can happen
// that one still tries to write something when the other closed
// the underlying output stream.
def wrapped = new java.io.FilterOutputStream(output) {
public void close() {
// no-op. we close this stream manually.
}
}
standardOutput = wrapped
errorOutput = wrapped
args += [ "@${optionsFile}" ]
// -J flags can't be passed via options file... (an error "javadoc: error - invalid flag: -J-Xmx512m" occurs.)
args += [ "-J-Xmx512m" ]
// force locale to be "en_US" (fix for: https://bugs.openjdk.java.net/browse/JDK-8222793)
args += [ "-J-Duser.language=en", "-J-Duser.country=US" ]
ignoreExitValue true
}
}
if (result.getExitValue() != 0) {
// Pipe the output to console. Intentionally skips any encoding conversion
// and pumps raw bytes.
System.out.write(outputFile.bytes)
def cause
try {
result.rethrowFailure()
} catch (ex) {
cause = ex
}
throw new GradleException("Javadoc generation failed for ${project.path},\n Options file at: ${optionsFile}\n Command output at: ${outputFile}", cause)
}
}
}

View File

@ -15,6 +15,9 @@ plugins {
rootProject.name = "OpenSearch"
include 'doc-tools'
includeBuild("doc-tools/missing-doclet")
List projects = [
'build-tools',
'build-tools:reaper',
@ -128,3 +131,4 @@ if (extraProjects.exists()) {
addSubProjects('', extraProjectDir)
}
}

View File

@ -33,6 +33,7 @@ apply plugin: 'opensearch.test.fixtures'
description = 'Fixture for Azure external service'
test.enabled = false
group = 'fixture.azure'
dependencies {
api project(':server')
}

View File

@ -37,6 +37,7 @@ dependencies {
api project(':server')
}
group = 'fixture.gcs'
preProcessFixture {
dependsOn jar, configurations.runtimeClasspath
doLast {

View File

@ -30,6 +30,8 @@
apply plugin: 'opensearch.java'
group = 'hdfs'
dependencies {
api "org.apache.hadoop:hadoop-minicluster:2.10.1"
}

View File

@ -37,6 +37,8 @@ a "ports" file with the port on which Elasticsearch is running.
apply plugin: 'opensearch.java'
test.enabled = false
group = 'oldes'
dependencies {
// Just for the constants....
api "org.apache.lucene:lucene-core:${versions.lucene}"

View File

@ -32,6 +32,7 @@ apply plugin: 'opensearch.test.fixtures'
description = 'Fixture for S3 Storage service'
test.enabled = false
group = 'fixture.s3'
dependencies {
api project(':server')