io.streamnative.pulsar.handlers.kop.schemaregistry.resources.SubjectVersionsResource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pulsar-kafka-schema-registry Show documentation
Show all versions of pulsar-kafka-schema-registry Show documentation
Kafka Compatible Schema Registry
The newest version!
/**
* Copyright (c) 2019 - 2024 StreamNative, Inc.. All Rights Reserved.
*/
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.streamnative.pulsar.handlers.kop.schemaregistry.resources;
import com.google.common.util.concurrent.UncheckedExecutionException;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.streamnative.pulsar.handlers.kop.schemaregistry.HttpJsonRequestProcessor;
import io.streamnative.pulsar.handlers.kop.schemaregistry.SchemaRegistryHandler;
import io.streamnative.pulsar.handlers.kop.schemaregistry.exceptions.Errors;
import io.streamnative.pulsar.handlers.kop.schemaregistry.exceptions.InvalidSchemaException;
import io.streamnative.pulsar.handlers.kop.schemaregistry.exceptions.InvalidVersionException;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.CompatibilityChecker;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.LookupFilter;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.Schema;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.SchemaStorage;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.SchemaStorageAccessor;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.VersionId;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.impl.SchemaStorageException;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.rest.CreateSchemaRequest;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.rest.CreateSchemaResponse;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.rest.SchemaReference;
import io.streamnative.pulsar.handlers.kop.schemaregistry.utils.SubjectUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.pulsar.common.util.FutureUtil;
@Slf4j
public class SubjectVersionsResource extends AbstractResource {
public SubjectVersionsResource(SchemaStorageAccessor schemaStorageAccessor,
String defaultNamespace) {
super(schemaStorageAccessor, defaultNamespace);
}
@Override
public void register(SchemaRegistryHandler schemaRegistryHandler) {
schemaRegistryHandler.addProcessor(new CreateNewSchema());
schemaRegistryHandler.addProcessor(new GetSchemaBySubjectAndVersion());
schemaRegistryHandler.addProcessor(new GetRawSchemaBySubjectAndVersion());
schemaRegistryHandler.addProcessor(new GetAllVersions());
schemaRegistryHandler.addProcessor(new DeleteSchemaVersion());
schemaRegistryHandler.addProcessor(new ReferencedBy());
}
@AllArgsConstructor
@Getter
public static class GetSchemaBySubjectAndVersionResponse {
private int id;
private String schema;
private String subject;
private int version;
}
// POST /subjects/(string: subject)/versions?normalize=(boolean: normalize)
public class CreateNewSchema extends HttpJsonRequestProcessor {
// Type: boolean, default false
private static final String QUERY_PARAM_NORMALIZE = "normalize";
public CreateNewSchema() {
super(CreateSchemaRequest.class, "/subjects/" + STRING_PATTERN + "/versions", POST);
}
@Override
protected CompletableFuture processRequest(
CreateSchemaRequest payload,
List patternGroups,
FullHttpRequest request,
Map> queryParams,
String currentTenant) throws Exception {
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
final boolean normalize = getBooleanQueryParam(QUERY_PARAM_NORMALIZE, "false", queryParams);
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
Schema schema = Schema.builder()
.type(payload.getSchemaType())
.id(payload.getId())
.version(payload.getVersion())
.subject(subject)
.schema(payload.getSchema())
.references(payload
.getReferences()
.stream()
.map(r -> {
String normalizedSubject = SubjectUtils.normalize(r.getSubject(), defaultNamespace);
return new SchemaReference(r.getName(), normalizedSubject, r.getVersion());
})
.collect(Collectors.toList()))
.build();
return schemaStorage.lookUpSchemaUnderSubject(subject, schema, normalize, false)
.thenCompose(existingSchema -> {
if (existingSchema != null) {
if (schema.getId() == null
|| schema.getId() < 0
|| schema.getId().equals(existingSchema.getId())
) {
return CompletableFuture.completedFuture(existingSchema.getId());
}
}
return schemaStorage.createSchemaVersion(schema, normalize).thenApply(Schema::getId);
})
.thenApply(CreateSchemaResponse::new)
.exceptionally(err -> {
while (err instanceof CompletionException || err instanceof UncheckedExecutionException) {
err = err.getCause();
}
if (err instanceof InvalidSchemaException) {
throw Errors.invalidSchemaException(err);
}
if (err instanceof CompatibilityChecker.IncompatibleSchemaChangeException) {
throw new CompletionException(
new SchemaStorageException(err.getMessage(), HttpResponseStatus.CONFLICT));
} else {
throw new CompletionException(err);
}
});
}
}
// GET /subjects/(string: subject)/versions/(versionId: version)
public class GetSchemaBySubjectAndVersion
extends HttpJsonRequestProcessor {
private static final String QUERY_PARAM_DELETED = "deleted";
public GetSchemaBySubjectAndVersion() {
super(Void.class, "/subjects/" + STRING_PATTERN + "/versions/" + VERSION_PATTERN, GET);
}
@Override
protected CompletableFuture processRequest(
Void payload, List patternGroups, FullHttpRequest request,
Map> queryParams,
String currentTenant)
throws Exception {
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
String version = getString(1, patternGroups);
VersionId versionId;
try {
versionId = new VersionId(version);
} catch (InvalidVersionException e) {
return FutureUtil.failedFuture(Errors.invalidVersionException(e.getMessage()));
}
boolean deleted = getBooleanQueryParam(QUERY_PARAM_DELETED, "false", queryParams);
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
CompletableFuture schema = schemaStorage.findSchemaBySubjectAndVersion(subject, versionId, deleted);
return schema.thenCompose(s -> {
if (s != null) {
return CompletableFuture.completedFuture(s);
}
return schemaStorage.hasSubjects(subject, deleted).thenCompose(exists -> {
if (!exists) {
return FutureUtil.failedFuture(Errors.subjectNotFoundException(subject));
}
return FutureUtil.failedFuture(Errors.versionNotFoundException(versionId.getVersionId()));
});
}).thenApply(s -> new GetSchemaBySubjectAndVersionResponse(
s.getId(), s.getSchema(), s.getSubject(), s.getVersion()));
}
}
// GET /subjects/(string: subject)/versions/(versionId: version)/schema
public class GetRawSchemaBySubjectAndVersion extends HttpJsonRequestProcessor {
public GetRawSchemaBySubjectAndVersion() {
super(Void.class, "/subjects/" + STRING_PATTERN + "/versions/" + VERSION_PATTERN + "/schema", GET);
}
@Override
protected CompletableFuture processRequest(Void payload, List patternGroups,
FullHttpRequest request,
Map> queryParams,
String currentTenant)
throws Exception {
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
String version = getString(1, patternGroups);
VersionId versionId;
try {
versionId = new VersionId(version);
} catch (InvalidVersionException e) {
return FutureUtil.failedFuture(Errors.invalidVersionException(e.getMessage()));
}
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
CompletableFuture schema = schemaStorage.findSchemaBySubjectAndVersion(subject, versionId, false);
return schema.thenApply(s -> {
if (s == null) {
return null;
}
return s.getSchema();
});
}
}
// GET /subjects/(string: subject)/versions
public class GetAllVersions extends HttpJsonRequestProcessor> {
// Type: boolean, default false
private static final String QUERY_PARAM_DELETED = "deleted";
// Type: boolean, default false
private static final String QUERY_PARAM_DELETED_ONLY = "deletedOnly";
public GetAllVersions() {
super(Void.class, "/subjects/" + STRING_PATTERN + "/versions", GET);
}
@Override
protected CompletableFuture> processRequest(Void payload, List patternGroups,
FullHttpRequest request,
Map> queryParams,
String currentTenant)
throws Exception {
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
boolean deleted = getBooleanQueryParam(QUERY_PARAM_DELETED, "false", queryParams);
boolean deletedOnly = getBooleanQueryParam(QUERY_PARAM_DELETED_ONLY, "false", queryParams);
LookupFilter filter = LookupFilter.DEFAULT;
// if both deleted && deletedOnly are true, return deleted only
if (deletedOnly) {
filter = LookupFilter.DELETED_ONLY;
} else if (deleted) {
filter = LookupFilter.INCLUDE_DELETED;
}
return schemaStorage.getSchemasForSubject(subject, filter).thenApply(schemas -> {
if (CollectionUtils.isEmpty(schemas)) {
throw Errors.subjectNotFoundException(subject);
}
List versions = new ArrayList<>();
for (Schema schema : schemas) {
versions.add(schema.getVersion());
}
return versions;
});
}
}
// DELETE /subjects/(string: subject)/versions/(string: versionId)
public class DeleteSchemaVersion extends HttpJsonRequestProcessor {
// Type: boolean, default false
private static final String QUERY_PARAM_PERMANENT_DELETE = "permanent";
public DeleteSchemaVersion() {
super(Void.class, "/subjects/" + STRING_PATTERN + "/versions/" + VERSION_PATTERN, DELETE);
}
@Override
public boolean acceptRequest(FullHttpRequest request) {
return super.acceptRequest(request);
}
@Override
protected CompletableFuture processRequest(Void payload,
List patternGroups,
FullHttpRequest request,
Map> queryParams,
String currentTenant)
throws Exception {
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
String version = getString(1, patternGroups);
VersionId versionId;
try {
versionId = new VersionId(version);
} catch (InvalidVersionException e) {
return FutureUtil.failedFuture(Errors.invalidVersionException(e.getMessage()));
}
boolean permanentDelete = getBooleanQueryParam(QUERY_PARAM_PERMANENT_DELETE, "false", queryParams);
if (log.isDebugEnabled()) {
log.debug("Deleting schema version {} from subject {}, permanentDelete {}",
versionId, subject, permanentDelete);
}
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
try {
return schemaStorage.schemaVersionExists(subject, versionId, true)
.thenCombine(schemaStorage.schemaVersionExists(subject, versionId, false),
(exists, active) -> {
if (exists && !permanentDelete && !active) {
throw Errors.schemaVersionSoftDeletedException(subject, version);
}
return null;
})
.thenCompose(__ -> schemaStorage.findSchemaBySubjectAndVersion(subject, versionId, true))
.thenCompose(schema -> {
if (schema == null) {
return schemaStorage.hasSubjects(subject, true)
.thenCompose(exists -> {
if (!exists) {
return FutureUtil.failedFuture(
Errors.subjectNotFoundException(subject));
}
return FutureUtil.failedFuture(
Errors.versionNotFoundException(versionId.getVersionId()));
});
}
return schemaStorage.deleteSchemaVersion(subject, schema, permanentDelete)
.thenApply(__ -> schema.getVersion());
});
} catch (Exception e) {
log.error("Failed to delete schema version", e);
return FutureUtil.failedFuture(new RuntimeException(e));
}
}
}
// GET /subjects/(string: subject)/versions/(string: versionId)/referencedby
public class ReferencedBy extends HttpJsonRequestProcessor> {
public ReferencedBy() {
super(Void.class, "/subjects/" + STRING_PATTERN + "/versions/" + VERSION_PATTERN
+ "/referencedby", GET);
}
@Override
protected CompletableFuture> processRequest(Void payload,
List patternGroups,
FullHttpRequest request,
Map> queryParams,
String currentTenant)
throws Exception {
final String subject = SubjectUtils.normalize(getString(0, patternGroups), defaultNamespace);
final String version = getString(1, patternGroups);
VersionId versionId;
try {
versionId = new VersionId(version);
} catch (InvalidVersionException e) {
return FutureUtil.failedFuture(Errors.invalidVersionException(e.getMessage()));
}
SchemaStorage schemaStorage = getSchemaStorage(currentTenant);
return schemaStorage.findSchemaBySubjectAndVersion(subject, versionId, true)
.thenCompose(schema -> {
if (schema == null) {
return schemaStorage.hasSubjects(subject, true)
.thenCompose(exists -> {
if (!exists) {
return FutureUtil.failedFuture(
Errors.subjectNotFoundException(subject));
}
return FutureUtil.failedFuture(
Errors.versionNotFoundException(versionId.getVersionId()));
});
}
return schemaStorage.getReferencedBy(subject, versionId);
});
}
}
}