diff --git a/app/pom.xml b/app/pom.xml index 6213724231..1c7948f75d 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -133,6 +133,7 @@ io.quarkus quarkus-smallrye-context-propagation + io.quarkus quarkus-rest-client diff --git a/app/src/main/java/io/apicurio/registry/ccompat/rest/v7/impl/AbstractResource.java b/app/src/main/java/io/apicurio/registry/ccompat/rest/v7/impl/AbstractResource.java index 3edb29a733..7ce7f12512 100644 --- a/app/src/main/java/io/apicurio/registry/ccompat/rest/v7/impl/AbstractResource.java +++ b/app/src/main/java/io/apicurio/registry/ccompat/rest/v7/impl/AbstractResource.java @@ -16,7 +16,6 @@ package io.apicurio.registry.ccompat.rest.v7.impl; - import io.apicurio.registry.ccompat.dto.SchemaReference; import io.apicurio.registry.ccompat.rest.error.ConflictException; import io.apicurio.registry.ccompat.rest.error.UnprocessableEntityException; @@ -25,14 +24,14 @@ import io.apicurio.registry.rules.RuleApplicationType; import io.apicurio.registry.rules.RuleViolationException; import io.apicurio.registry.rules.RulesService; +import io.apicurio.registry.storage.ArtifactNotFoundException; import io.apicurio.registry.storage.RegistryStorage; +import io.apicurio.registry.storage.RuleNotFoundException; +import io.apicurio.registry.storage.VersionNotFoundException; import io.apicurio.registry.storage.dto.ArtifactMetaDataDto; import io.apicurio.registry.storage.dto.ArtifactReferenceDto; import io.apicurio.registry.storage.dto.ArtifactVersionMetaDataDto; import io.apicurio.registry.storage.dto.StoredArtifactDto; -import io.apicurio.registry.storage.ArtifactNotFoundException; -import io.apicurio.registry.storage.RuleNotFoundException; -import io.apicurio.registry.storage.VersionNotFoundException; import io.apicurio.registry.storage.impl.sql.RegistryContentUtils; import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.ArtifactType; @@ -40,13 +39,12 @@ import io.apicurio.registry.types.RuleType; import io.apicurio.registry.types.provider.ArtifactTypeUtilProvider; import io.apicurio.registry.types.provider.ArtifactTypeUtilProviderFactory; +import jakarta.inject.Inject; import org.apache.avro.AvroTypeException; import org.apache.avro.SchemaParseException; import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; -import jakarta.inject.Inject; - import java.util.Collections; import java.util.List; import java.util.Map; diff --git a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java index b3f8f12c28..2c91b75c7c 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java @@ -73,6 +73,7 @@ import io.apicurio.registry.storage.dto.VersionSearchResultsDto; import io.apicurio.registry.storage.impl.sql.RegistryContentUtils; import io.apicurio.registry.types.ArtifactState; +import io.apicurio.registry.types.ArtifactType; import io.apicurio.registry.types.Current; import io.apicurio.registry.types.ReferenceType; import io.apicurio.registry.types.RuleType; @@ -185,8 +186,17 @@ public Response getLatestArtifact(String groupId, String artifactId, Boolean der ArtifactTypeUtilProvider artifactTypeProvider = factory.getArtifactTypeProvider(metaData.getType()); - if (dereference && !artifact.getReferences().isEmpty() && artifactTypeProvider.getContentDereferencer() != null) { - contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + if (dereference && !artifact.getReferences().isEmpty()) { + if (ArtifactType.JSON.equals(metaData.getType())) { + RegistryContentUtils.RewrittenContentHolder rewrittenContent = RegistryContentUtils.recursivelyResolveReferencesWithContext(contentToReturn, + metaData.getType(), + artifact.getReferences(), storage::getContentByReference); + + contentToReturn = artifactTypeProvider.getContentDereferencer() + .dereference(rewrittenContent.getRewrittenContent(), rewrittenContent.getResolvedReferences()); + } else { + contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + } } Response.ResponseBuilder builder = Response.ok(contentToReturn, contentType); @@ -574,7 +584,16 @@ public Response getArtifactVersion(String groupId, String artifactId, String ver ArtifactTypeUtilProvider artifactTypeProvider = factory.getArtifactTypeProvider(metaData.getType()); if (dereference && !artifact.getReferences().isEmpty()) { - contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + if (ArtifactType.JSON.equals(metaData.getType())) { + RegistryContentUtils.RewrittenContentHolder rewrittenContent = RegistryContentUtils.recursivelyResolveReferencesWithContext(contentToReturn, + metaData.getType(), + artifact.getReferences(), storage::getContentByReference); + + contentToReturn = artifactTypeProvider.getContentDereferencer() + .dereference(rewrittenContent.getRewrittenContent(), rewrittenContent.getResolvedReferences()); + } else { + contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + } } Response.ResponseBuilder builder = Response.ok(contentToReturn, contentType); diff --git a/app/src/main/java/io/apicurio/registry/rest/v2/IdsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v2/IdsResourceImpl.java index 047378d070..c2d33ad51b 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v2/IdsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v2/IdsResourceImpl.java @@ -34,6 +34,7 @@ import io.apicurio.registry.storage.impl.sql.RegistryContentUtils; import io.apicurio.registry.types.ArtifactMediaTypes; import io.apicurio.registry.types.ArtifactState; +import io.apicurio.registry.types.ArtifactType; import io.apicurio.registry.types.Current; import io.apicurio.registry.types.ReferenceType; import io.apicurio.registry.types.provider.ArtifactTypeUtilProvider; @@ -53,7 +54,7 @@ * @author eric.wittmann@gmail.com */ @ApplicationScoped -@Interceptors({ResponseErrorLivenessCheck.class, ResponseTimeoutReadinessCheck.class}) +@Interceptors({ ResponseErrorLivenessCheck.class, ResponseTimeoutReadinessCheck.class }) @Logged public class IdsResourceImpl implements IdsResource { @@ -106,7 +107,16 @@ public Response getContentByGlobalId(long globalId, Boolean dereference) { ArtifactTypeUtilProvider artifactTypeProvider = factory.getArtifactTypeProvider(metaData.getType()); if (dereference && !artifact.getReferences().isEmpty()) { - contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + if (ArtifactType.JSON.equals(metaData.getType())) { + RegistryContentUtils.RewrittenContentHolder rewrittenContent = RegistryContentUtils.recursivelyResolveReferencesWithContext(contentToReturn, + metaData.getType(), + artifact.getReferences(), storage::getContentByReference); + + contentToReturn = artifactTypeProvider.getContentDereferencer() + .dereference(rewrittenContent.getRewrittenContent(), rewrittenContent.getResolvedReferences()); + } else { + contentToReturn = artifactTypeProvider.getContentDereferencer().dereference(artifact.getContent(), RegistryContentUtils.recursivelyResolveReferences(artifact.getReferences(), storage::getContentByReference)); + } } Response.ResponseBuilder builder = Response.ok(contentToReturn, contentType); @@ -154,7 +164,8 @@ public List referencesByGlobalId(long globalId, ReferenceType return artifact.getReferences().stream() .map(V2ApiUtil::referenceDtoToReference) .collect(Collectors.toList()); - } else { + } + else { ArtifactMetaDataDto amd = storage.getArtifactMetaData(globalId); return storage.getInboundArtifactReferences(amd.getGroupId(), amd.getId(), amd.getVersion()).stream() .map(V2ApiUtil::referenceDtoToReference) diff --git a/app/src/main/java/io/apicurio/registry/storage/dto/ContentAndReferencesDto.java b/app/src/main/java/io/apicurio/registry/storage/dto/ContentAndReferencesDto.java index 785abcdb0e..60573cb044 100644 --- a/app/src/main/java/io/apicurio/registry/storage/dto/ContentAndReferencesDto.java +++ b/app/src/main/java/io/apicurio/registry/storage/dto/ContentAndReferencesDto.java @@ -35,4 +35,6 @@ public class ContentAndReferencesDto { private ContentHandle content; private List references; + + private String artifactType; } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java index f5bb0fede5..95c0692a4b 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java @@ -3510,7 +3510,9 @@ public GroupSearchResultsDto searchGroups(Set filters, OrderBy ord public ContentAndReferencesDto getContentByReference(ArtifactReferenceDto reference) { try { var meta = getArtifactVersionMetaDataInternal(reference.getGroupId(), reference.getArtifactId(), reference.getVersion()); - return getArtifactByContentId(meta.getContentId()); + ContentAndReferencesDto artifactByContentId = getArtifactByContentId(meta.getContentId()); + artifactByContentId.setArtifactType(meta.getType()); + return artifactByContentId; } catch (VersionNotFoundException e) { return null; } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/RegistryContentUtils.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/RegistryContentUtils.java index 16e2d83cf5..4d79f6e8b4 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/RegistryContentUtils.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/RegistryContentUtils.java @@ -7,6 +7,7 @@ import io.apicurio.registry.storage.dto.ArtifactReferenceDto; import io.apicurio.registry.storage.dto.ContentAndReferencesDto; import io.apicurio.registry.types.RegistryException; +import io.apicurio.registry.types.provider.ArtifactTypeUtilProvider; import io.apicurio.registry.types.provider.ArtifactTypeUtilProviderFactory; import io.apicurio.registry.types.provider.DefaultArtifactTypeUtilProviderImpl; import io.apicurio.registry.utils.StringUtil; @@ -16,10 +17,14 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; - public class RegistryContentUtils { private static final Logger log = LoggerFactory.getLogger(RegistryContentUtils.class); @@ -30,31 +35,92 @@ public class RegistryContentUtils { public static final ArtifactTypeUtilProviderFactory ARTIFACT_TYPE_UTIL = new DefaultArtifactTypeUtilProviderImpl(); - private RegistryContentUtils() { } - /** * Recursively resolve the references. */ - public static Map recursivelyResolveReferences(List references, Function loader) { + public static Map recursivelyResolveReferences(List references, + Function loader) { if (references == null || references.isEmpty()) { return Map.of(); - } else { + } + else { Map result = new LinkedHashMap<>(); resolveReferences(result, references, loader); return result; } } + /** + * Recursively resolve the references. Instead of using the reference name as the key, it uses the full coordinates of the artifact version. + * Re-writes each schema node content to use the full coordinates of the artifact version instead of just using the original reference name. + * @return the main content rewritten to use the full coordinates of the artifact version and the full tree of dependencies, also rewritten to use coordinates instead of the reference name. + */ + public static RewrittenContentHolder recursivelyResolveReferencesWithContext(ContentHandle mainContent, String mainContentType, List references, + Function loader) { + if (references == null || references.isEmpty()) { + return new RewrittenContentHolder(mainContent, Collections.emptyMap()); + } + else { + Map resolvedReferences = new LinkedHashMap<>(); + //First we resolve all the references tree, re-writing the nested contents tu use the artifact version coordinates instead of the version name. + return resolveReferencesWithContext(mainContent, mainContentType, resolvedReferences, references, loader); + } + } - private static void resolveReferences(Map partialRecursivelyResolvedReferences, List references, Function loader) { + /** + * Recursively resolve the references. Instead of using the reference name as the key, it uses the full coordinates of the artifact version. + * Re-writes each schema node content to use the full coordinates of the artifact version instead of just using the original reference name. + * This allows to dereference json schema artifacts where there might be duplicate file names in a single hierarchy. + */ + private static RewrittenContentHolder resolveReferencesWithContext(ContentHandle mainContent, String schemaType, + Map partialRecursivelyResolvedReferences, + List references, + Function loader) { + Map referencesRewrites = new HashMap<>(); if (references != null && !references.isEmpty()) { for (ArtifactReferenceDto reference : references) { if (reference.getArtifactId() == null || reference.getName() == null || reference.getVersion() == null) { throw new IllegalStateException("Invalid reference: " + reference); - } else { + } + else { + if (!partialRecursivelyResolvedReferences.containsKey(reference.getName())) { + try { + var nested = loader.apply(reference); + if (nested != null) { + ArtifactTypeUtilProvider typeUtilProvider = ARTIFACT_TYPE_UTIL.getArtifactTypeProvider(nested.getArtifactType()); + RewrittenContentHolder rewrittenContentHolder = resolveReferencesWithContext(nested.getContent(), nested.getArtifactType(), + partialRecursivelyResolvedReferences, nested.getReferences(), loader); + String referenceCoordinates = concatArtifactVersionCoordinatesWithRefName(reference.getGroupId(), reference.getArtifactId(), + reference.getVersion(), reference.getName()); + referencesRewrites.put(reference.getName(), referenceCoordinates); + ContentHandle rewrittenContent = typeUtilProvider.getContentDereferencer() + .rewriteReferences(rewrittenContentHolder.getRewrittenContent(), referencesRewrites); + partialRecursivelyResolvedReferences.put(referenceCoordinates, rewrittenContent); + } + } + catch (Exception ex) { + log.error("Could not resolve reference " + reference + ".", ex); + } + } + } + } + } + ArtifactTypeUtilProvider typeUtilProvider = ARTIFACT_TYPE_UTIL.getArtifactTypeProvider(schemaType); + ContentHandle rewrittenContent = typeUtilProvider.getContentDereferencer().rewriteReferences(mainContent, referencesRewrites); + return new RewrittenContentHolder(rewrittenContent, partialRecursivelyResolvedReferences); + } + + private static void resolveReferences(Map partialRecursivelyResolvedReferences, List references, + Function loader) { + if (references != null && !references.isEmpty()) { + for (ArtifactReferenceDto reference : references) { + if (reference.getArtifactId() == null || reference.getName() == null || reference.getVersion() == null) { + throw new IllegalStateException("Invalid reference: " + reference); + } + else { if (!partialRecursivelyResolvedReferences.containsKey(reference.getName())) { try { var nested = loader.apply(reference); @@ -62,7 +128,8 @@ private static void resolveReferences(Map partialRecursiv resolveReferences(partialRecursivelyResolvedReferences, nested.getReferences(), loader); partialRecursivelyResolvedReferences.put(reference.getName(), nested.getContent()); } - } catch (Exception ex) { + } + catch (Exception ex) { log.error("Could not resolve reference " + reference + ".", ex); } } @@ -71,7 +138,6 @@ private static void resolveReferences(Map partialRecursiv } } - /** * Canonicalize the given content. *

@@ -82,7 +148,8 @@ private static ContentHandle canonicalizeContent(String artifactType, ContentHan return ARTIFACT_TYPE_UTIL.getArtifactTypeProvider(artifactType) .getContentCanonicalizer() .canonicalize(content, recursivelyResolvedReferences); - } catch (Exception ex) { + } + catch (Exception ex) { // TODO: We should consider explicitly failing when a content could not be canonicalized. // throw new RegistryException("Failed to canonicalize content.", ex); log.debug("Failed to canonicalize content: {}", content.content()); @@ -90,7 +157,6 @@ private static ContentHandle canonicalizeContent(String artifactType, ContentHan } } - /** * Canonicalize the given content. * @@ -99,12 +165,12 @@ private static ContentHandle canonicalizeContent(String artifactType, ContentHan public static ContentHandle canonicalizeContent(String artifactType, ContentAndReferencesDto data, Function loader) { try { return canonicalizeContent(artifactType, data.getContent(), recursivelyResolveReferences(data.getReferences(), loader)); - } catch (Exception ex) { + } + catch (Exception ex) { throw new RegistryException("Failed to canonicalize content.", ex); } } - /** * @param loader can be null *if and only if* references are empty. */ @@ -114,16 +180,17 @@ public static String canonicalContentHash(String artifactType, ContentAndReferen String serializedReferences = serializeReferences(data.getReferences()); ContentHandle canonicalContent = canonicalizeContent(artifactType, data, loader); return DigestUtils.sha256Hex(concatContentAndReferences(canonicalContent.bytes(), serializedReferences)); - } else { + } + else { ContentHandle canonicalContent = canonicalizeContent(artifactType, data.getContent(), Map.of()); return DigestUtils.sha256Hex(canonicalContent.bytes()); } - } catch (IOException ex) { + } + catch (IOException ex) { throw new RegistryException("Failed to compute canonical content hash.", ex); } } - /** * data.references may be null */ @@ -132,15 +199,16 @@ public static String contentHash(ContentAndReferencesDto data) { if (notEmpty(data.getReferences())) { String serializedReferences = serializeReferences(data.getReferences()); return DigestUtils.sha256Hex(concatContentAndReferences(data.getContent().bytes(), serializedReferences)); - } else { + } + else { return data.getContent().getSha256Hash(); } - } catch (IOException ex) { + } + catch (IOException ex) { throw new RegistryException("Failed to compute content hash.", ex); } } - private static byte[] concatContentAndReferences(byte[] contentBytes, String serializedReferences) throws IOException { if (serializedReferences != null && !serializedReferences.isEmpty()) { var serializedReferencesBytes = ContentHandle.create(serializedReferences).bytes(); @@ -148,12 +216,12 @@ private static byte[] concatContentAndReferences(byte[] contentBytes, String ser bytes.put(contentBytes); bytes.put(serializedReferencesBytes); return bytes.array(); - } else { + } + else { throw new IllegalArgumentException("serializedReferences is null or empty"); } } - /** * Serializes the given collection of labels to a string for artifactStore in the DB. * @@ -168,12 +236,12 @@ public static String serializeLabels(List labels) { return null; } return MAPPER.writeValueAsString(labels); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - /** * Deserialize the labels from their string form to a List<String> form. * @@ -186,12 +254,12 @@ public static List deserializeLabels(String labelsStr) { return null; } return MAPPER.readValue(labelsStr, List.class); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - /** * Serializes the given collection of properties to a string for artifactStore in the DB. * @@ -206,12 +274,12 @@ public static String serializeProperties(Map properties) { return null; } return MAPPER.writeValueAsString(properties); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - /** * Deserialize the properties from their string form to a Map form. * @@ -224,12 +292,12 @@ public static Map deserializeProperties(String propertiesStr) { return null; } return MAPPER.readValue(propertiesStr, Map.class); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - /** * Serializes the given collection of references to a string for artifactStore in the DB. * @@ -241,12 +309,12 @@ public static String serializeReferences(List references) return null; } return MAPPER.writeValueAsString(references); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - /** * Deserialize the references from their string form to a List form. * @@ -259,12 +327,12 @@ public static List deserializeReferences(String references } return MAPPER.readValue(references, new TypeReference>() { }); - } catch (JsonProcessingException e) { + } + catch (JsonProcessingException e) { throw new RuntimeException(e); } } - public static String normalizeGroupId(String groupId) { if (groupId == null || "default".equals(groupId)) { return NULL_GROUP_ID; @@ -272,7 +340,6 @@ public static String normalizeGroupId(String groupId) { return groupId; } - public static String denormalizeGroupId(String groupId) { if (NULL_GROUP_ID.equals(groupId)) { return null; @@ -280,8 +347,29 @@ public static String denormalizeGroupId(String groupId) { return groupId; } - public static boolean notEmpty(Collection collection) { return collection != null && !collection.isEmpty(); } + + public static String concatArtifactVersionCoordinatesWithRefName(String groupId, String artifactId, String version, String referenceName) { + return groupId + ":" + artifactId + ":" + version + ":" + referenceName; + } + + public static class RewrittenContentHolder { + final ContentHandle rewrittenContent; + final Map resolvedReferences; + + public RewrittenContentHolder(ContentHandle rewrittenContent, Map resolvedReferences) { + this.rewrittenContent = rewrittenContent; + this.resolvedReferences = resolvedReferences; + } + + public ContentHandle getRewrittenContent() { + return rewrittenContent; + } + + public Map getResolvedReferences() { + return resolvedReferences; + } + } } diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v2/GroupsResourceTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v2/GroupsResourceTest.java index 899206be83..ed988da7d9 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/rest/v2/GroupsResourceTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v2/GroupsResourceTest.java @@ -47,10 +47,16 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; import static io.restassured.RestAssured.given; -import static java.net.HttpURLConnection.*; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.anything; import static org.hamcrest.Matchers.*; @@ -157,7 +163,7 @@ public void testDefaultGroup() throws Exception { @Test public void testUpdateArtifactOwner() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactOwner", "testUpdateArtifactOwner/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactOwner", "testUpdateArtifactOwner/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); ArtifactOwner artifactOwner = new ArtifactOwner("newOwner"); @@ -175,7 +181,7 @@ public void testUpdateArtifactOwner() throws Exception { @Test public void testUpdateEmptyArtifactOwner() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateEmptyArtifactOwner", "testUpdateEmptyArtifactOwner/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateEmptyArtifactOwner", "testUpdateEmptyArtifactOwner/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); ArtifactOwner artifactOwner = new ArtifactOwner(""); @@ -615,7 +621,7 @@ public void testUpdateArtifact() throws Exception { @Test public void testUpdateArtifactState() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactState", "testUpdateArtifactState/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactState", "testUpdateArtifactState/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); UpdateState updateState = new UpdateState(); updateState.setState(ArtifactState.DEPRECATED); @@ -652,17 +658,17 @@ public void testUpdateArtifactState() throws Exception { .then() .statusCode(200) .header("X-Registry-Deprecated", "true"); - } + } @Test public void testUpdateArtifactVersionState() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactVersionState", "testUpdateArtifactVersionState/EmptyAPI",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactVersionState", "testUpdateArtifactVersionState/EmptyAPI", ArtifactType.OPENAPI, oaiArtifactContent); UpdateState updateState = new UpdateState(); updateState.setState(ArtifactState.DEPRECATED); - // Update the artifact state to DEPRECATED. + // Update the artifact state to DEPRECATED. given() .when() .contentType(CT_JSON) @@ -697,7 +703,7 @@ public void testUpdateArtifactVersionState() throws Exception { .then() .statusCode(200) .header("X-Registry-Deprecated", "true"); - } + } @Test @DisabledIfEnvironmentVariable(named = CURRENT_ENV, matches = CURRENT_ENV_MAS_REGEX) @@ -1040,7 +1046,7 @@ public void testListArtifactVersions() throws Exception { .pathParam("artifactId", artifactId) .get("/registry/v2/groups/{groupId}/artifacts/{artifactId}/versions") .then() -// .log().all() + // .log().all() .statusCode(200) .body("count", equalTo(6)) .body("versions[0].version", notNullValue()); @@ -1619,7 +1625,7 @@ public void testArtifactRules() throws Exception { .pathParam("artifactId", artifactId) .get("/registry/v2/groups/{groupId}/artifacts/{artifactId}/rules") .then() -// .log().all() + // .log().all() .statusCode(200) .contentType(ContentType.JSON) .body("[0]", anyOf(equalTo("VALIDITY"), equalTo("COMPATIBILITY"))) @@ -1710,7 +1716,6 @@ public void testArtifactMetaData() throws Exception { .then() .statusCode(204); - // Get the (updated) artifact meta-data TestUtils.retry(() -> { List expectedLabels = Arrays.asList("Empty API label 1", "Empty API label 2"); @@ -1959,7 +1964,6 @@ public void testYamlContentType() throws Exception { .body("info.title", equalTo("Empty API")); } - @Test public void testWsdlArtifact() throws Exception { String artifactId = "testWsdlArtifact"; @@ -2491,7 +2495,6 @@ void testArtifactWithReferences() throws Exception { var referencingMD = metadata; assertEquals(references, metadata.getReferences()); - // Trying to use different references with the same content is ok, but the contentId and contentHash is different. List references2 = List.of(ArtifactReference.builder() .groupId(metadata.getGroupId()) @@ -2539,7 +2542,8 @@ void testArtifactWithReferences() throws Exception { final String referencesSerialized = RegistryContentUtils.serializeReferences(toReferenceDtos(references)); //We calculate the hash using the content itself and the references - String contentHash = DigestUtils.sha256Hex(concatContentAndReferences(artifactContent.getBytes(StandardCharsets.UTF_8), referencesSerialized.getBytes(StandardCharsets.UTF_8))); + String contentHash = DigestUtils.sha256Hex( + concatContentAndReferences(artifactContent.getBytes(StandardCharsets.UTF_8), referencesSerialized.getBytes(StandardCharsets.UTF_8))); assertEquals(references, referenceResponse); @@ -2604,15 +2608,14 @@ void testArtifactWithReferences() throws Exception { assertEquals(referencingMD.getId(), referenceResponse.get(0).getArtifactId()); assertEquals(referencingMD.getVersion(), referenceResponse.get(0).getVersion()); - - // Dereferencing JSON Schema fails with 400 as it's not implemented. + // Dereferencing JSON Schema works given() .when() .pathParam("globalId", metadata.getGlobalId()) .queryParam("dereference", true) .get("/registry/v2/ids/globalIds/{globalId}") .then() - .statusCode(HTTP_BAD_REQUEST); + .statusCode(HTTP_OK); } private byte[] concatContentAndReferences(byte[] contentBytes, byte[] referencesBytes) throws IOException { @@ -2621,7 +2624,7 @@ private byte[] concatContentAndReferences(byte[] contentBytes, byte[] references outputStream.write(referencesBytes); return outputStream.toByteArray(); } - + @Test public void testArtifactComments() throws Exception { String artifactId = "testArtifactComments/EmptyAPI"; @@ -2641,7 +2644,7 @@ public void testArtifactComments() throws Exception { .extract().as(new TypeRef>() { }); assertEquals(0, comments.size()); - + // Create a new comment NewComment nc = NewComment.builder().value("COMMENT_1").build(); Comment comment1 = given() @@ -2659,7 +2662,7 @@ public void testArtifactComments() throws Exception { assertNotNull(comment1.getValue()); assertNotNull(comment1.getCreatedOn()); assertEquals("COMMENT_1", comment1.getValue()); - + // Create another new comment nc = NewComment.builder().value("COMMENT_2").build(); Comment comment2 = given() @@ -2677,7 +2680,7 @@ public void testArtifactComments() throws Exception { assertNotNull(comment2.getValue()); assertNotNull(comment2.getCreatedOn()); assertEquals("COMMENT_2", comment2.getValue()); - + // Get the list of comments (should have 2) comments = given() .when() diff --git a/app/src/test/java/io/apicurio/registry/noprofile/serde/JsonSchemaSerdeTest.java b/app/src/test/java/io/apicurio/registry/noprofile/serde/JsonSchemaSerdeTest.java index 9bc0e8fc3a..2e9e647cb6 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/serde/JsonSchemaSerdeTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/serde/JsonSchemaSerdeTest.java @@ -40,6 +40,8 @@ import io.apicurio.registry.support.Citizen; import io.apicurio.registry.support.CitizenIdentifier; import io.apicurio.registry.support.City; +import io.apicurio.registry.support.CityQualification; +import io.apicurio.registry.support.IdentifierQualification; import io.apicurio.registry.support.Person; import io.apicurio.registry.support.Qualification; import io.apicurio.registry.types.ArtifactType; @@ -96,7 +98,7 @@ public void testJsonSchemaSerde() throws Exception { Person person = new Person("Ales", "Justin", 23); try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); - Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { Map config = new HashMap<>(); config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); @@ -119,7 +121,8 @@ public void testJsonSchemaSerde() throws Exception { try { serializer.serialize(artifactId, new RecordHeaders(), person); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } serializer.setValidationEnabled(false); // disable validation @@ -129,7 +132,8 @@ public void testJsonSchemaSerde() throws Exception { try { deserializer.deserialize(artifactId, headers, bytes); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } } } @@ -142,7 +146,7 @@ public void testJsonSchemaSerdeAutoRegister() throws Exception { Person person = new Person("Carles", "Arnal", 30); try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); - Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { Map config = new HashMap<>(); config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); @@ -167,7 +171,8 @@ public void testJsonSchemaSerdeAutoRegister() throws Exception { try { serializer.serialize(artifactId, new RecordHeaders(), person); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } serializer.setValidationEnabled(false); // disable validation @@ -177,7 +182,8 @@ public void testJsonSchemaSerdeAutoRegister() throws Exception { try { deserializer.deserialize(artifactId, headers, bytes); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } } } @@ -197,7 +203,7 @@ public void testJsonSchemaSerdeHeaders() throws Exception { Person person = new Person("Ales", "Justin", 23); try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); - Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { Map config = new HashMap<>(); config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); @@ -243,7 +249,7 @@ public void testJsonSchemaSerdeMagicByte() throws Exception { Person person = new Person("Ales", "Justin", 23); try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); - Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { Map config = new HashMap<>(); config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); @@ -269,9 +275,9 @@ public void testJsonSchemaSerdeMagicByte() throws Exception { @Test public void testJsonSchemaSerdeWithReferences() throws Exception { - InputStream citySchema = getClass().getResourceAsStream("/io/apicurio/registry/util/city.json"); - InputStream citizenSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/citizen.json"); - InputStream citizenIdentifier = getClass().getResourceAsStream("/io/apicurio/registry/util/citizenIdentifier.json"); + InputStream citySchema = getClass().getResourceAsStream("/io/apicurio/registry/util/city1.json"); + InputStream citizenSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/citizen1.json"); + InputStream citizenIdentifier = getClass().getResourceAsStream("/io/apicurio/registry/util/citizenIdentifier1.json"); InputStream qualificationSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/qualification.json"); InputStream addressSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/sample.address.json"); @@ -282,14 +288,12 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { Assertions.assertNotNull(qualificationSchema); Assertions.assertNotNull(addressSchema); - String groupId = TestUtils.generateGroupId(); String cityArtifactId = generateArtifactId(); String qualificationsId = generateArtifactId(); String identifierArtifactId = generateArtifactId(); String addressId = generateArtifactId(); - final Integer cityDependencyGlobalId = createArtifact(groupId, cityArtifactId, ArtifactType.JSON, IoUtil.toString(citySchema)); this.waitForGlobalId(cityDependencyGlobalId); @@ -315,7 +319,7 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { cityReference.setVersion("1"); cityReference.setGroupId(groupId); cityReference.setArtifactId(cityArtifactId); - cityReference.setName("city.json"); + cityReference.setName("city1.json"); final Integer identifierDependencyGlobalId = createArtifact(groupId, identifierArtifactId, ArtifactType.JSON, IoUtil.toString(citizenIdentifier)); this.waitForGlobalId(identifierDependencyGlobalId); @@ -324,11 +328,12 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { identifierReference.setVersion("1"); identifierReference.setGroupId(groupId); identifierReference.setArtifactId(identifierArtifactId); - identifierReference.setName("citizenIdentifier.json"); + identifierReference.setName("citizenIdentifier1.json"); String artifactId = generateArtifactId(); - final Integer globalId = createArtifactWithReferences(groupId, artifactId, ArtifactType.JSON, IoUtil.toString(citizenSchema), List.of(qualificationsReference, cityReference, identifierReference, addressReference)); + final Integer globalId = createArtifactWithReferences(groupId, artifactId, ArtifactType.JSON, IoUtil.toString(citizenSchema), + List.of(qualificationsReference, cityReference, identifierReference, addressReference)); this.waitForGlobalId(globalId); City city = new City("New York", 10001); @@ -336,7 +341,7 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { Citizen citizen = new Citizen("Carles", "Arnal", 23, city, identifier, Collections.emptyList()); try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); - Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { Map config = new HashMap<>(); config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); @@ -360,7 +365,8 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { try { serializer.serialize(artifactId, new RecordHeaders(), citizen); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } citizen.setAge(23); @@ -370,9 +376,151 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { try { serializer.serialize(artifactId, new RecordHeaders(), citizen); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { + } + + //invalid identifier present, should fail + identifier = new CitizenIdentifier(-1234356); + citizen.setIdentifier(identifier); + + city = new City("Kansas CIty", 22222); + citizen.setCity(city); + + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } + + //no identifier present, should pass + citizen.setIdentifier(null); + serializer.serialize(artifactId, new RecordHeaders(), citizen); + + //valid qualification, should pass + citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), 7), + new Qualification(UUID.randomUUID().toString(), 8))); + serializer.serialize(artifactId, new RecordHeaders(), citizen); + + //invalid qualification, should fail + citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), -7), + new Qualification(UUID.randomUUID().toString(), 8))); + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } + } + } + + @Test + public void testJsonSchemaSerdeWithReferencesDeserializerDereferenced() throws Exception { + InputStream citySchema = getClass().getResourceAsStream("/io/apicurio/registry/util/city1.json"); + InputStream citizenSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/citizen1.json"); + InputStream citizenIdentifier = getClass().getResourceAsStream("/io/apicurio/registry/util/citizenIdentifier1.json"); + InputStream qualificationSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/qualification.json"); + + InputStream addressSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/sample.address.json"); + + Assertions.assertNotNull(citizenSchema); + Assertions.assertNotNull(citySchema); + Assertions.assertNotNull(citizenIdentifier); + Assertions.assertNotNull(qualificationSchema); + Assertions.assertNotNull(addressSchema); + + String groupId = TestUtils.generateGroupId(); + String cityArtifactId = generateArtifactId(); + String qualificationsId = generateArtifactId(); + String identifierArtifactId = generateArtifactId(); + String addressId = generateArtifactId(); + + final Integer cityDependencyGlobalId = createArtifact(groupId, cityArtifactId, ArtifactType.JSON, IoUtil.toString(citySchema)); + this.waitForGlobalId(cityDependencyGlobalId); + + final Integer qualificationsGlobalId = createArtifact(groupId, qualificationsId, ArtifactType.JSON, IoUtil.toString(qualificationSchema)); + this.waitForGlobalId(qualificationsGlobalId); + + final ArtifactReference qualificationsReference = new ArtifactReference(); + qualificationsReference.setVersion("1"); + qualificationsReference.setGroupId(groupId); + qualificationsReference.setArtifactId(qualificationsId); + qualificationsReference.setName("qualification.json"); + + final Integer addressGlobalID = createArtifact(groupId, addressId, ArtifactType.JSON, IoUtil.toString(addressSchema)); + this.waitForGlobalId(addressGlobalID); + + final ArtifactReference addressReference = new ArtifactReference(); + addressReference.setVersion("1"); + addressReference.setGroupId(groupId); + addressReference.setArtifactId(addressId); + addressReference.setName("sample.address.json"); + + final ArtifactReference cityReference = new ArtifactReference(); + cityReference.setVersion("1"); + cityReference.setGroupId(groupId); + cityReference.setArtifactId(cityArtifactId); + cityReference.setName("city1.json"); + + final Integer identifierDependencyGlobalId = createArtifact(groupId, identifierArtifactId, ArtifactType.JSON, IoUtil.toString(citizenIdentifier)); + this.waitForGlobalId(identifierDependencyGlobalId); + + final ArtifactReference identifierReference = new ArtifactReference(); + identifierReference.setVersion("1"); + identifierReference.setGroupId(groupId); + identifierReference.setArtifactId(identifierArtifactId); + identifierReference.setName("citizenIdentifier1.json"); + + String artifactId = generateArtifactId(); + + final Integer globalId = createArtifactWithReferences(groupId, artifactId, ArtifactType.JSON, IoUtil.toString(citizenSchema), + List.of(qualificationsReference, cityReference, identifierReference, addressReference)); + this.waitForGlobalId(globalId); + + City city = new City("New York", 10001); + CitizenIdentifier identifier = new CitizenIdentifier(123456789); + Citizen citizen = new Citizen("Carles", "Arnal", 23, city, identifier, Collections.emptyList()); + + try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + + Map config = new HashMap<>(); + config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); + config.put(SerdeConfig.ARTIFACT_RESOLVER_STRATEGY, SimpleTopicIdStrategy.class.getName()); + serializer.configure(config, false); + + deserializer.configure(Map.of(SchemaResolverConfig.DESERIALIZER_DEREFERENCE_SCHEMA, "true"), false); + + Headers headers = new RecordHeaders(); + byte[] bytes = serializer.serialize(artifactId, headers, citizen); + + citizen = deserializer.deserialize(artifactId, headers, bytes); + + Assertions.assertEquals("Carles", citizen.getFirstName()); + Assertions.assertEquals("Arnal", citizen.getLastName()); + Assertions.assertEquals(23, citizen.getAge()); + Assertions.assertEquals("New York", citizen.getCity().getName()); + + citizen.setAge(-1); + + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { } + citizen.setAge(23); + city = new City("Kansas CIty", -31); + citizen.setCity(city); + + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } //invalid identifier present, should fail identifier = new CitizenIdentifier(-1234356); @@ -384,7 +532,8 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { try { serializer.serialize(artifactId, new RecordHeaders(), citizen); Assertions.fail(); - } catch (Exception ignored) { + } + catch (Exception ignored) { } //no identifier present, should pass @@ -392,16 +541,181 @@ public void testJsonSchemaSerdeWithReferences() throws Exception { serializer.serialize(artifactId, new RecordHeaders(), citizen); //valid qualification, should pass - citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), 7), new Qualification(UUID.randomUUID().toString(), 8))); + citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), 7), + new Qualification(UUID.randomUUID().toString(), 8))); serializer.serialize(artifactId, new RecordHeaders(), citizen); //invalid qualification, should fail - citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), -7), new Qualification(UUID.randomUUID().toString(), 8))); + citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), -7), + new Qualification(UUID.randomUUID().toString(), 8))); + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } + } + } + + @Test + public void testWithReferencesDeserializerDereferencedComplexUsecase() throws Exception { + InputStream citySchema = getClass().getResourceAsStream("/io/apicurio/registry/util/types/city/city.json"); + InputStream citizenSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/citizen.json"); + InputStream citizenIdentifier = getClass().getResourceAsStream("/io/apicurio/registry/util/types/identifier/citizenIdentifier.json"); + InputStream qualificationSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/qualification.json"); + InputStream addressSchema = getClass().getResourceAsStream("/io/apicurio/registry/util/sample.address.json"); + + InputStream identifierQuarlification = getClass().getResourceAsStream("/io/apicurio/registry/util/types/identifier/qualification.json"); + InputStream cityQualification = getClass().getResourceAsStream("/io/apicurio/registry/util/types/city/qualification.json"); + + Assertions.assertNotNull(citizenSchema); + Assertions.assertNotNull(citySchema); + Assertions.assertNotNull(citizenIdentifier); + Assertions.assertNotNull(qualificationSchema); + Assertions.assertNotNull(addressSchema); + Assertions.assertNotNull(identifierQuarlification); + Assertions.assertNotNull(cityQualification); + + String groupId = TestUtils.generateGroupId(); + String cityArtifactId = generateArtifactId(); + String qualificationsId = generateArtifactId(); + String identifierArtifactId = generateArtifactId(); + String addressId = generateArtifactId(); + String identifierQualificationId = generateArtifactId(); + String cityQualificationId = generateArtifactId(); + + //Create the two nested qualification schemas, one for the city, and one for the identifier + final Integer identifierQualificationDependencyGlobalId = createArtifact(groupId, identifierQualificationId, ArtifactType.JSON, IoUtil.toString(identifierQuarlification)); + this.waitForGlobalId(identifierQualificationDependencyGlobalId); + final Integer cityQualificationDependencyGlobalId = createArtifact(groupId, cityQualificationId, ArtifactType.JSON, IoUtil.toString(cityQualification)); + this.waitForGlobalId(cityQualificationDependencyGlobalId); + + final ArtifactReference cityQualificationReference = new ArtifactReference(); + cityQualificationReference.setVersion("1"); + cityQualificationReference.setGroupId(groupId); + cityQualificationReference.setArtifactId(cityQualificationId); + cityQualificationReference.setName("qualification.json"); + + //create the city schema with the reference to its qualification + final Integer cityDependencyGlobalId = createArtifactWithReferences(groupId, cityArtifactId, ArtifactType.JSON, IoUtil.toString(citySchema), + List.of(cityQualificationReference)); + this.waitForGlobalId(cityDependencyGlobalId); + + final ArtifactReference identifierQualificationReference = new ArtifactReference(); + identifierQualificationReference.setVersion("1"); + identifierQualificationReference.setGroupId(groupId); + identifierQualificationReference.setArtifactId(identifierQualificationId); + identifierQualificationReference.setName("qualification.json"); + + //create the identifier schema with the reference to its qualification + final Integer identifierDependencyGlobalId = createArtifactWithReferences(groupId, identifierArtifactId, ArtifactType.JSON, IoUtil.toString(citizenIdentifier), + List.of(identifierQualificationReference)); + this.waitForGlobalId(identifierDependencyGlobalId); + + //create the main qualification schema, used for the citizen + final Integer qualificationsGlobalId = createArtifact(groupId, qualificationsId, ArtifactType.JSON, IoUtil.toString(qualificationSchema)); + this.waitForGlobalId(qualificationsGlobalId); + + final ArtifactReference qualificationsReference = new ArtifactReference(); + qualificationsReference.setVersion("1"); + qualificationsReference.setGroupId(groupId); + qualificationsReference.setArtifactId(qualificationsId); + qualificationsReference.setName("qualification.json"); + + final Integer addressGlobalID = createArtifact(groupId, addressId, ArtifactType.JSON, IoUtil.toString(addressSchema)); + this.waitForGlobalId(addressGlobalID); + + final ArtifactReference addressReference = new ArtifactReference(); + addressReference.setVersion("1"); + addressReference.setGroupId(groupId); + addressReference.setArtifactId(addressId); + addressReference.setName("sample.address.json"); + + final ArtifactReference cityReference = new ArtifactReference(); + cityReference.setVersion("1"); + cityReference.setGroupId(groupId); + cityReference.setArtifactId(cityArtifactId); + cityReference.setName("types/city/city.json"); + + final ArtifactReference identifierReference = new ArtifactReference(); + identifierReference.setVersion("1"); + identifierReference.setGroupId(groupId); + identifierReference.setArtifactId(identifierArtifactId); + identifierReference.setName("types/identifier/citizenIdentifier.json"); + + String artifactId = generateArtifactId(); + + //create the citizen schema, with references to qualifications, city, identifier and address + final Integer globalId = createArtifactWithReferences(groupId, artifactId, ArtifactType.JSON, IoUtil.toString(citizenSchema), + List.of(qualificationsReference, cityReference, identifierReference, addressReference)); + this.waitForGlobalId(globalId); + + City city = new City("New York", 10001); + CitizenIdentifier identifier = new CitizenIdentifier(123456789); + Citizen citizen = new Citizen("Carles", "Arnal", 23, city, identifier, Collections.emptyList()); + + try (JsonSchemaKafkaSerializer serializer = new JsonSchemaKafkaSerializer<>(restClient, true); + Deserializer deserializer = new JsonSchemaKafkaDeserializer<>(restClient, true)) { + + Map config = new HashMap<>(); + config.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, groupId); + config.put(SerdeConfig.ARTIFACT_RESOLVER_STRATEGY, SimpleTopicIdStrategy.class.getName()); + config.put(SchemaResolverConfig.SERIALIZER_DEREFERENCE_SCHEMA, "true"); + serializer.configure(config, false); + + deserializer.configure(Map.of(SchemaResolverConfig.DESERIALIZER_DEREFERENCE_SCHEMA, "true"), false); + + Headers headers = new RecordHeaders(); + byte[] bytes = serializer.serialize(artifactId, headers, citizen); + + citizen = deserializer.deserialize(artifactId, headers, bytes); + + Assertions.assertEquals("Carles", citizen.getFirstName()); + Assertions.assertEquals("Arnal", citizen.getLastName()); + Assertions.assertEquals(23, citizen.getAge()); + Assertions.assertEquals("New York", citizen.getCity().getName()); + + //invalid qualification, should fail + citizen.setQualifications(List.of(new Qualification(UUID.randomUUID().toString(), 6), new Qualification(UUID.randomUUID().toString(), -7), + new Qualification(UUID.randomUUID().toString(), 8))); + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } + + //invalid city qualification, minimum is 10 should fail + city.setQualification(new CityQualification("city_qualification", 9)); + citizen.setCity(city); + citizen.setQualifications(Collections.emptyList()); try { serializer.serialize(artifactId, new RecordHeaders(), citizen); Assertions.fail(); - } catch (Exception ignored) { } + catch (Exception ignored) { + } + + //valid city qualification, should pass + city.setQualification(new CityQualification("city_qualification", 11)); + citizen.setCity(city); + citizen.setQualifications(Collections.emptyList()); + serializer.serialize(artifactId, new RecordHeaders(), citizen); + + //invalid identifier qualification, minimum is 20, should fail + identifier.setIdentifierQualification(new IdentifierQualification("test_subject", 19)); + citizen.setIdentifier(identifier); + try { + serializer.serialize(artifactId, new RecordHeaders(), citizen); + Assertions.fail(); + } + catch (Exception ignored) { + } + + //valid identifier qualification + identifier.setIdentifierQualification(new IdentifierQualification("test_subject", 20)); + citizen.setIdentifier(identifier); + serializer.serialize(artifactId, new RecordHeaders(), citizen); } } @@ -442,7 +756,6 @@ public void complexObjectValidation() throws Exception { ContentTypes.APPLICATION_CREATE_EXTENDED, null, null, phone, null); - final ArtifactReference addressReference = new ArtifactReference(); addressReference.setVersion(amdAddress.getVersion()); addressReference.setGroupId(amdAddress.getGroupId()); diff --git a/app/src/test/java/io/apicurio/registry/support/CitizenIdentifier.java b/app/src/test/java/io/apicurio/registry/support/CitizenIdentifier.java index 5534fbadc7..2de8f59b23 100644 --- a/app/src/test/java/io/apicurio/registry/support/CitizenIdentifier.java +++ b/app/src/test/java/io/apicurio/registry/support/CitizenIdentifier.java @@ -23,6 +23,9 @@ public class CitizenIdentifier { @JsonProperty("identifier") private Integer identifier; + @JsonProperty("qualification") + private IdentifierQualification identifierQualification; + public CitizenIdentifier() { } @@ -30,6 +33,11 @@ public CitizenIdentifier(Integer identifier) { this.identifier = identifier; } + public CitizenIdentifier(Integer identifier, IdentifierQualification identifierQualification) { + this.identifier = identifier; + this.identifierQualification = identifierQualification; + } + public Integer getIdentifier() { return identifier; } @@ -37,4 +45,12 @@ public Integer getIdentifier() { public void setIdentifier(Integer identifier) { this.identifier = identifier; } + + public IdentifierQualification getIdentifierQualification() { + return identifierQualification; + } + + public void setIdentifierQualification(IdentifierQualification identifierQualification) { + this.identifierQualification = identifierQualification; + } } diff --git a/app/src/test/java/io/apicurio/registry/support/City.java b/app/src/test/java/io/apicurio/registry/support/City.java index 6d8233856a..f2eb4387bd 100644 --- a/app/src/test/java/io/apicurio/registry/support/City.java +++ b/app/src/test/java/io/apicurio/registry/support/City.java @@ -26,6 +26,9 @@ public class City { @JsonProperty("zipCode") private Integer zipCode; + @JsonProperty("qualification") + private CityQualification qualification; + public City() { } @@ -34,6 +37,12 @@ public City(String name, Integer zipCode) { this.zipCode = zipCode; } + public City(String name, Integer zipCode, CityQualification cityQualification) { + this.name = name; + this.zipCode = zipCode; + this.qualification = cityQualification; + } + public String getName() { return name; } @@ -49,4 +58,12 @@ public Integer getZipCode() { public void setZipCode(Integer zipCode) { this.zipCode = zipCode; } + + public CityQualification getQualification() { + return qualification; + } + + public void setQualification(CityQualification qualification) { + this.qualification = qualification; + } } diff --git a/app/src/test/java/io/apicurio/registry/support/CityQualification.java b/app/src/test/java/io/apicurio/registry/support/CityQualification.java new file mode 100644 index 0000000000..04aaa65799 --- /dev/null +++ b/app/src/test/java/io/apicurio/registry/support/CityQualification.java @@ -0,0 +1,36 @@ +package io.apicurio.registry.support; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CityQualification { + + @JsonProperty("subject_name") + private String subjectName; + + @JsonProperty("qualification") + private int qualification; + + public CityQualification() { + } + + public CityQualification(String subjectName, int qualification) { + this.subjectName = subjectName; + this.qualification = qualification; + } + + public String getSubjectName() { + return subjectName; + } + + public void setSubjectName(String subjectName) { + this.subjectName = subjectName; + } + + public int getQualification() { + return qualification; + } + + public void setQualification(int qualification) { + this.qualification = qualification; + } +} diff --git a/app/src/test/java/io/apicurio/registry/support/IdentifierQualification.java b/app/src/test/java/io/apicurio/registry/support/IdentifierQualification.java new file mode 100644 index 0000000000..731c4d4ffb --- /dev/null +++ b/app/src/test/java/io/apicurio/registry/support/IdentifierQualification.java @@ -0,0 +1,36 @@ +package io.apicurio.registry.support; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IdentifierQualification { + + @JsonProperty("subject_name") + private String subjectName; + + @JsonProperty("qualification") + private int qualification; + + public IdentifierQualification() { + } + + public IdentifierQualification(String subjectName, int qualification) { + this.subjectName = subjectName; + this.qualification = qualification; + } + + public String getSubjectName() { + return subjectName; + } + + public void setSubjectName(String subjectName) { + this.subjectName = subjectName; + } + + public int getQualification() { + return qualification; + } + + public void setQualification(int qualification) { + this.qualification = qualification; + } +} diff --git a/app/src/test/resources/io/apicurio/registry/util/citizen.json b/app/src/test/resources/io/apicurio/registry/util/citizen.json index 700fb09a83..daa67f08e4 100644 --- a/app/src/test/resources/io/apicurio/registry/util/citizen.json +++ b/app/src/test/resources/io/apicurio/registry/util/citizen.json @@ -18,10 +18,10 @@ "minimum": 0 }, "city": { - "$ref": "city.json" + "$ref": "types/city/city.json" }, "identifier": { - "$ref": "citizenIdentifier.json" + "$ref": "types/identifier/citizenIdentifier.json" }, "qualifications": { "type": "array", diff --git a/app/src/test/resources/io/apicurio/registry/util/citizen1.json b/app/src/test/resources/io/apicurio/registry/util/citizen1.json new file mode 100644 index 0000000000..abea24149f --- /dev/null +++ b/app/src/test/resources/io/apicurio/registry/util/citizen1.json @@ -0,0 +1,36 @@ +{ + "$id": "https://example.com/citizen1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Citizen", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The citizen's first name." + }, + "lastName": { + "type": "string", + "description": "The citizen's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + }, + "city": { + "$ref": "city1.json" + }, + "identifier": { + "$ref": "citizenIdentifier1.json" + }, + "qualifications": { + "type": "array", + "items": { + "$ref": "qualification.json" + } + } + }, + "required": [ + "city" + ] +} \ No newline at end of file diff --git a/app/src/test/resources/io/apicurio/registry/util/citizenIdentifier.json b/app/src/test/resources/io/apicurio/registry/util/citizenIdentifier1.json similarity index 81% rename from app/src/test/resources/io/apicurio/registry/util/citizenIdentifier.json rename to app/src/test/resources/io/apicurio/registry/util/citizenIdentifier1.json index 3a896e55f0..2b4c20118a 100644 --- a/app/src/test/resources/io/apicurio/registry/util/citizenIdentifier.json +++ b/app/src/test/resources/io/apicurio/registry/util/citizenIdentifier1.json @@ -1,5 +1,5 @@ { - "$id": "https://example.com/citizenIdentifier.json", + "$id": "https://example.com/citizenIdentifier1.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Identifier", "type": "object", diff --git a/app/src/test/resources/io/apicurio/registry/util/city.json b/app/src/test/resources/io/apicurio/registry/util/city1.json similarity index 87% rename from app/src/test/resources/io/apicurio/registry/util/city.json rename to app/src/test/resources/io/apicurio/registry/util/city1.json index a86e3e473c..a02d7ef37e 100644 --- a/app/src/test/resources/io/apicurio/registry/util/city.json +++ b/app/src/test/resources/io/apicurio/registry/util/city1.json @@ -1,5 +1,5 @@ { - "$id": "https://example.com/city.json", + "$id": "https://example.com/city1.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "City", "type": "object", diff --git a/app/src/test/resources/io/apicurio/registry/util/types/city/city.json b/app/src/test/resources/io/apicurio/registry/util/types/city/city.json new file mode 100644 index 0000000000..66a1105c0a --- /dev/null +++ b/app/src/test/resources/io/apicurio/registry/util/types/city/city.json @@ -0,0 +1,20 @@ +{ + "$id": "https://example.com/types/city/city.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "City", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The city's name." + }, + "zipCode": { + "type": "integer", + "description": "The zip code.", + "minimum": 0 + }, + "qualification": { + "$ref": "qualification.json" + } + } +} \ No newline at end of file diff --git a/app/src/test/resources/io/apicurio/registry/util/types/city/qualification.json b/app/src/test/resources/io/apicurio/registry/util/types/city/qualification.json new file mode 100644 index 0000000000..4f19d81a31 --- /dev/null +++ b/app/src/test/resources/io/apicurio/registry/util/types/city/qualification.json @@ -0,0 +1,17 @@ +{ + "$id": "https://example.com/types/city/qualification.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Qualification", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The subject's name" + }, + "qualification": { + "type": "integer", + "description": "The city qualification", + "minimum": 10 + } + } +} \ No newline at end of file diff --git a/app/src/test/resources/io/apicurio/registry/util/types/identifier/citizenIdentifier.json b/app/src/test/resources/io/apicurio/registry/util/types/identifier/citizenIdentifier.json new file mode 100644 index 0000000000..0c4677f84a --- /dev/null +++ b/app/src/test/resources/io/apicurio/registry/util/types/identifier/citizenIdentifier.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/types/identifier/citizenIdentifier.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Identifier", + "type": "object", + "properties": { + "identifier": { + "type": "integer", + "description": "The citizen identifier.", + "minimum": 0 + }, + "qualification": { + "$ref": "qualification.json" + } + } +} \ No newline at end of file diff --git a/app/src/test/resources/io/apicurio/registry/util/types/identifier/qualification.json b/app/src/test/resources/io/apicurio/registry/util/types/identifier/qualification.json new file mode 100644 index 0000000000..931557b9d1 --- /dev/null +++ b/app/src/test/resources/io/apicurio/registry/util/types/identifier/qualification.json @@ -0,0 +1,17 @@ +{ + "$id": "https://example.com/types/identifier/qualification.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Qualification", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The subject's name" + }, + "qualification": { + "type": "integer", + "description": "The identifier qualification", + "minimum": 20 + } + } +} \ No newline at end of file diff --git a/docs/modules/ROOT/partials/getting-started/proc-managing-artifact-references-using-rest-api.adoc b/docs/modules/ROOT/partials/getting-started/proc-managing-artifact-references-using-rest-api.adoc index b079c88d34..6f4fd361f2 100644 --- a/docs/modules/ROOT/partials/getting-started/proc-managing-artifact-references-using-rest-api.adoc +++ b/docs/modules/ROOT/partials/getting-started/proc-managing-artifact-references-using-rest-api.adoc @@ -150,9 +150,11 @@ $ curl -H "Authorization: Bearer $ACCESS_TOKEN" MY-REGISTRY-URL/apis/registry/v2 There are some cases where returning artifact content with referenced content inline might be helpful. For these cases, the Core Registry API v2 supports the `dereference` query parameter in certain operations. -This support is currently implemented only for Avro and Protobuf artifacts when the `dereference` parameter is specified in the API operation. This parameter is not supported for any other artifact types. +This support is currently implemented only for Avro, Protobuf, and JSON Schema artifacts when the `dereference` parameter is specified in the API operation. This parameter is not supported for any other artifact types. -NOTE: For Protobuf artifacts, dereferencing content is supported only when all of the schemas belong to the same package. +NOTE: For Protobuf artifacts, dereferencing content is supported only when all the schemas belong to the same package. + +NOTE: For JSON Schema artifacts, dereferencing content is supported only for artifacts that reference the full content of a separate artifact. Dereferencing when an artifact references a part of a second artifact is *not* supported [role="_additional-resources"] diff --git a/pom.xml b/pom.xml index 427d63edf6..b6f3891043 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,7 @@ 1.11.3 1.4.0 + 4.5.7 4.9.9 4.9.9 3.9.0 @@ -562,6 +563,11 @@ json-schema-validator ${json-schema-validator.version} + + io.vertx + vertx-json-schema + ${vertx-json-schema.version} + com.squareup.wire wire-schema diff --git a/schema-resolver/src/main/java/io/apicurio/registry/resolver/AbstractSchemaResolver.java b/schema-resolver/src/main/java/io/apicurio/registry/resolver/AbstractSchemaResolver.java index 42d1df7ad8..696ac87fb9 100644 --- a/schema-resolver/src/main/java/io/apicurio/registry/resolver/AbstractSchemaResolver.java +++ b/schema-resolver/src/main/java/io/apicurio/registry/resolver/AbstractSchemaResolver.java @@ -62,7 +62,8 @@ public abstract class AbstractSchemaResolver implements SchemaResolver configs, SchemaParser schemaParser) { @@ -123,7 +124,7 @@ public void configure(Map configs, SchemaParser schemaParser) { this.explicitArtifactVersion = artifactVersionOverride; } - this.deserializerDereference = config.deserializerDereference(); + this.dereference = config.deserializerDereference() || config.serializerDereference(); } /** @@ -182,7 +183,7 @@ protected String resolveArtifactId(String artifactId, boolean isReference, Strin protected SchemaLookupResult resolveSchemaByGlobalId(long globalId) { return schemaCache.getByGlobalId(globalId, globalIdKey -> { - if (deserializerDereference) { + if (dereference) { return resolveSchemaDereferenced(globalIdKey); } else { return resolveSchemaWithReferences(globalIdKey); diff --git a/schema-resolver/src/main/java/io/apicurio/registry/resolver/DefaultSchemaResolver.java b/schema-resolver/src/main/java/io/apicurio/registry/resolver/DefaultSchemaResolver.java index 3131a6eaff..80d004e685 100644 --- a/schema-resolver/src/main/java/io/apicurio/registry/resolver/DefaultSchemaResolver.java +++ b/schema-resolver/src/main/java/io/apicurio/registry/resolver/DefaultSchemaResolver.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,7 +44,7 @@ public class DefaultSchemaResolver extends AbstractSchemaResolver { private boolean autoCreateArtifact; private IfExists autoCreateBehavior; private boolean findLatest; - private boolean dereference; + private boolean registerDereferenced; /** * @see io.apicurio.registry.resolver.AbstractSchemaResolver#reset() @@ -65,7 +66,7 @@ public void configure(Map configs, SchemaParser schemaParser) { } this.autoCreateArtifact = config.autoRegisterArtifact(); - this.dereference = config.serializerDereference(); + this.registerDereferenced = config.registerDereferenced(); this.autoCreateBehavior = IfExists.fromValue(config.autoRegisterArtifactIfExists()); this.findLatest = config.findLatest(); } @@ -81,7 +82,7 @@ public SchemaLookupResult resolveSchema(Record data) { ParsedSchema parsedSchema = null; if (artifactResolverStrategy.loadSchema() && schemaParser.supportsExtractSchemaFromData()) { - parsedSchema = schemaParser.getSchemaFromData(data, dereference); + parsedSchema = schemaParser.getSchemaFromData(data, registerDereferenced); } final ArtifactReference artifactReference = resolveArtifactReference(data, parsedSchema, false, null); @@ -111,7 +112,7 @@ private SchemaLookupResult getSchemaFromRegistry(ParsedSchema parsedSchema if (schemaParser.supportsExtractSchemaFromData()) { if (parsedSchema == null) { - parsedSchema = schemaParser.getSchemaFromData(data, dereference); + parsedSchema = schemaParser.getSchemaFromData(data, registerDereferenced); } if (parsedSchema.hasReferences()) { @@ -355,13 +356,17 @@ private SchemaLookupResult resolveByCoordinates(String groupId, String artifa loadFromArtifactMetaData(metadata, result); gid = metadata.getGlobalId(); } - - InputStream rawSchema = client.getContentByGlobalId(gid); - - //Get the artifact references - final List artifactReferences = client.getArtifactReferencesByGlobalId(gid); - //If there are any references for the schema being parsed, resolve them before parsing the schema - final Map> resolvedReferences = resolveReferences(artifactReferences); + InputStream rawSchema; + Map> resolvedReferences = new HashMap<>(); + if (dereference) { + rawSchema = client.getContentByGlobalId(gid, false, true); + } else { + rawSchema = client.getContentByGlobalId(gid); + //Get the artifact references + final List artifactReferences = client.getArtifactReferencesByGlobalId(gid); + //If there are any references for the schema being parsed, resolve them before parsing the schema + resolvedReferences = resolveReferences(artifactReferences); + } byte[] schema = IoUtil.toBytes(rawSchema); S parsed = schemaParser.parseSchema(schema, resolvedReferences); diff --git a/schema-resolver/src/main/java/io/apicurio/registry/resolver/SchemaResolverConfig.java b/schema-resolver/src/main/java/io/apicurio/registry/resolver/SchemaResolverConfig.java index 3b046d9e1d..93da3b8153 100644 --- a/schema-resolver/src/main/java/io/apicurio/registry/resolver/SchemaResolverConfig.java +++ b/schema-resolver/src/main/java/io/apicurio/registry/resolver/SchemaResolverConfig.java @@ -218,9 +218,14 @@ public class SchemaResolverConfig { public static final String DEREFERENCE_SCHEMA = "apicurio.registry.dereference-schema"; public static final boolean DEREFERENCE_SCHEMA_DEFAULT = false; + /** + * Used to indicate the serializer to ask Registry to return the schema dereferenced. This is useful to reduce the number of http requests to the server. + */ + public static final String SERIALIZER_DEREFERENCE_SCHEMA = "apicurio.registry.serializer.dereference-schema"; + public static final boolean SERIALIZER_DEREFERENCE_SCHEMA_DEFAULT = false; + /** * Used to indicate the deserializer to ask Registry to return the schema dereferenced. This is useful to reduce the number of http requests to the server. - * Only applicable to Avro schemas. */ public static final String DESERIALIZER_DEREFERENCE_SCHEMA = "apicurio.registry.deserializer.dereference-schema"; public static final boolean DESERIALIZER_DEREFERENCE_SCHEMA_DEFAULT = false; diff --git a/schema-resolver/src/main/java/io/apicurio/registry/resolver/config/DefaultSchemaResolverConfig.java b/schema-resolver/src/main/java/io/apicurio/registry/resolver/config/DefaultSchemaResolverConfig.java index 9dd5e2b7ab..e30507de4f 100644 --- a/schema-resolver/src/main/java/io/apicurio/registry/resolver/config/DefaultSchemaResolverConfig.java +++ b/schema-resolver/src/main/java/io/apicurio/registry/resolver/config/DefaultSchemaResolverConfig.java @@ -42,7 +42,8 @@ public class DefaultSchemaResolverConfig { entry(RETRY_COUNT, RETRY_COUNT_DEFAULT), entry(RETRY_BACKOFF_MS, RETRY_BACKOFF_MS_DEFAULT), entry(DEREFERENCE_SCHEMA, DEREFERENCE_SCHEMA_DEFAULT), - entry(DESERIALIZER_DEREFERENCE_SCHEMA, DESERIALIZER_DEREFERENCE_SCHEMA_DEFAULT) + entry(DESERIALIZER_DEREFERENCE_SCHEMA, DESERIALIZER_DEREFERENCE_SCHEMA_DEFAULT), + entry(SERIALIZER_DEREFERENCE_SCHEMA, SERIALIZER_DEREFERENCE_SCHEMA_DEFAULT) ); private Map originals; @@ -155,7 +156,7 @@ Object getObject(String key) { return originals.get(key); } - public boolean serializerDereference() { + public boolean registerDereferenced() { return getBooleanOrFalse(DEREFERENCE_SCHEMA); } @@ -163,6 +164,10 @@ public boolean deserializerDereference() { return getBooleanOrFalse(DESERIALIZER_DEREFERENCE_SCHEMA); } + public boolean serializerDereference() { + return getBooleanOrFalse(SERIALIZER_DEREFERENCE_SCHEMA); + } + private Duration getDurationNonNegativeMillis(String key) { Object value = getObject(key); if (value == null) { diff --git a/schema-util/json/pom.xml b/schema-util/json/pom.xml index 38b9bc160f..ba8b20af9f 100644 --- a/schema-util/json/pom.xml +++ b/schema-util/json/pom.xml @@ -42,6 +42,10 @@ com.github.everit-org.json-schema org.everit.json.schema + + io.vertx + vertx-json-schema + com.fasterxml.jackson.datatype jackson-datatype-json-org diff --git a/schema-util/json/src/main/java/io/apicurio/registry/content/dereference/JsonSchemaDereferencer.java b/schema-util/json/src/main/java/io/apicurio/registry/content/dereference/JsonSchemaDereferencer.java index 189fc51e86..78a06f8671 100644 --- a/schema-util/json/src/main/java/io/apicurio/registry/content/dereference/JsonSchemaDereferencer.java +++ b/schema-util/json/src/main/java/io/apicurio/registry/content/dereference/JsonSchemaDereferencer.java @@ -16,6 +16,7 @@ package io.apicurio.registry.content.dereference; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,7 +27,15 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import io.apicurio.registry.content.ContentHandle; +import io.vertx.core.json.JsonObject; +import io.vertx.json.schema.Draft; +import io.vertx.json.schema.JsonSchema; +import io.vertx.json.schema.JsonSchemaOptions; +import io.vertx.json.schema.impl.JsonRef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -38,6 +47,9 @@ public class JsonSchemaDereferencer implements ContentDereferencer { private static final ObjectMapper objectMapper; + private static final Logger log = LoggerFactory.getLogger(JsonSchemaDereferencer.class); + private static final String idKey = "$id"; + private static final String schemaKey = "$schema"; static { objectMapper = new ObjectMapper(); @@ -52,7 +64,47 @@ public class JsonSchemaDereferencer implements ContentDereferencer { @Override public ContentHandle dereference(ContentHandle content, Map resolvedReferences) { - throw new DereferencingNotSupportedException("Content dereferencing is not supported for JSON Schema"); + //Here, when using rewrite, I need the new reference coordinates, using the full artifact coordinates + // and not just the reference name and the old name, to be able to do the re-write. + String id = null; + String schema = null; + + try { + JsonNode contentNode = objectMapper.readTree(content.content()); + id = contentNode.get(idKey).asText(); + schema = contentNode.get(schemaKey).asText(); + } + catch (JsonProcessingException e) { + log.warn("No schema or id provided for schema"); + } + + JsonSchemaOptions jsonSchemaOptions = new JsonSchemaOptions() + .setBaseUri("http://localhost"); + + if (null != schema) { + jsonSchemaOptions.setDraft(Draft.fromIdentifier(schema)); + } + + Map lookups = new HashMap<>(); + resolveReferences(resolvedReferences, lookups); + JsonObject resolvedSchema = JsonRef.resolve(new JsonObject(content.content()), lookups); + + if (null != id) { + resolvedSchema.put(idKey, id); + } + + if (schema != null) { + resolvedSchema.put(schemaKey, schema); + } + + return ContentHandle.create(resolvedSchema.encodePrettily()); + } + + private void resolveReferences(Map resolvedReferences, Map lookups) { + resolvedReferences.forEach((referenceName, schema) -> { + JsonObject resolvedSchema = JsonRef.resolve(new JsonObject(schema.content()), lookups); + lookups.put(referenceName, JsonSchema.of(resolvedSchema)); + }); } /** @@ -65,7 +117,8 @@ public ContentHandle rewriteReferences(ContentHandle content, Map