diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/utils/ServerlessWorkflowUtils.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/utils/ServerlessWorkflowUtils.java index 4795b1e00ef..03ae00308f4 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/utils/ServerlessWorkflowUtils.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/utils/ServerlessWorkflowUtils.java @@ -36,6 +36,7 @@ import org.kie.kogito.serverless.workflow.extensions.OutputSchema; import org.kie.kogito.serverless.workflow.extensions.URIDefinitions; import org.kie.kogito.serverless.workflow.io.URIContentLoaderFactory; +import org.kie.kogito.serverless.workflow.io.URIContentLoaderFactory.Builder; import org.kie.kogito.serverless.workflow.models.JsonNodeModel; import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.suppliers.ConfigWorkItemSupplier; @@ -195,9 +196,10 @@ public static Optional loadResourceFile(Workflow workflow, Optional loadResourceFile(String uriStr, Optional workflow, Optional parserContext, String authRef) { final URI uri = URI.create(uriStr); try { - final byte[] bytes = - URIContentLoaderFactory.readAllBytes(URIContentLoaderFactory.loader(uri, parserContext.map(p -> p.getContext().getClassLoader()), Optional.empty(), workflow, authRef)); - return Optional.of(bytes); + Builder builder = URIContentLoaderFactory.builder(uri).withAuthRef(authRef); + workflow.ifPresent(builder::withWorkflow); + parserContext.map(p -> p.getContext().getClassLoader()).ifPresent(builder::withClassloader); + return Optional.of(URIContentLoaderFactory.readAllBytes(builder.build())); } catch (UncheckedIOException io) { // if file cannot be found in build context, warn it and return the unmodified uri (it might be possible that later the resource is available at runtime) logger.warn("Resource {} cannot be found at build time, ignoring", uri, io); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/actions/JsonSchemaValidator.java b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/actions/JsonSchemaValidator.java index 080580c2033..5d29e78ad68 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/actions/JsonSchemaValidator.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/actions/JsonSchemaValidator.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; +import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.ValidationMessage; @@ -43,7 +44,7 @@ public class JsonSchemaValidator implements WorkflowModelValidator { protected final String schemaRef; protected final boolean failOnValidationErrors; - private final AtomicReference schemaObject = new AtomicReference<>(); + private final AtomicReference schemaObject = new AtomicReference<>(); public JsonSchemaValidator(String schema, boolean failOnValidationErrors) { this.schemaRef = schema; @@ -54,7 +55,7 @@ public JsonSchemaValidator(String schema, boolean failOnValidationErrors) { public void validate(Map model) { try { Set report = - JsonSchemaFactory.getInstance(VersionFlag.V4).getSchema(schemaData()).validate((JsonNode) model.getOrDefault(SWFConstants.DEFAULT_WORKFLOW_VAR, NullNode.instance)); + schema().validate((JsonNode) model.getOrDefault(SWFConstants.DEFAULT_WORKFLOW_VAR, NullNode.instance)); if (!report.isEmpty()) { StringBuilder sb = new StringBuilder("There are JsonSchema validation errors:"); report.forEach(m -> sb.append(System.lineSeparator()).append(m.getMessage())); @@ -70,9 +71,13 @@ public void validate(Map model) { } public JsonNode schemaData() throws IOException { - JsonNode result = schemaObject.get(); + return schema().getSchemaNode(); + } + + private JsonSchema schema() throws IOException { + JsonSchema result = schemaObject.get(); if (result == null) { - result = ObjectMapperFactory.get().readTree(readAllBytes(runtimeLoader(schemaRef))); + result = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(ObjectMapperFactory.get().readTree(readAllBytes(runtimeLoader(schemaRef)))); schemaObject.set(result); } return result; diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderFactory.java b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderFactory.java index ce374d33d2f..1ea401d4874 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderFactory.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderFactory.java @@ -35,22 +35,87 @@ public static byte[] readAllBytes(URIContentLoader loader) { public static URIContentLoader runtimeLoader(String uriStr) { URI uri = URI.create(uriStr); - return loader(uri, Optional.empty(), Optional.of(new ClassPathContentLoader(uri, Optional.empty())), Optional.empty(), null); + Builder builder = new Builder(uri); + builder.withFallback(new ClassPathContentLoader(uri, Optional.empty())); + return builder.build(); + } public static URIContentLoader buildLoader(URI uri, ClassLoader cl, Workflow workflow, String authRef) { - return loader(uri, Optional.of(cl), Optional.empty(), Optional.of(workflow), authRef); + return new Builder(uri).withClassloader(cl).withWorkflow(workflow).withAuthRef(authRef).build(); } + /** + * @deprecated Use builder + */ + @Deprecated public static URIContentLoader loader(URI uri, Optional cl, Optional fallback, Optional workflow, String authRef) { - switch (URIContentLoaderType.from(uri)) { - case FILE: - return new FileContentLoader(uri, fallback); - case HTTP: - return new HttpContentLoader(uri, fallback, workflow, authRef); - default: - case CLASSPATH: - return new ClassPathContentLoader(uri, cl); + Builder builder = new Builder(uri); + cl.ifPresent(builder::withClassloader); + fallback.ifPresent(builder::withFallback); + workflow.ifPresent(builder::withWorkflow); + builder.withAuthRef(authRef); + return builder.build(); + } + + public static Builder builder(URI uri) { + return new Builder(uri); + } + + public static class Builder { + private URI uri; + private ClassLoader cl; + private URIContentLoader fallback; + private Workflow workflow; + private String authRef; + private String baseURI; + + private Builder(URI uri) { + this.uri = uri; + } + + public Builder withClassloader(ClassLoader cl) { + this.cl = cl; + return this; + } + + public Builder withFallback(URIContentLoader fallback) { + this.fallback = fallback; + return this; + } + + public Builder withWorkflow(Workflow workflow) { + this.workflow = workflow; + return this; + } + + public Builder withAuthRef(String authRef) { + this.authRef = authRef; + return this; + } + + public Builder withBaseURI(String baseURI) { + this.baseURI = baseURI; + return this; + } + + public URIContentLoader build() { + if (uri.getScheme() == null) { + if (baseURI != null) { + uri = URI.create(baseURI + "/" + uri.toString()); + } else { + return new ClassPathContentLoader(uri, Optional.ofNullable(cl)); + } + } + switch (URIContentLoaderType.from(uri)) { + case FILE: + return new FileContentLoader(uri, Optional.ofNullable(fallback)); + case HTTP: + return new HttpContentLoader(uri, Optional.ofNullable(fallback), Optional.ofNullable(workflow), authRef); + default: + case CLASSPATH: + return new ClassPathContentLoader(uri, Optional.ofNullable(cl)); + } } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderType.java b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderType.java index 3920305bf46..649294ad9c7 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderType.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-runtime/src/main/java/org/kie/kogito/serverless/workflow/io/URIContentLoaderType.java @@ -23,9 +23,6 @@ public enum URIContentLoaderType { HTTP; public static URIContentLoaderType from(URI uri) { - if (uri.getScheme() == null) { - return CLASSPATH; - } switch (uri.getScheme().toLowerCase()) { case "file": return FILE; diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/JsonSchemaImpl.java b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/JsonSchemaImpl.java index 238b006b614..60fd6221afc 100644 --- a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/JsonSchemaImpl.java +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/JsonSchemaImpl.java @@ -16,15 +16,25 @@ package org.kie.kogito.serverless.workflow.parser.schema; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import java.util.Map; import org.eclipse.microprofile.openapi.models.media.Schema; +import org.kie.kogito.jackson.utils.ObjectMapperFactory; +import org.kie.kogito.serverless.workflow.io.URIContentLoaderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.smallrye.openapi.api.constants.OpenApiConstants; import io.smallrye.openapi.api.models.media.SchemaImpl; /** @@ -35,6 +45,38 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class JsonSchemaImpl extends SchemaImpl { + private static final Logger logger = LoggerFactory.getLogger(JsonSchemaImpl.class); + + @JsonSetter("$id") + public void setId(String id) { + RefSchemas.baseURI(id); + } + + @JsonSetter("$ref") + @Override + public void setRef(String ref) { + if (ref != null && !ref.startsWith("#")) { + try (InputStream is = URIContentLoaderFactory.builder(new URI(ref)).withBaseURI(RefSchemas.getBaseURI()).build().getInputStream()) { + JsonSchemaImpl schema = ObjectMapperFactory.get().readValue(is.readAllBytes(), JsonSchemaImpl.class); + String key; + if (schema.getTitle() == null) { + key = RefSchemas.getKey(); + schema.title(key); + } else { + key = schema.getTitle(); + } + if (key != null) { + RefSchemas.get().put(key, schema); + } + ref = OpenApiConstants.REF_PREFIX_SCHEMA + key; + } catch (URISyntaxException | IOException e) { + // if not a valid uri, let super handle it + logger.info("Error loading ref {}", ref, e); + } + } + super.setRef(ref); + } + @JsonDeserialize(as = JsonSchemaImpl.class) @Override public Schema getItems() { @@ -76,5 +118,4 @@ public Schema getAdditionalPropertiesSchema() { public Schema getNot() { return super.getNot(); } - } diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/OpenApiModelSchemaGenerator.java b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/OpenApiModelSchemaGenerator.java index 4759f8b5436..68756ba8e58 100644 --- a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/OpenApiModelSchemaGenerator.java +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/OpenApiModelSchemaGenerator.java @@ -59,14 +59,20 @@ private OpenApiModelSchemaGenerator() { public static void addOpenAPIModelSchema(KogitoWorkflowProcess workflow, Map schemas) { if (workflow instanceof WorkflowProcess) { WorkflowProcess workflowProcess = (WorkflowProcess) workflow; - getSchema(workflowProcess.getInputValidator()).ifPresent(v -> { - String key = getSchemaName(workflow.getId(), INPUT_SUFFIX); - schemas.put(key, schemaTitle(key, v)); - }); - getSchema(workflowProcess.getOutputValidator()).ifPresent(v -> { - String key = getSchemaName(workflow.getId(), OUTPUT_SUFFIX); - schemas.put(key, createOutputSchema(schemaTitle(key, v))); - }); + RefSchemas.init(workflow.getId()); + try { + getSchema(workflowProcess.getInputValidator()).ifPresent(v -> { + String key = getSchemaName(workflow.getId(), INPUT_SUFFIX); + schemas.put(key, schemaTitle(key, v)); + }); + getSchema(workflowProcess.getOutputValidator()).ifPresent(v -> { + String key = getSchemaName(workflow.getId(), OUTPUT_SUFFIX); + schemas.put(key, createOutputSchema(schemaTitle(key, v))); + }); + schemas.putAll(RefSchemas.get()); + } finally { + RefSchemas.reset(); + } } } diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/RefSchemas.java b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/RefSchemas.java new file mode 100644 index 00000000000..f68072a5978 --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-deployment/src/main/java/org/kie/kogito/serverless/workflow/parser/schema/RefSchemas.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * 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 org.kie.kogito.serverless.workflow.parser.schema; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.microprofile.openapi.models.media.Schema; + +class RefSchemas { + + private RefSchemas() { + } + + private static class ThreadInfo { + private final String id; + private final Map map = new HashMap<>(); + private int counter; + private String baseURI; + + private ThreadInfo(String id) { + this.id = id; + } + } + + private static ThreadLocal threadInfo = new ThreadLocal<>(); + + public static void init(String id) { + threadInfo.set(new ThreadInfo(id)); + } + + public static Map get() { + return threadInfo.get().map; + } + + public static void baseURI(String baseURI) { + if (baseURI != null) { + int lastIndexOf = baseURI.lastIndexOf('/'); + threadInfo.get().baseURI = lastIndexOf != -1 ? baseURI.substring(0, lastIndexOf) : baseURI; + } + } + + public static String getBaseURI() { + return threadInfo.get().baseURI; + } + + public static String getKey() { + ThreadInfo t = threadInfo.get(); + return t.id + "_nested_" + ++t.counter; + } + + public static void reset() { + threadInfo.remove(); + } +}