/**
* Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0
* which accompanies this distribution, and is available at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Contributors:
* Puppet Labs
*/
package com.puppetlabs.puppetdb.javaclient.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.inject.Inject;
import com.puppetlabs.puppetdb.javaclient.APIException;
import com.puppetlabs.puppetdb.javaclient.APIPreferences;
import com.puppetlabs.puppetdb.javaclient.HttpConnector;
import com.puppetlabs.puppetdb.javaclient.query.Paging;
/**
* Class responsible for all HTTP request and response processing. Based on the
* Apache {@link HttpClient}.
*/
public class HttpComponentsConnector implements HttpConnector {
static InputStream getStream(HttpEntity entity) throws IOException {
if(entity == null)
return null;
return entity.getContent();
}
protected static <T> T parseJson(Gson gson, InputStream stream, Type type) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, HttpConnector.UTF_8), 2048);
StringBuilder bld = new StringBuilder();
String line;
while((line = reader.readLine()) != null) {
bld.append(line);
bld.append('\n');
}
try {
return gson.fromJson(bld.toString(), type);
}
catch(JsonSyntaxException jpe) {
throw new APIException("Parse exception converting JSON to object", jpe); //$NON-NLS-1$
}
finally {
try {
reader.close();
}
catch(IOException ignored) {
// Ignored
}
}
}
private final Gson gson;
private final HttpClient httpClient;
private final APIPreferences preferences;
private HttpRequestBase currentRequest;
/**
* <p>
* Creates a new HttpCommonsConnector.
* </p>
* <p>
* <b>For Guice injection only.</b> Don't use this constructor from code
* </p>
*
* @param gson
* The instance used when parsing or serializing JSON
* @param httpClient
* The client to use for the connection
* @param preferences
* API connection preferences
*/
@Inject
public HttpComponentsConnector(Gson gson, HttpClient httpClient, APIPreferences preferences) {
this.gson = gson;
this.preferences = preferences;
this.httpClient = httpClient;
}
@Override
public synchronized void abortCurrentRequest() {
if(currentRequest != null) {
currentRequest.abort();
currentRequest = null;
}
}
protected void assignContent(HttpEntityEnclosingRequestBase request, Map<String, String> params) {
if(params != null && !params.isEmpty()) {
List<NameValuePair> pairs = new ArrayList<NameValuePair>(params.size());
for(Map.Entry<String, String> param : params.entrySet())
pairs.add(new BasicNameValuePair(param.getKey(), param.getValue()));
try {
StringEntity entity = new StringEntity(URLEncodedUtils.format(pairs, UTF_8.name()), UTF_8.name());
entity.setContentType(CONTENT_TYPE_WWW_FORM_URLENCODED);
request.setEntity(entity);
}
catch(UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
}
protected void configureRequest(final HttpRequestBase request) {
request.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON);
request.addHeader(HttpHeaders.USER_AGENT, USER_AGENT);
}
/**
* Create exception from response
*
* @param response
* @param code
* @param status
* @return non-null newly {@link IOException}
*/
protected HttpResponseException createException(InputStream response, int code, String status) {
String message;
if(status != null && status.length() > 0)
message = status;
else
message = "Unknown error occurred";
return new HttpResponseException(code, message);
}
private HttpGet createGetRequest(String urlStr, Map<String, String> params) {
StringBuilder bld = new StringBuilder(createURI(urlStr));
if(params != null && !params.isEmpty()) {
List<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
for(Map.Entry<String, String> param : params.entrySet())
pairs.add(new BasicNameValuePair(param.getKey(), param.getValue()));
bld.append('?');
bld.append(URLEncodedUtils.format(pairs, UTF_8.name()));
}
return new HttpGet(URI.create(bld.toString()));
}
/**
* Create full URI from path
*
* @param path
* @return uri
*/
protected String createURI(String path) {
StringBuilder bld = new StringBuilder();
if(preferences.getCertPEM() == null)
bld.append("http://");
else
bld.append("https://");
bld.append(preferences.getServiceHostname());
bld.append(':');
bld.append(preferences.getServicePort());
bld.append('/');
if(path.startsWith("../"))
// Skip the 'v3' part (this is probably ../experimental/<something>
bld.append(path, 3, path.length());
else {
bld.append("v3");
bld.append(path);
}
return bld.toString();
}
@Override
public void delete(final String uri) throws IOException {
HttpDelete request = new HttpDelete(createURI(uri));
configureRequest(request);
executeRequest(request, null, null);
}
@Override
public void download(String urlStr, Map<String, String> params, final OutputStream output) throws IOException {
HttpGet request = createGetRequest(urlStr, params);
configureRequest(request);
httpClient.execute(request, new ResponseHandler<Void>() {
@Override
public Void handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
StatusLine statusLine = response.getStatusLine();
int code = statusLine.getStatusCode();
if(code != HttpStatus.SC_OK)
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
HttpEntity entity = response.getEntity();
entity.writeTo(output);
return null;
}
});
}
private synchronized void endRequest() {
currentRequest = null;
}
protected <V> V executeRequest(final HttpRequestBase request, final Type type, int[] totalCount) throws IOException {
startRequest(request);
try {
HttpResponse response = httpClient.execute(request);
StatusLine statusLine = response.getStatusLine();
int code = statusLine.getStatusCode();
if(code >= 300) {
String msg;
try {
msg = EntityUtils.toString(response.getEntity());
if(msg == null)
msg = statusLine.getReasonPhrase();
else {
msg = statusLine.getReasonPhrase() + ": " + msg;
}
}
catch(Exception e) {
// Just skip
msg = statusLine.getReasonPhrase();
}
throw new HttpResponseException(statusLine.getStatusCode(), msg);
}
HttpEntity entity = response.getEntity();
if(isOk(code)) {
if(type == null)
return null;
if(totalCount != null) {
Header xrecs = response.getFirstHeader("X-Records");
if(xrecs != null)
try {
totalCount[0] = Integer.parseInt(xrecs.getValue());
}
catch(NumberFormatException e) {
}
}
return parseJson(gson, getStream(entity), type);
}
throw createException(getStream(entity), code, statusLine.getReasonPhrase());
}
finally {
endRequest();
}
}
@Override
public <V> V get(String urlStr, Map<String, String> params, Type type) throws IOException {
HttpGet request = createGetRequest(urlStr, params);
configureRequest(request);
return executeRequest(request, type, null);
}
@Override
public <V, Q> V get(String urlStr, Paging<Q> params, Type type) throws IOException {
Map<String, String> queryParams = null;
int[] totalCount = null;
if(params != null) {
queryParams = new HashMap<String, String>();
params.appendTo(queryParams);
totalCount = new int[] { -1 };
}
HttpGet request = createGetRequest(urlStr, queryParams);
configureRequest(request);
V result = executeRequest(request, type, totalCount);
if(params != null)
params.setTotalCount(totalCount[0]);
return result;
}
/**
* Does status code denote a non-error response?
*
* @param code
* @return true if okay, false otherwise
*/
protected boolean isOk(final int code) {
switch(code) {
case HttpStatus.SC_OK:
case HttpStatus.SC_CREATED:
case HttpStatus.SC_ACCEPTED:
case HttpStatus.SC_NO_CONTENT: // weird, but returned by DELETE calls
return true;
default:
return false;
}
}
@Override
public <V> V patch(final String uri, final Map<String, String> params, final Class<V> type) throws IOException {
// HttpPatch is introduced in 4.2. This code is compatible with 4.1 in order to
// play nice with Eclipse Juno and Kepler
HttpPost request = new HttpPost(createURI(uri)) {
@Override
public String getMethod() {
return "PATCH";
}
};
configureRequest(request);
assignContent(request, params);
return executeRequest(request, type, null);
}
@Override
public <V> V post(final String uri, final Map<String, String> params, final Class<V> type) throws IOException {
HttpPost request = new HttpPost(createURI(uri));
configureRequest(request);
assignContent(request, params);
return executeRequest(request, type, null);
}
@Override
public <V> V postUpload(String uri, Map<String, String> stringParts, InputStream in, String mimeType, String fileName,
final long fileSize, Class<V> type) throws IOException {
HttpPost request = new HttpPost(createURI(uri));
configureRequest(request);
MultipartEntity entity = new MultipartEntity();
for(Map.Entry<String, String> entry : stringParts.entrySet())
entity.addPart(entry.getKey(), StringBody.create(entry.getValue(), "text/plain", UTF_8));
entity.addPart("file", new InputStreamBody(in, mimeType, fileName) {
@Override
public long getContentLength() {
return fileSize;
}
});
request.setEntity(entity);
return executeRequest(request, type, null);
}
@Override
public <V> V put(final String uri, final Map<String, String> params, final Class<V> type) throws IOException {
HttpPut request = new HttpPut(createURI(uri));
configureRequest(request);
assignContent(request, params);
return executeRequest(request, type, null);
}
private synchronized void startRequest(HttpRequestBase request) {
if(currentRequest != null)
currentRequest.abort();
currentRequest = request;
}
@Override
public String toJSON(Object object) {
return gson.toJson(object);
}
}