
com.cybozu.kintone.client.connection.Connection Maven / Gradle / Ivy
/**
* MIT License
*
* Copyright (c) 2018 Cybozu
* https://github.com/kintone/kintone-java-sdk/blob/master/LICENSE
*/
package com.cybozu.kintone.client.connection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Properties;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import com.cybozu.kintone.client.authentication.Auth;
import com.cybozu.kintone.client.exception.ErrorResponse;
import com.cybozu.kintone.client.exception.KintoneAPIException;
import com.cybozu.kintone.client.model.bulkrequest.BulkRequestItem;
import com.cybozu.kintone.client.model.http.HTTPHeader;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* Connection object to working with rest api.
* This class provide core request function for all modules management classes.
*/
public class Connection {
/*
* HTTP header content-type for getting json data from rest api.
*/
private static final String JSON_CONTENT = "application/json";
/*
* Json parser for parsing the request's result.
*/
private static final JsonParser jsonParser = new JsonParser();
/*
* Json string to object converter.
*/
private static final Gson gson = new Gson();
/*
* User agent http header.
*/
public static String userAgent = ConnectionConstants.USER_AGENT_VALUE;
/*
* Object contains user's credential.
*/
private Auth auth;
/*
* Kintone domain url.
*/
private String domain;
/*
* Guest space number in kintone domain.
* User describe it when connect data in guest space.
*/
private int guestSpaceId = -1;
/*
* Contains addition headers user set.
*/
private ArrayList headers = new ArrayList();
/*
* Contains information for bypass proxy.
*/
private String proxyHost = null;
private Integer proxyPort = null;
private Boolean isHttpsProxy = false;
private String proxyUser = null;
private String proxyPass = null;
private Authenticator proxyAuthenticator = null;
/**
* Constructor for init a connection object to connect to guest space.
*
* @param domain Kintone domain url
* @param auth Credential information
* @param guestSpaceId Guest space number in kintone domain.
*/
public Connection(String domain, Auth auth, int guestSpaceId) {
this.domain = domain;
this.auth = auth;
this.guestSpaceId = guestSpaceId;
userAgent += "/" + getProperties().getProperty("version");
}
/**
* Constructor for init a connection object to connect to normal space.
*
* @param domain Kintone domain url
* @param auth Credential information
*/
public Connection(String domain, Auth auth) {
this(domain, auth, -1);
}
/**
* Rest http request.
* This method is low level api, use the correspondence methods in module package instead.
*
* @param method rest http method. Only accept "GET", "POST", "PUT", "DELETE" value.
* @param apiName api name
* @param body body of http request. In case "GET" method, the parameters.
* @return json object
* @throws KintoneAPIException the KintoneAPIException to throw
*/
public JsonElement request(String method, String apiName, String body) throws KintoneAPIException {
HttpsURLConnection connection = null;
String response = null;
try {
boolean isGet = false;
if (ConnectionConstants.GET_REQUEST.equals(method)) {
isGet = true;
}
URL url = null;
url = this.getURL(apiName, null);
connection = openApiConnection(url);
this.setHTTPHeaders(connection);
connection.setRequestMethod(method);
connection.setDoOutput(true);
connection.setRequestProperty(ConnectionConstants.CONTENT_TYPE_HEADER, JSON_CONTENT);
if (isGet) {
connection.setRequestProperty(ConnectionConstants.METHOD_OVERRIDE_HEADER, ConnectionConstants.GET_REQUEST);
}
connection.connect();
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
try(
OutputStream outputStream = connection.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
) {
writer.write(body);
writer.close();
checkStatus(connection, body);
try(
InputStream inputStream = connection.getInputStream();
) {
response = this.readStream(inputStream);
}
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
return jsonParser.parse(response);
}
/**
* Rest http request.
* This method is execute file download
*
* @param body the body of the downloadfile
* @return inputstream
* @throws KintoneAPIException the KintoneAPIException to throw
*/
public InputStream downloadFile(String body) throws KintoneAPIException {
HttpsURLConnection connection = null;
URL url;
try {
url = this.getURL(ConnectionConstants.FILE, null);
connection = openApiConnection(url);
this.setHTTPHeaders(connection);
connection.setRequestMethod(ConnectionConstants.GET_REQUEST);
connection.setDoOutput(true);
connection.setRequestProperty(ConnectionConstants.CONTENT_TYPE_HEADER, JSON_CONTENT);
connection.setRequestProperty(ConnectionConstants.METHOD_OVERRIDE_HEADER, ConnectionConstants.GET_REQUEST);
connection.connect();
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
try (
OutputStream outputStream = connection.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
) {
writer.write(body);
writer.close();
checkStatus(connection, body);
return connection.getInputStream();
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
}
/**
* Rest http request.
* This method is execute file upload.
*
* @param fileName upload file name
* @param fis file inputstream
* @return json object
* @throws KintoneAPIException the KintoneAPIException to throw
*/
public JsonElement uploadFile(String fileName, InputStream fis) throws KintoneAPIException {
HttpsURLConnection connection;
String response = null;
URL url = null;
try {
url = this.getURL(ConnectionConstants.FILE, null);
connection = openApiConnection(url);
this.setHTTPHeaders(connection);
connection.setRequestMethod(ConnectionConstants.POST_REQUEST);
connection.setDoOutput(true);
connection.setRequestProperty(ConnectionConstants.CONTENT_TYPE_HEADER,
"multipart/form-data; boundary=" + ConnectionConstants.BOUNDARY);
connection.connect();
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
try (
OutputStream outputStream = connection.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
) {
writer.write("--" + ConnectionConstants.BOUNDARY + "\r\n");
writer.write("Content-Disposition: form-data; name=\"file\"; filename=\""
+ fileName + "\"\r\n");
writer.write(ConnectionConstants.CONTENT_TYPE_HEADER + ": " + ConnectionConstants.DEFAULT_CONTENT_TYPE
+ "\r\n\r\n");
writer.flush();
byte[] buffer = new byte[8192];
int n = 0;
while (-1 != (n = fis.read(buffer))) {
outputStream.write(buffer, 0, n);
}
outputStream.flush();
writer.write("\r\n--" + ConnectionConstants.BOUNDARY + "--\r\n");
outputStream.flush();
writer.close();
fis.close();
checkStatus(connection, null);
try (InputStream inputStream = connection.getInputStream();) {
response = readStream(inputStream);
return jsonParser.parse(response);
}
} catch(KintoneAPIException kintoneError) {
throw kintoneError;
} catch (Exception e) {
throw new KintoneAPIException(e.getMessage(), e);
}
}
/**
* Get url string from domain name, api name and parameters.
*
* @param apiName
* @param parameters
* @return url
* @throws MalformedURLException
* @throws KintoneAPIException
*/
private URL getURL(String apiName, String parameters) throws MalformedURLException, KintoneAPIException {
if (this.domain == null || this.domain.isEmpty()) {
throw new NullPointerException("domain is empty");
}
if (apiName == null || apiName.isEmpty()) {
throw new NullPointerException("api is empty");
}
StringBuilder sb = new StringBuilder();
if (!this.domain.contains(ConnectionConstants.HTTPS_PREFIX)) {
sb.append(ConnectionConstants.HTTPS_PREFIX);
}
if (this.domain.contains(ConnectionConstants.SECURE_ACCESS_SYMBOL) && this.auth.getClientCert() == null) {
throw new KintoneAPIException("client-cert is not set");
}
sb.append(this.domain);
String urlString = ConnectionConstants.BASE_URL;
if (this.guestSpaceId >= 0) {
urlString = ConnectionConstants.BASE_GUEST_URL.replaceAll("\\{GUEST_SPACE_ID\\}", this.guestSpaceId + "");
}
urlString = urlString.replaceAll("\\{API_NAME\\}", apiName);
sb.append(urlString);
if (parameters != null) {
sb.append(parameters);
}
return new URL(sb.toString().replaceAll("\\s", "%20"));
}
/**
* Set HTTP headers for connections.
*
* @param connection
*/
private void setHTTPHeaders(HttpURLConnection connection) {
for (HTTPHeader header : this.auth.createHeaderCredentials()) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
connection.setRequestProperty(ConnectionConstants.USER_AGENT_KEY, userAgent);
for (HTTPHeader header : this.headers) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
}
/**
* Set addition header when connect.
*
* @param key the key to set
* @param value the value to set
* @return connection
* Connection object.
*/
public Connection setHeader(String key, String value) {
this.headers.add(new HTTPHeader(key, value));
return this;
}
/**
* Set authentication for connection
*
* @param auth the auth to set
* @return connection
* Connection object.
*/
public Connection setAuth(Auth auth) {
this.auth = auth;
return this;
}
/**
* Parse input stream to string.
*
* @param is InputStream
* @return result
* String data
*/
private String readStream(InputStream is) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
char[] buffer = new char[1024];
int line = -1;
while ((line = reader.read(buffer)) >= 0) {
sb.append(buffer, 0, line);
}
} catch (IOException e) {
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
return sb.toString();
}
/**
* Get kintone domain url of connection.
*
* @return kintone domain
*/
public String getDomain() {
return this.domain;
}
public int getGuestSpaceId() {
return this.guestSpaceId;
}
public Auth getAuth() {
return this.auth;
}
/**
* Get pom.properties
*
* @return pom properties
*/
private Properties getProperties() {
Properties properties = new Properties();
InputStream inStream = null;
try {
inStream = this.getClass().getResourceAsStream("/pom.properties");
properties.load(inStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return properties;
}
/**
* Checks the status code of the response.
*
* @param conn a connection object
* @param body
*/
private void checkStatus(HttpURLConnection conn, String body) throws IOException, KintoneAPIException {
int statusCode = conn.getResponseCode();
if (statusCode == 404) {
ErrorResponse response = getErrorResponse(conn);
if (response == null) {
throw new KintoneAPIException("not found");
} else {
throw new KintoneAPIException(statusCode, response);
}
}
if (statusCode == 401) {
throw new KintoneAPIException("401 Unauthorized");
}
if (statusCode != 200) {
if (conn.getURL().getFile().toString().equals(this.getPathURI(ConnectionConstants.BULK_REQUEST))) {
ArrayList responses = getErrorResponses(conn);
if (responses == null) {
throw new KintoneAPIException("http status error(" + statusCode + ")");
} else {
JsonObject jobject = new Gson().fromJson(body, JsonObject.class);
JsonArray jarray = jobject.getAsJsonArray("requests");
Gson gson = new Gson();
Type listType = new TypeToken>() {
}.getType();
ArrayList requestlist = gson.fromJson(jarray, listType);
throw new KintoneAPIException(statusCode, requestlist, responses);
}
} else {
ErrorResponse response = getErrorResponse(conn);
if (response == null) {
throw new KintoneAPIException("http status error(" + statusCode + ")");
} else {
throw new KintoneAPIException(statusCode, response);
}
}
}
}
/**
* Creates an error response object.
*
* @param conn
* @return ErrorResponse object. return null if any error occurred
*/
private ErrorResponse getErrorResponse(HttpURLConnection conn) {
InputStream err = conn.getErrorStream();
String response;
try {
if (err == null) {
err = conn.getInputStream();
}
response = parseString(err);
} catch (IOException e) {
return null;
}
try {
return gson.fromJson(response, ErrorResponse.class);
} catch (JsonSyntaxException e) {
return null;
}
}
/**
* Creates an error response list object.
*
* @param conn
* @return ErrorResponse list object. return null if any error occurred
*/
private ArrayList getErrorResponses(HttpURLConnection conn) {
InputStream err = conn.getErrorStream();
String response;
try {
if (err == null) {
err = conn.getInputStream();
}
response = parseString(err);
} catch (IOException e) {
return null;
}
JsonElement jsonele = jsonParser.parse(response);
JsonObject object = jsonele.getAsJsonObject();
JsonArray array = object.getAsJsonArray("results");
Type type = new TypeToken>() {
}.getType();
ArrayList errorResponseList = gson.fromJson(array, type);
return errorResponseList;
}
/**
* An utility method converts a stream object to string.
*
* @param is input stream
* @return string
* @throws IOException
*/
private String parseString(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
try {
char[] buffer = new char[1024];
int dataOffset;
while (0 <= (dataOffset = reader.read(buffer))) {
sb.append(buffer, 0, dataOffset);
}
} finally {
reader.close();
}
return sb.toString();
}
/**
* Sets the proxy.
*
* @param host proxy host
* @param port proxy port
*/
public void setProxy(String host, Integer port) {
this.proxyHost = host;
this.proxyPort = port;
this.isHttpsProxy = false;
}
/**
* Sets the proxy with authentication.
*
* @param host proxy host
* @param port proxy port
* @param username proxy user
* @param password proxy password
*/
public void setProxy(String host, Integer port, String username, String password) {
this.proxyHost = host;
this.proxyPort = port;
this.proxyUser = username;
this.proxyPass = password;
this.isHttpsProxy = false;
//allowAuthenticationForHttps. This is required only for jdk > 8u11
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
this.proxyAuthenticator = new Authenticator() {
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(proxyUser, proxyPass.toCharArray());
}
};
}
/**
* Sets the SSL-secured proxy.
*
* @param host proxy host
* @param port proxy port
*/
public void setHttpsProxy(String host, Integer port) {
this.proxyHost = host;
this.proxyPort = port;
this.isHttpsProxy = true;
}
/**
* Sets the SSL-secured proxy with authentication.
*
* @param host proxy host
* @param port proxy port
* @param username proxy user
* @param password proxy password
*/
public void setHttpsProxy(String host, Integer port, String username, String password) {
this.proxyHost = host;
this.proxyPort = port;
this.proxyUser = username;
this.proxyPass = password;
this.isHttpsProxy = true;
}
/**
* Get uri string from api name.
*
* @param apiName api name
* @return pathURI
*/
public String getPathURI(String apiName) {
String pathURI = "";
if (this.guestSpaceId >= 0) {
pathURI = ConnectionConstants.BASE_GUEST_URL.replaceAll("\\{GUEST_SPACE_ID\\}", this.guestSpaceId + "");
} else {
pathURI = ConnectionConstants.BASE_URL;
}
pathURI = pathURI.replaceAll("\\{API_NAME\\}", apiName);
return pathURI;
}
private HttpsURLConnection openApiConnection(URL apiEndpoint) throws Exception {
HttpsURLConnection connection = null;
SSLContext sslcontext = null;
SSLSocketFactoryForHttpsProxy sslSocketFactory = null;
Proxy proxy = null;
try {
// if there is client certificate get the ssl context
if (this.auth.getClientCert() != null) {
sslcontext = this.auth.getClientCert();
}
// set proxy if any is present
if(proxyHost != null && proxyPort != null) {
if(this.isHttpsProxy && sslcontext != null) {
// forward factory from ssl context with client certificate
sslSocketFactory = new SSLSocketFactoryForHttpsProxy(sslcontext.getSocketFactory());
} else if( this.isHttpsProxy && sslcontext == null) {
sslSocketFactory = new SSLSocketFactoryForHttpsProxy();
} else {
// normal http proxy
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
}
// if sslSocketFactory exists it means proxy is https and needs to set proxy info
if(sslSocketFactory != null) {
sslSocketFactory.setProxyHost(proxyHost);
sslSocketFactory.setProxyPort(proxyPort);
if(proxyUser != null && proxyPass != null) {
sslSocketFactory.setProxyUserName(proxyUser);
sslSocketFactory.setProxyPassword(proxyPass);
}
}
} else if(sslcontext != null) {
// no ssl proxy, but there is client certificate so set factory to factory of ssl context
}
} catch (Exception err) {
throw err;
}
// set http proxy for connection
if(proxy != null) {
connection = (HttpsURLConnection) apiEndpoint.openConnection(proxy);
if(this.proxyAuthenticator != null) {
connection.setAuthenticator(this.proxyAuthenticator);
}
} else {
connection = (HttpsURLConnection) apiEndpoint.openConnection();
}
// set ssl socket factory for connection to connect to ssl secured proxy
if(sslSocketFactory != null) {
connection.setSSLSocketFactory(sslSocketFactory);
}
return connection;
}
}