diff --git a/impl/openapi/pom.xml b/impl/openapi/pom.xml index 4cc623cb..9c8bbb41 100644 --- a/impl/openapi/pom.xml +++ b/impl/openapi/pom.xml @@ -7,13 +7,6 @@ serverlessworkflow-impl-openapi Serverless Workflow :: Impl :: OpenAPI - - - 1.8.1 - 3.20.0 - 1.20.0 - - jakarta.ws.rs @@ -27,35 +20,15 @@ io.serverlessworkflow serverlessworkflow-impl-http - - io.swagger.parser.v3 - swagger-parser - ${version.io.swagger.parser.v3} - - - - - - org.mozilla - rhino - ${version.org.mozilla.rhino} - - - org.apache.commons - commons-lang3 - ${version.org.apache.commons.lang3} - - - commons-codec - commons-codec - ${version.commons.codec} - - org.junit.jupiter junit-jupiter-engine test + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + org.junit.jupiter junit-jupiter-params diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPI.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPI.java new file mode 100644 index 00000000..de503da6 --- /dev/null +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPI.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 io.serverlessworkflow.impl.executors.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class OpenAPI { + + public enum SwaggerVersion { + SWAGGER_V2, + OPENAPI_V3 + } + + private final JsonNode root; + private final boolean isSwaggerV2; + + public OpenAPI(JsonNode root) { + this.root = Objects.requireNonNull(root, "root cannot be null"); + this.isSwaggerV2 = isSwaggerV2(); + this.validatePaths(); + this.moveRequestInBodyToRequestBody(); + } + + private void moveRequestInBodyToRequestBody() { + if (!isSwaggerV2) { + return; + } + + JsonNode pathsNode = root.get("paths"); + if (pathsNode == null || !pathsNode.isObject()) { + return; + } + + Iterator pathNames = pathsNode.fieldNames(); + pathNames.forEachRemaining( + pathName -> { + JsonNode pathNode = pathsNode.get(pathName); + if (pathNode == null || !pathNode.isObject()) { + return; + } + Iterator methodNames = pathNode.fieldNames(); + methodNames.forEachRemaining( + methodName -> { + JsonNode operationNode = pathNode.get(methodName); + if (operationNode != null && operationNode.isObject()) { + processOperationNode((ObjectNode) operationNode); + } + }); + }); + } + + private void processOperationNode(ObjectNode operationNode) { + JsonNode parametersNode = operationNode.get("parameters"); + if (parametersNode == null || !parametersNode.isArray()) { + return; + } + + ArrayNode originalParameters = (ArrayNode) parametersNode; + ArrayNode filteredParameters = originalParameters.arrayNode(); + boolean requestBodyCreated = false; + + for (JsonNode parameterNode : originalParameters) { + if (parameterNode == null || !parameterNode.isObject()) { + filteredParameters.add(parameterNode); + continue; + } + + JsonNode inNode = parameterNode.get("in"); + if (inNode != null && "body".equals(inNode.asText())) { + if (!requestBodyCreated) { + ObjectNode requestBodyNode = operationNode.putObject("requestBody"); + ObjectNode contentNode = requestBodyNode.putObject("content"); + ObjectNode mediaTypeNode = contentNode.putObject("application/json"); + ObjectNode schemaNode = mediaTypeNode.putObject("schema"); + JsonNode schemaFromParam = parameterNode.get("schema"); + if (schemaFromParam != null && schemaFromParam.isObject()) { + schemaNode.setAll((ObjectNode) schemaFromParam); + } + requestBodyCreated = true; + } + } else { + filteredParameters.add(parameterNode); + } + } + + operationNode.set("parameters", filteredParameters); + } + + private void validatePaths() { + if (!root.has("paths")) { + throw new IllegalArgumentException("OpenAPI document must contain 'paths' field"); + } + } + + private boolean isSwaggerV2() { + JsonNode swaggerNode = root.get("swagger"); + return swaggerNode != null && swaggerNode.asText().startsWith("2.0"); + } + + public PathItemInfo findOperationById(String operationId) { + JsonNode paths = root.get("paths"); + + Set> properties = paths.properties(); + + for (Map.Entry path : properties) { + JsonNode pathNode = path.getValue(); + Set> methods = pathNode.properties(); + for (Map.Entry method : methods) { + JsonNode operationNode = method.getValue().get("operationId"); + if (operationNode != null && operationNode.asText().equals(operationId)) { + return new PathItemInfo(path.getKey(), path.getValue(), method.getKey()); + } + } + } + throw new IllegalArgumentException("Operation with ID " + operationId + " not found"); + } + + public List getServers() { + + if (isSwaggerV2) { + if (root.has("host")) { + String host = root.get("host").asText(); + String basePath = root.has("basePath") ? root.get("basePath").asText() : ""; + String scheme = "http"; + if (root.has("schemes") + && root.get("schemes").isArray() + && !root.get("schemes").isEmpty()) { + scheme = root.get("schemes").get(0).asText(); + } + return List.of(scheme + "://" + host + basePath); + } else { + return List.of(); + } + } + + return root.has("servers") ? List.of(root.get("servers").findPath("url").asText()) : List.of(); + } + + public SwaggerVersion getSwaggerVersion() { + return isSwaggerV2 ? SwaggerVersion.SWAGGER_V2 : SwaggerVersion.OPENAPI_V3; + } + + public JsonNode resolveSchema(String ref) { + if (!ref.startsWith("#/")) { + throw new IllegalArgumentException("Only local references are supported"); + } + String[] parts = ref.substring(2).split("/"); + JsonNode currentNode = root; + for (String part : parts) { + currentNode = currentNode.get(part); + if (currentNode == null) { + throw new IllegalArgumentException("Reference " + ref + " could not be resolved"); + } + } + return currentNode; + } + + public record PathItemInfo(String path, JsonNode operation, String method) {} +} diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java index da3e369a..22814f1a 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.impl.executors.openapi; +import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.ExternalResource; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; @@ -24,7 +25,6 @@ import io.serverlessworkflow.impl.executors.http.HttpExecutor; import io.serverlessworkflow.impl.executors.http.HttpExecutorBuilder; import io.serverlessworkflow.impl.resources.ResourceLoaderUtils; -import io.swagger.v3.oas.models.media.Schema; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -113,8 +113,8 @@ private void fillHttpBuilder(WorkflowApplication application, OperationDefinitio if (!missingParams.isEmpty()) { throw new IllegalArgumentException( "Missing required OpenAPI parameters for operation '" - + (operation.getOperation().getOperationId() != null - ? operation.getOperation().getOperationId() + + (operation.getOperation().get("operationId") != null + ? operation.getOperation().get("operationId").asText() : "" + "': ") + missingParams); } @@ -135,8 +135,9 @@ private void param( if (origMap.containsKey(name)) { collectorMap.put(parameter.getName(), origMap.remove(name)); } else if (parameter.getRequired()) { - Schema schema = parameter.getSchema(); - Object defaultValue = schema != null ? schema.getDefault() : null; + + JsonNode schema = parameter.getSchema(); + Object defaultValue = schema != null ? schema.get("default") : null; if (defaultValue != null) { collectorMap.put(name, defaultValue); } else { diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIParser.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIParser.java new file mode 100644 index 00000000..49f2464b --- /dev/null +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIParser.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 io.serverlessworkflow.impl.executors.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import java.util.Objects; + +/** + * Parses OpenAPI content (JSON or YAML) into a {@link OpenAPI} using Jackson. + * + *

This class detects JSON if the first non-whitespace character is '{'; otherwise it treats the + * content as YAML. + */ +public final class OpenAPIParser { + + private static final ObjectMapper YAML_MAPPER = new YAMLMapper(); + private static final ObjectMapper JSON_MAPPER = new JsonMapper(); + + /** + * Parse the provided OpenAPI content (JSON or YAML) and return a {@link OpenAPI}. + * + * @param content the OpenAPI document content (must not be null or blank) + * @return parsed {@link OpenAPI} + * @throws IllegalArgumentException if content is null/blank or cannot be parsed + */ + public OpenAPI parse(String content) { + Objects.requireNonNull(content, "content must not be null"); + String trimmed = content.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("content must not be blank"); + } + + ObjectMapper mapper = selectMapper(trimmed); + try { + JsonNode root = mapper.readTree(content); + return new OpenAPI(root); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse content", e); + } + } + + private ObjectMapper selectMapper(String trimmedContent) { + char first = firstNonWhitespaceChar(trimmedContent); + if (first == '{') { + return JSON_MAPPER; + } + return YAML_MAPPER; + } + + private static char firstNonWhitespaceChar(String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!Character.isWhitespace(c)) { + return c; + } + } + return '\0'; + } +} diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java index f8da9994..26428101 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java @@ -15,89 +15,25 @@ */ package io.serverlessworkflow.impl.executors.openapi; -import io.swagger.parser.OpenAPIParser; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.parser.core.models.ParseOptions; -import io.swagger.v3.parser.core.models.SwaggerParseResult; -import java.util.Set; +import java.util.Objects; -class OpenAPIProcessor { +public class OpenAPIProcessor { private final String operationId; - OpenAPIProcessor(String operationId) { - this.operationId = operationId; + public OpenAPIProcessor(String operationId) { + this.operationId = Objects.requireNonNull(operationId); } public OperationDefinition parse(String content) { OpenAPIParser parser = new OpenAPIParser(); - ParseOptions opts = new ParseOptions(); - opts.setResolve(true); - opts.setResolveFully(true); - - SwaggerParseResult result = parser.readContents(content, null, opts); - - if (result.getMessages() != null && !result.getMessages().isEmpty()) { - throw new IllegalArgumentException( - "Failed to parse OpenAPI document: " + String.join(", ", result.getMessages())); - } - return getOperation(result.getOpenAPI(), !result.isOpenapi31()); + OpenAPI openAPI = parser.parse(content); + OpenAPI.PathItemInfo pathItemInfo = openAPI.findOperationById(this.operationId); + return new OperationDefinition( + openAPI, + pathItemInfo.operation().get(pathItemInfo.method()), + pathItemInfo.path(), + pathItemInfo.method().toUpperCase(), + openAPI.getSwaggerVersion()); } - - private OperationDefinition getOperation( - OpenAPI openAPI, boolean emulateSwaggerV2BodyParameters) { - if (openAPI == null || openAPI.getPaths() == null) { - throw new IllegalArgumentException("Invalid OpenAPI document"); - } - - Set paths = openAPI.getPaths().keySet(); - - for (String path : paths) { - PathItem pathItem = openAPI.getPaths().get(path); - OperationAndMethod operationAndMethod = findInPathItem(pathItem, operationId); - if (operationAndMethod != null) { - return new OperationDefinition( - openAPI, - operationAndMethod.operation, - path, - operationAndMethod.method, - emulateSwaggerV2BodyParameters); - } - } - throw new IllegalArgumentException( - "No operation with id '" + operationId + "' found in OpenAPI document"); - } - - private OperationAndMethod findInPathItem(PathItem pathItem, String operationId) { - if (pathItem == null) { - return null; - } - - if (matches(pathItem.getGet(), operationId)) - return new OperationAndMethod(pathItem.getGet(), "GET"); - if (matches(pathItem.getPost(), operationId)) - return new OperationAndMethod(pathItem.getPost(), "POST"); - if (matches(pathItem.getPut(), operationId)) - return new OperationAndMethod(pathItem.getPut(), "PUT"); - if (matches(pathItem.getDelete(), operationId)) - return new OperationAndMethod(pathItem.getDelete(), "DELETE"); - if (matches(pathItem.getPatch(), operationId)) - return new OperationAndMethod(pathItem.getPatch(), "PATCH"); - if (matches(pathItem.getHead(), operationId)) - return new OperationAndMethod(pathItem.getHead(), "HEAD"); - if (matches(pathItem.getOptions(), operationId)) - return new OperationAndMethod(pathItem.getOptions(), "OPTIONS"); - if (matches(pathItem.getTrace(), operationId)) - return new OperationAndMethod(pathItem.getTrace(), "TRACE"); - - return null; - } - - private boolean matches(Operation op, String operationId) { - return op != null && operationId.equals(op.getOperationId()); - } - - private record OperationAndMethod(Operation operation, String method) {} } diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java index 5111a38b..6ae20c02 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java @@ -15,35 +15,33 @@ */ package io.serverlessworkflow.impl.executors.openapi; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.servers.Server; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Set; class OperationDefinition { - private final Operation operation; + + private final JsonNode operation; private final String method; private final OpenAPI openAPI; private final String path; - private final boolean emulateSwaggerV2BodyParameters; + private final OpenAPI.SwaggerVersion swaggerVersion; OperationDefinition( OpenAPI openAPI, - Operation operation, + JsonNode operation, String path, String method, - boolean emulateSwaggerV2BodyParameters) { - this.openAPI = openAPI; - this.operation = operation; - this.path = path; - this.method = method; - this.emulateSwaggerV2BodyParameters = emulateSwaggerV2BodyParameters; + OpenAPI.SwaggerVersion swaggerVersion) { + this.openAPI = Objects.requireNonNull(openAPI, "openAPI must not be null"); + this.operation = Objects.requireNonNull(operation, "operation must not be null"); + this.path = Objects.requireNonNull(path, "path must not be null"); + this.method = Objects.requireNonNull(method, "method must not be null"); + this.swaggerVersion = Objects.requireNonNull(swaggerVersion, "swaggerVersion must not be null"); } String getMethod() { @@ -54,7 +52,7 @@ String getPath() { return path; } - Operation getOperation() { + JsonNode getOperation() { return operation; } @@ -62,77 +60,93 @@ List getServers() { if (openAPI.getServers() == null) { return List.of(); } - return openAPI.getServers().stream().map(Server::getUrl).toList(); + + return openAPI.getServers(); } List getParameters() { - return emulateSwaggerV2BodyParameters ? getSwaggerV2Parameters() : getOpenApiParameters(); + return swaggerVersion.equals(OpenAPI.SwaggerVersion.SWAGGER_V2) + ? getSwaggerV2Parameters() + : getOpenApiParameters(); } private List getOpenApiParameters() { - if (operation.getParameters() == null) { - return List.of(); + List paramDefinitions = new ArrayList<>(); + for (JsonNode parameterItem : operation.withArray("parameters")) { + paramDefinitions.add(new ParameterDefinition(parameterItem)); } - return operation.getParameters().stream().map(ParameterDefinition::new).toList(); + + if (operation.has("requestBody")) { + List fromBody = parametersFromRequestBody(operation.path("requestBody")); + if (!fromBody.isEmpty()) { + paramDefinitions.addAll(fromBody); + } + } + + return paramDefinitions; } - @SuppressWarnings({"rawtypes"}) private List getSwaggerV2Parameters() { - if (operation.getParameters() != null && !operation.getParameters().isEmpty()) { - return operation.getParameters().stream().map(ParameterDefinition::new).toList(); + if (operation.has("parameters") && !operation.withArray("parameters").isEmpty()) { + ArrayNode parameters = operation.withArray("parameters"); + List parameterDefinitions = new ArrayList<>(); + parameters.forEach(jsonNode -> parameterDefinitions.add(new ParameterDefinition(jsonNode))); + return parameterDefinitions; } - if (operation.getRequestBody() != null) { - Schema schema = null; - if (operation.getRequestBody().getContent() != null - && operation - .getRequestBody() - .getContent() - .containsKey(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)) { - MediaType mt = - operation - .getRequestBody() - .getContent() - .get(jakarta.ws.rs.core.MediaType.APPLICATION_JSON); - schema = mt.getSchema(); - } else if (operation.getRequestBody().get$ref() != null) { - schema = resolveSchema(operation.getRequestBody().get$ref()); - } - - if (schema == null) { - return List.of(); - } - Set required = - schema.getRequired() != null ? new HashSet<>(schema.getRequired()) : new HashSet<>(); - - Map properties = schema.getProperties(); - if (properties != null) { - List result = new ArrayList<>(); - for (Map.Entry prop : properties.entrySet()) { - String fieldName = prop.getKey(); - ParameterDefinition fieldParam = - new ParameterDefinition( - fieldName, "body", required.contains(fieldName), prop.getValue()); - result.add(fieldParam); - } - return result; - } + if (operation.has("requestBody")) { + return parametersFromRequestBody(operation.path("requestBody")); } + return List.of(); } - Schema resolveSchema(String ref) { - if (ref == null || !ref.startsWith("#/components/schemas/")) { - throw new IllegalArgumentException("Unsupported $ref format: " + ref); + private List parametersFromRequestBody(JsonNode requestBody) { + if (requestBody == null) { + return List.of(); } - String name = ref.substring("#/components/schemas/".length()); - if (openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) { - throw new IllegalStateException("No components/schemas found in OpenAPI"); + + JsonNode content = requestBody.path("content"); + if (!content.has("application/json")) { + return List.of(); + } + + JsonNode mediaType = content.path("application/json"); + JsonNode schema = mediaType.path("schema"); + + // resolve $ref if present + if (schema != null && schema.has("$ref")) { + String ref = schema.path("$ref").asText(); + schema = openAPI.resolveSchema(ref); + } + + if (schema == null || !schema.has("properties")) { + return List.of(); } - Schema schema = openAPI.getComponents().getSchemas().get(name); - if (schema == null) { - throw new IllegalArgumentException("Schema not found: " + name); + + JsonNode properties = schema.path("properties"); + Set requiredFields = requiredFields(schema); + + List result = new ArrayList<>(); + properties + .fieldNames() + .forEachRemaining( + fieldName -> { + JsonNode fieldSchema = properties.path(fieldName); + boolean isRequired = requiredFields.contains(fieldName); + result.add(new ParameterDefinition(fieldName, "body", isRequired, fieldSchema)); + }); + + return result; + } + + private Set requiredFields(JsonNode schema) { + Set required = new HashSet<>(); + if (schema != null && schema.has("required")) { + for (JsonNode req : schema.path("required")) { + required.add(req.asText()); + } } - return schema; + return required; } } diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ParameterDefinition.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ParameterDefinition.java index 54290096..7ae8c37f 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ParameterDefinition.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ParameterDefinition.java @@ -15,25 +15,24 @@ */ package io.serverlessworkflow.impl.executors.openapi; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.parameters.Parameter; +import com.fasterxml.jackson.databind.JsonNode; class ParameterDefinition { private final String name; private final String in; private final boolean required; - private final Schema schema; + private final JsonNode schema; - ParameterDefinition(Parameter parameter) { + ParameterDefinition(JsonNode parameter) { this( - parameter.getName(), - parameter.getIn(), - parameter.getRequired() != null && parameter.getRequired(), - parameter.getSchema()); + parameter.get("name").asText(), + parameter.get("in").asText(), + parameter.has("required") && parameter.get("required").asBoolean(), + parameter.get("schema")); } - ParameterDefinition(String name, String in, boolean required, Schema schema) { + ParameterDefinition(String name, String in, boolean required, JsonNode schema) { this.name = name; this.in = in; this.required = required; @@ -52,7 +51,7 @@ public boolean getRequired() { return required; } - public Schema getSchema() { + public JsonNode getSchema() { return schema; } } diff --git a/pom.xml b/pom.xml index 11800721..ac99765d 100644 --- a/pom.xml +++ b/pom.xml @@ -94,8 +94,6 @@ 2.0.17 9.1.0.Final 6.0.0 - - 2.1.36 true java