All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.streamnative.pulsar.handlers.kop.schemaregistry.resources.SubjectVersionsResource Maven / Gradle / Ivy

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);
                });
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy