Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[KOGITO-9584] Ensure nested schemas are resolved for swagger doc #3122

Merged
merged 6 commits into from
Jul 20, 2023
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 @@ -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;
Expand Down Expand Up @@ -195,9 +196,10 @@ public static Optional<byte[]> loadResourceFile(Workflow workflow, Optional<Pars
public static Optional<byte[]> loadResourceFile(String uriStr, Optional<Workflow> workflow, Optional<ParserContext> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,7 +44,7 @@ public class JsonSchemaValidator implements WorkflowModelValidator {

protected final String schemaRef;
protected final boolean failOnValidationErrors;
private final AtomicReference<JsonNode> schemaObject = new AtomicReference<>();
private final AtomicReference<JsonSchema> schemaObject = new AtomicReference<>();

public JsonSchemaValidator(String schema, boolean failOnValidationErrors) {
this.schemaRef = schema;
Expand All @@ -54,7 +55,7 @@ public JsonSchemaValidator(String schema, boolean failOnValidationErrors) {
public void validate(Map<String, Object> model) {
try {
Set<ValidationMessage> 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()));
Expand All @@ -70,9 +71,13 @@ public void validate(Map<String, Object> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassLoader> cl, Optional<URIContentLoader> fallback, Optional<Workflow> 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));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
fjtirado marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Error loading ref {}", ref, e);
}
}
super.setRef(ref);
}

@JsonDeserialize(as = JsonSchemaImpl.class)
@Override
public Schema getItems() {
Expand Down Expand Up @@ -76,5 +118,4 @@ public Schema getAdditionalPropertiesSchema() {
public Schema getNot() {
return super.getNot();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,20 @@ private OpenApiModelSchemaGenerator() {
public static void addOpenAPIModelSchema(KogitoWorkflowProcess workflow, Map<String, Schema> 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();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Schema> map = new HashMap<>();
private int counter;
private String baseURI;

private ThreadInfo(String id) {
this.id = id;
}
}

private static ThreadLocal<ThreadInfo> threadInfo = new ThreadLocal<>();

public static void init(String id) {
threadInfo.set(new ThreadInfo(id));
}

public static Map<String, Schema> 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();
}
}