/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.streamsets.lib.security.http;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A REST Client with minimal dependencies (JDK & JACKSON -for JSON- )
*/
public class RestClient {
@VisibleForTesting
static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
static final String CONTENT_TYPE = "content-type";
static final String ACCEPT = "accept";
static final String APPLICATION_JSON = "application/json";
public static class Builder {
@VisibleForTesting
String name;
final String baseUrl;
String path;
boolean csrf;
boolean json;
String componentId;
String appAuthToken;
Map<String, List<String>> headers;
Map<String, List<String>> queryParams;
int timeoutMillis;
ObjectMapper jsonMapper;
private Builder(String baseUrl) {
name = "RestClient";
this.baseUrl = (baseUrl.endsWith("/")) ? baseUrl : baseUrl + "/";
path = null;
csrf = true;
json = true;
headers = new HashMap<>();
queryParams = new HashMap<>();
timeoutMillis = 30 * 1000;
jsonMapper = OBJECT_MAPPER;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder path(String path) {
this.path = path;
return this;
}
public Builder csrf(boolean csrf) {
this.csrf = csrf;
return this;
}
public Builder jsonMapper(ObjectMapper mapper) {
this.jsonMapper = mapper;
return this;
}
public Builder componentId(String componentId) {
this.componentId = componentId;
return this;
}
public Builder appAuthToken(String appAuthToken) {
this.appAuthToken = appAuthToken;
return this;
}
public Builder json(boolean json) {
this.json = json;
return this;
}
Builder map(Map<String, List<String>> map, String name, String value) {
if (value == null) {
map.remove(name);
} else {
List<String> values = map.get(name);
if (values == null) {
values = new ArrayList<>();
map.put(name, values);
}
values.add(value);
}
return this;
}
public Builder header(String name, String value) {
return map(headers, name.toLowerCase(), value);
}
public Builder queryParam(String name, String value) {
return map(queryParams, name, value);
}
public Builder timeout(int timeoutMillis) {
this.timeoutMillis = timeoutMillis;
return this;
}
public RestClient build() throws IOException {
return build(path, queryParams);
}
public RestClient build(String path) throws IOException {
return build(path, queryParams);
}
public RestClient build(Map<String, List<String>> queryParams) throws IOException {
return build(path, queryParams);
}
public RestClient build(String path, Map<String, List<String>> queryParams) throws IOException {
return new RestClient(name,
baseUrl,
path,
componentId,
appAuthToken,
csrf,
json,
headers,
queryParams,
timeoutMillis,
jsonMapper
);
}
}
public static class Response {
@VisibleForTesting
final ObjectMapper jsonMapper;
final HttpURLConnection conn;
Response(ObjectMapper jsonMapper, HttpURLConnection conn) {
this.jsonMapper = jsonMapper;
this.conn = conn;
}
public HttpURLConnection getConnection() {
return conn;
}
public int getStatus() throws IOException {
return conn.getResponseCode();
}
public String getContentType() {
return conn.getContentType();
}
public boolean isJson() {
String contentType = conn.getContentType();
return contentType != null && contentType.toLowerCase().trim().startsWith(APPLICATION_JSON);
}
public String getHeader(String headerName) {
return conn.getHeaderField(headerName);
}
public Map<String, List<String>> getHeaders() {
return conn.getHeaderFields();
}
public InputStream getInputStream() throws IOException {
return conn.getInputStream();
}
public <T> T getData(TypeReference<T> typeReference) throws IOException {
try(InputStream inputStream = getInputStream()) {
return jsonMapper.readValue(inputStream, typeReference);
}
}
public <T> T getData(Class<T> klass) throws IOException {
if (isJson()) {
try(InputStream inputStream = getInputStream()) {
return jsonMapper.readValue(inputStream, klass);
}
} else {
throw new IllegalStateException("Response is not application/json, is " + conn.getContentType());
}
}
@SuppressWarnings("unchecked")
public Map getError() throws IOException {
Map map;
if (isJson()) {
try(InputStream inputStream = conn.getErrorStream()) {
map = jsonMapper.readValue(inputStream, Map.class);
}
} else {
map = new HashMap();
map.put("message", conn.getResponseMessage());
}
return map;
}
}
public static Builder builder(String baseUrl) {
return new Builder(baseUrl);
}
@VisibleForTesting
final String baseUrl;
final Map<String, List<String>> headers;
final Map<String, List<String>> queryParams;
final String path;
final boolean json;
final int timeoutMillis;
final ObjectMapper jsonMapper;
HttpURLConnection conn;
Map<String, List<String>> deepCopy(Map<String, List<String>> map) {
Map<String, List<String>> copy = new HashMap<>();
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return copy;
}
private RestClient(
String name,
String baseUrl,
String path,
String componentId,
String appAuthToken,
boolean csrf,
boolean json,
Map<String, List<String>> headers,
Map<String, List<String>> queryParams,
int timeoutMillis,
ObjectMapper jsonMapper
) throws IOException {
this.baseUrl = baseUrl;
this.path = path;
this.headers = deepCopy(headers);
if (csrf && !this.headers.containsKey(SSOConstants.X_REST_CALL.toLowerCase())) {
this.headers.put(SSOConstants.X_REST_CALL.toLowerCase(), Collections.singletonList(name));
}
this.json = json;
if (json && !this.headers.containsKey(CONTENT_TYPE)) {
this.headers.put(CONTENT_TYPE.toLowerCase(), Collections.singletonList(APPLICATION_JSON));
this.headers.put(ACCEPT, Collections.singletonList(APPLICATION_JSON));
}
if (componentId != null && appAuthToken != null) {
this.headers.put(SSOConstants.X_APP_COMPONENT_ID.toLowerCase(), Collections.singletonList(componentId));
this.headers.put(SSOConstants.X_APP_AUTH_TOKEN.toLowerCase(), Collections.singletonList(appAuthToken));
}
this.queryParams = deepCopy(queryParams);
this.timeoutMillis = timeoutMillis;
this.jsonMapper = jsonMapper;
reset();
}
HttpURLConnection createConnection() throws IOException {
URL url = new URL(baseUrl);
if (path != null) {
url = new URL(url, path);
}
if (!queryParams.isEmpty()) {
StringBuilder sb = new StringBuilder();
String separator = "?";
for (Map.Entry<String, List<String>> param : queryParams.entrySet()) {
for (String value : param.getValue()) {
sb
.append(separator)
.append(URLEncoder.encode(param.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(value, "UTF-8"));
separator = "&";
}
}
url = new URL(sb.insert(0, url.toExternalForm()).toString());
}
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
for (String value : header.getValue()) {
conn.addRequestProperty(header.getKey(), value);
}
}
conn.setConnectTimeout(timeoutMillis);
conn.setReadTimeout(timeoutMillis);
conn.setDefaultUseCaches(false);
return conn;
}
public void reset() throws IOException {
conn = createConnection();
}
public HttpURLConnection getConnection() {
return conn;
}
Response doHttp(HttpURLConnection conn, String method, Object upload) throws IOException {
conn.setRequestMethod(method);
if (upload != null) {
if (json) {
try(OutputStream outputStream = conn.getOutputStream()) {
jsonMapper.writeValue(outputStream, upload);
}
} else {
throw new IllegalStateException("Content type is not JSON, cannot upload data bean");
}
}
return new Response(jsonMapper, conn);
}
public Response get() throws IOException {
getConnection().setDoOutput(true);
return doHttp(getConnection(), "GET", null);
}
public <T> Response post(T data) throws IOException {
getConnection().setDoOutput(true);
getConnection().setDoInput(true);
return doHttp(getConnection(), "POST", data);
}
public <T> Response put(T data) throws IOException {
getConnection().setDoOutput(true);
getConnection().setDoInput(true);
return doHttp(getConnection(), "PUT", data);
}
public Response delete() throws IOException {
getConnection().setDoOutput(true);
return doHttp(getConnection(), "DELETE", null);
}
}