/** * Copyright 2015 Confluent Inc. * * 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.confluent.kafka.schemaregistry.client.rest; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.confluent.kafka.schemaregistry.client.rest.entities.Config; import io.confluent.kafka.schemaregistry.client.rest.entities.ErrorMessage; import io.confluent.kafka.schemaregistry.client.rest.entities.Schema; import io.confluent.kafka.schemaregistry.client.rest.entities.SchemaString; import io.confluent.kafka.schemaregistry.client.rest.entities.requests.CompatibilityCheckResponse; import io.confluent.kafka.schemaregistry.client.rest.entities.requests.ConfigUpdateRequest; import io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaRequest; import io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaResponse; import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; import io.confluent.kafka.schemaregistry.client.rest.utils.UrlList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Rest access layer for sending requests to the schema registry. */ public class RestService { private static final Logger log = LoggerFactory.getLogger(RestService.class); private static final TypeReference<RegisterSchemaResponse> REGISTER_RESPONSE_TYPE = new TypeReference<RegisterSchemaResponse>() { }; private static final TypeReference<Config> GET_CONFIG_RESPONSE_TYPE = new TypeReference<Config>() { }; private static final TypeReference<SchemaString> GET_SCHEMA_BY_ID_RESPONSE_TYPE = new TypeReference<SchemaString>() { }; private static final TypeReference<Schema> GET_SCHEMA_BY_VERSION_RESPONSE_TYPE = new TypeReference<Schema>() { }; private static final TypeReference<List<Integer>> ALL_VERSIONS_RESPONSE_TYPE = new TypeReference<List<Integer>>() { }; private static final TypeReference<List<String>> ALL_TOPICS_RESPONSE_TYPE = new TypeReference<List<String>>() { }; private static final TypeReference<CompatibilityCheckResponse> COMPATIBILITY_CHECK_RESPONSE_TYPE_REFERENCE = new TypeReference<CompatibilityCheckResponse>() { }; private static final TypeReference<Schema> SUBJECT_SCHEMA_VERSION_RESPONSE_TYPE_REFERENCE = new TypeReference<Schema>() { }; private static final TypeReference<ConfigUpdateRequest> UPDATE_CONFIG_RESPONSE_TYPE_REFERENCE = new TypeReference<ConfigUpdateRequest>() { }; private static final TypeReference<Integer> DELETE_SUBJECT_VERSION_RESPONSE_TYPE = new TypeReference<Integer>() { }; private static final TypeReference<? extends List<Integer>> DELETE_SUBJECT_RESPONSE_TYPE = new TypeReference<List<Integer>>() { }; private static final int JSON_PARSE_ERROR_CODE = 50005; private static ObjectMapper jsonDeserializer = new ObjectMapper(); public static final Map<String, String> DEFAULT_REQUEST_PROPERTIES; static { DEFAULT_REQUEST_PROPERTIES = new HashMap<String, String>(); DEFAULT_REQUEST_PROPERTIES.put("Content-Type", Versions.SCHEMA_REGISTRY_V1_JSON_WEIGHTED); } private UrlList baseUrls; public RestService(UrlList baseUrls) { this.baseUrls = baseUrls; } public RestService(List<String> baseUrls) { this(new UrlList(baseUrls)); } public RestService(String baseUrlConfig) { this(parseBaseUrl(baseUrlConfig)); } /** * @param requestUrl HTTP connection will be established with this url. * @param method HTTP method ("GET", "POST", "PUT", etc.) * @param requestBodyData Bytes to be sent in the request body. * @param requestProperties HTTP header properties. * @param responseFormat Expected format of the response to the HTTP request. * @param <T> The type of the deserialized response to the HTTP request. * @return The deserialized response to the HTTP request, or null if no data is expected. */ private <T> T sendHttpRequest(String requestUrl, String method, byte[] requestBodyData, Map<String, String> requestProperties, TypeReference<T> responseFormat) throws IOException, RestClientException { log.debug(String.format("Sending %s with input %s to %s", method, requestBodyData == null ? "null" : new String(requestBodyData), requestUrl)); HttpURLConnection connection = null; try { URL url = new URL(requestUrl); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(method); // connection.getResponseCode() implicitly calls getInputStream, so always set to true. // On the other hand, leaving this out breaks nothing. connection.setDoInput(true); for (Map.Entry<String, String> entry : requestProperties.entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); } connection.setUseCaches(false); if (requestBodyData != null) { connection.setDoOutput(true); OutputStream os = null; try { os = connection.getOutputStream(); os.write(requestBodyData); os.flush(); } catch (IOException e) { log.error("Failed to send HTTP request to endpoint: " + url, e); throw e; } finally { if (os != null) { os.close(); } } } int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { InputStream is = connection.getInputStream(); T result = jsonDeserializer.readValue(is, responseFormat); is.close(); return result; } else if (responseCode == HttpURLConnection.HTTP_NO_CONTENT) { return null; } else { InputStream es = connection.getErrorStream(); ErrorMessage errorMessage; try { errorMessage = jsonDeserializer.readValue(es, ErrorMessage.class); } catch (JsonProcessingException e) { errorMessage = new ErrorMessage(JSON_PARSE_ERROR_CODE, e.getMessage()); } es.close(); throw new RestClientException(errorMessage.getMessage(), responseCode, errorMessage.getErrorCode()); } } finally { if (connection != null) { connection.disconnect(); } } } private <T> T httpRequest(String path, String method, byte[] requestBodyData, Map<String, String> requestProperties, TypeReference<T> responseFormat) throws IOException, RestClientException { for (int i = 0, n = baseUrls.size(); i < n; i++) { String baseUrl = baseUrls.current(); String requestUrl = buildRequestUrl(baseUrl, path); try { return sendHttpRequest(requestUrl, method, requestBodyData, requestProperties, responseFormat); } catch (IOException e) { baseUrls.fail(baseUrl); if (i == n - 1) { throw e; // Raise the exception since we have no more urls to try } } } throw new IOException("Internal HTTP retry error"); // Can't get here } // Visible for testing static String buildRequestUrl(String baseUrl, String path) { // Join base URL and path, collapsing any duplicate forward slash delimiters return baseUrl.replaceFirst("/$", "") + "/" + path.replaceFirst("^/", ""); } public Schema lookUpSubjectVersion(String schemaString, String subject) throws IOException, RestClientException { RegisterSchemaRequest request = new RegisterSchemaRequest(); request.setSchema(schemaString); return lookUpSubjectVersion(request, subject); } public Schema lookUpSubjectVersion(RegisterSchemaRequest registerSchemaRequest, String subject) throws IOException, RestClientException { return lookUpSubjectVersion(DEFAULT_REQUEST_PROPERTIES, registerSchemaRequest, subject, false); } public Schema lookUpSubjectVersion(Map<String, String> requestProperties, RegisterSchemaRequest registerSchemaRequest, String subject) throws IOException, RestClientException { String path = String.format("/subjects/%s", subject); if (requestProperties.isEmpty()) { requestProperties = DEFAULT_REQUEST_PROPERTIES; } Schema schema = httpRequest(path, "POST", registerSchemaRequest.toJson().getBytes(), requestProperties, SUBJECT_SCHEMA_VERSION_RESPONSE_TYPE_REFERENCE); return schema; } public Schema lookUpSubjectVersion(String schemaString, String subject, boolean lookupDeletedSchema) throws IOException, RestClientException { RegisterSchemaRequest request = new RegisterSchemaRequest(); request.setSchema(schemaString); return lookUpSubjectVersion(DEFAULT_REQUEST_PROPERTIES, request, subject, lookupDeletedSchema); } public Schema lookUpSubjectVersion(Map<String, String> requestProperties, RegisterSchemaRequest registerSchemaRequest, String subject, boolean lookupDeletedSchema) throws IOException, RestClientException { String path = String.format("/subjects/%s?deleted=%s", subject, lookupDeletedSchema); Schema schema = httpRequest(path, "POST", registerSchemaRequest.toJson().getBytes(), requestProperties, SUBJECT_SCHEMA_VERSION_RESPONSE_TYPE_REFERENCE); return schema; } public int registerSchema(String schemaString, String subject) throws IOException, RestClientException { RegisterSchemaRequest request = new RegisterSchemaRequest(); request.setSchema(schemaString); return registerSchema(request, subject); } public int registerSchema(RegisterSchemaRequest registerSchemaRequest, String subject) throws IOException, RestClientException { return registerSchema(DEFAULT_REQUEST_PROPERTIES, registerSchemaRequest, subject); } public int registerSchema(Map<String, String> requestProperties, RegisterSchemaRequest registerSchemaRequest, String subject) throws IOException, RestClientException { String path = String.format("/subjects/%s/versions", subject); RegisterSchemaResponse response = httpRequest(path, "POST", registerSchemaRequest.toJson().getBytes(), requestProperties, REGISTER_RESPONSE_TYPE); return response.getId(); } public boolean testCompatibility(String schemaString, String subject, String version) throws IOException, RestClientException { RegisterSchemaRequest request = new RegisterSchemaRequest(); request.setSchema(schemaString); return testCompatibility(request, subject, version); } public boolean testCompatibility(RegisterSchemaRequest registerSchemaRequest, String subject, String version) throws IOException, RestClientException { return testCompatibility(DEFAULT_REQUEST_PROPERTIES, registerSchemaRequest, subject, version); } public boolean testCompatibility(Map<String, String> requestProperties, RegisterSchemaRequest registerSchemaRequest, String subject, String version) throws IOException, RestClientException { String path = String.format("/compatibility/subjects/%s/versions/%s", subject, version); CompatibilityCheckResponse response = httpRequest(path, "POST", registerSchemaRequest.toJson().getBytes(), requestProperties, COMPATIBILITY_CHECK_RESPONSE_TYPE_REFERENCE); return response.getIsCompatible(); } public ConfigUpdateRequest updateCompatibility(String compatibility, String subject) throws IOException, RestClientException { ConfigUpdateRequest request = new ConfigUpdateRequest(); request.setCompatibilityLevel(compatibility); return updateConfig(request, subject); } public ConfigUpdateRequest updateConfig(ConfigUpdateRequest configUpdateRequest, String subject) throws IOException, RestClientException { return updateConfig(DEFAULT_REQUEST_PROPERTIES, configUpdateRequest, subject); } /** * On success, this api simply echoes the request in the response. */ public ConfigUpdateRequest updateConfig(Map<String, String> requestProperties, ConfigUpdateRequest configUpdateRequest, String subject) throws IOException, RestClientException { String path = subject != null ? String.format("/config/%s", subject) : "/config"; ConfigUpdateRequest response = httpRequest(path, "PUT", configUpdateRequest.toJson().getBytes(), requestProperties, UPDATE_CONFIG_RESPONSE_TYPE_REFERENCE); return response; } public Config getConfig(String subject) throws IOException, RestClientException { return getConfig(DEFAULT_REQUEST_PROPERTIES, subject); } public Config getConfig(Map<String, String> requestProperties, String subject) throws IOException, RestClientException { String path = subject != null ? String.format("/config/%s", subject) : "/config"; Config config = httpRequest(path, "GET", null, requestProperties, GET_CONFIG_RESPONSE_TYPE); return config; } public SchemaString getId(int id) throws IOException, RestClientException { return getId(DEFAULT_REQUEST_PROPERTIES, id); } public SchemaString getId(Map<String, String> requestProperties, int id) throws IOException, RestClientException { String path = String.format("/schemas/ids/%d", id); SchemaString response = httpRequest(path, "GET", null, requestProperties, GET_SCHEMA_BY_ID_RESPONSE_TYPE); return response; } public Schema getVersion(String subject, int version) throws IOException, RestClientException { return getVersion(DEFAULT_REQUEST_PROPERTIES, subject, version); } public Schema getVersion(Map<String, String> requestProperties, String subject, int version) throws IOException, RestClientException { String path = String.format("/subjects/%s/versions/%d", subject, version); Schema response = httpRequest(path, "GET", null, requestProperties, GET_SCHEMA_BY_VERSION_RESPONSE_TYPE); return response; } public Schema getLatestVersion(String subject) throws IOException, RestClientException { return getLatestVersion(DEFAULT_REQUEST_PROPERTIES, subject); } public Schema getLatestVersion(Map<String, String> requestProperties, String subject) throws IOException, RestClientException { String path = String.format("/subjects/%s/versions/latest", subject); Schema response = httpRequest(path, "GET", null, requestProperties, GET_SCHEMA_BY_VERSION_RESPONSE_TYPE); return response; } public List<Integer> getAllVersions(String subject) throws IOException, RestClientException { return getAllVersions(DEFAULT_REQUEST_PROPERTIES, subject); } public List<Integer> getAllVersions(Map<String, String> requestProperties, String subject) throws IOException, RestClientException { String path = String.format("/subjects/%s/versions", subject); List<Integer> response = httpRequest(path, "GET", null, requestProperties, ALL_VERSIONS_RESPONSE_TYPE); return response; } public List<String> getAllSubjects() throws IOException, RestClientException { return getAllSubjects(DEFAULT_REQUEST_PROPERTIES); } public List<String> getAllSubjects(Map<String, String> requestProperties) throws IOException, RestClientException { List<String> response = httpRequest("/subjects", "GET", null, requestProperties, ALL_TOPICS_RESPONSE_TYPE); return response; } public Integer deleteSchemaVersion(String subject, String version) throws IOException, RestClientException { String path = String.format("/subjects/%s/versions/%s", subject, version); Integer response = httpRequest(path, "DELETE", null, DEFAULT_REQUEST_PROPERTIES, DELETE_SUBJECT_VERSION_RESPONSE_TYPE); return response; } public List<Integer> deleteSubject(String subject) throws IOException, RestClientException { String path = String.format("/subjects/%s", subject); List<Integer> response = httpRequest(path, "DELETE", null, DEFAULT_REQUEST_PROPERTIES, DELETE_SUBJECT_RESPONSE_TYPE); return response; } private static List<String> parseBaseUrl(String baseUrl) { List<String> baseUrls = Arrays.asList(baseUrl.split("\\s*,\\s*")); if (baseUrls.isEmpty()) { throw new IllegalArgumentException("Missing required schema registry url list"); } return baseUrls; } public UrlList getBaseUrls() { return baseUrls; } }