io.streamnative.pulsar.handlers.kop.schemaregistry.HttpJsonRequestProcessor 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
/**
* 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;
}