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

io.streamnative.pulsar.handlers.kop.schemaregistry.HttpJsonRequestProcessor Maven / Gradle / Ivy

There is a newer version: 4.0.0.4
Show 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;

import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;

import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.streamnative.pulsar.handlers.kop.schemaregistry.exceptions.RestException;
import io.streamnative.pulsar.handlers.kop.schemaregistry.model.impl.SchemaStorageException;
import java.io.DataInput;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.common.util.FutureUtil;

@Slf4j
public abstract class HttpJsonRequestProcessor extends HttpRequestProcessor {

    protected static final String RESPONSE_CONTENT_TYPE = "application/vnd.schemaregistry.v1+json";

    private final Class requestModel;
    private final Pattern pattern;
    private final String method;

    public HttpJsonRequestProcessor(Class requestModel, String uriPattern, String method) {
        this.requestModel = requestModel;
        this.pattern = Pattern.compile(uriPattern);
        this.method = method;
    }

    protected static int getInt(int position, List queryStringGroups) {
        return Integer.parseInt(queryStringGroups.get(position));
    }

    protected static String getString(int position, List queryStringGroups) {
        return queryStringGroups.get(position);
    }

    @Override
    public boolean acceptRequest(FullHttpRequest request) {
        if (!request.method().name().equals(method)) {
            return false;
        }
        return detectGroups(request) != null;
    }

    @Override
    public CompletableFuture processRequest(FullHttpRequest request, String currentTenant) {
        List groups = detectGroups(request);
        // Decode the URL here, because we don't want to use the decoded URL to match the pattern.
        // For example if decode the URL first:
        // 1. DELETE /subjects/(string: subject)/versions/(string: versionId)
        // 2. DELETE /subjects/(string: subject)
        //
        // Client send: /subjects/public%2Fversions%2Flatest -> /subjects/public/versions/latest
        //
        // It will be matched to the 1st pattern, but the 2nd pattern is expected.
        try {
            groups.replaceAll(s -> URLDecoder.decode(s, StandardCharsets.UTF_8));
        } catch (IllegalArgumentException e) {
            return CompletableFuture.completedFuture(buildErrorResponse(HttpResponseStatus.BAD_REQUEST,
                    "Invalid URL: " + e.getMessage()));
        }
        try (ByteBufInputStream inputStream = new ByteBufInputStream(request.content())) {
            K decodeRequest;
            if (requestModel == Void.class) {
                decodeRequest = null;
            } else {
                decodeRequest = MAPPER.readValue((DataInput) inputStream, requestModel);
            }
            CompletableFuture result;
            try {
                QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
                result = processRequest(decodeRequest, groups, request, decoder.parameters(), currentTenant);
            } catch (Exception err) {
                result = FutureUtil.failedFuture(err);
            }
            return result.thenApply(resp -> {
                if (resp == null) {
                    return buildErrorResponse(NOT_FOUND,
                            request.method() + " " + request.uri() + " Not found");
                }
                if (resp.getClass() == String.class) {
                    return buildStringResponse(((String) resp), RESPONSE_CONTENT_TYPE);
                } else {
                    return buildJsonResponse(resp, RESPONSE_CONTENT_TYPE);
                }
            }).exceptionally(err -> {
                Throwable throwable = err;
                while (throwable.getCause() != null) {
                    throwable = throwable.getCause();
                }
                if (throwable instanceof SchemaStorageException e) {
                    return buildErrorResponse(e.getHttpStatusCode(), e.getMessage());
                } else if (throwable instanceof RestException e) {
                    return buildErrorResponse(new HttpResponseStatus(e.getErrorCode(), e.getMessage()), e.getMessage());
                } else {
                    log.error("Error while processing request", err);
                    return buildJsonErrorResponse(err);
                }
            });
        } catch (IOException err) {
            log.error("Cannot decode request", err);
            return CompletableFuture.completedFuture(buildErrorResponse(HttpResponseStatus.BAD_REQUEST,
                    "Cannot decode request: " + err.getMessage()));
        }
    }

    private List detectGroups(FullHttpRequest request) {
        String uri = request.uri();
        // TODO: here we are discarding the query string part
        // in the future we will probably have to implement
        // query string parameters
        int questionMark = uri.lastIndexOf('?');
        if (questionMark > 0) {
            uri = uri.substring(0, questionMark);
        }
        if (uri.endsWith("/")) {
            // compatibility with Confluent Schema registry
            uri = uri.substring(0, uri.length() - 1);
        }
        Matcher matcher = pattern.matcher(uri);
        if (!matcher.matches()) {
            return null;
        }
        List groups = new ArrayList<>(matcher.groupCount());
        for (int i = 0; i < matcher.groupCount(); i++) {
            groups.add(matcher.group(i + 1));
        }
        return groups;
    }

    protected abstract CompletableFuture processRequest(K payload, List patternGroups,
                                                           FullHttpRequest request,
                                                           Map> queryParams,
                                                           String currentTenant) throws Exception;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy