/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.rest;
import com.google.common.io.CharStreams;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.dto.server.JsonArrayImpl;
import org.eclipse.che.dto.server.JsonSerializable;
import org.eclipse.che.dto.server.JsonStringMapImpl;
import javax.validation.constraints.NotNull;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Objects.requireNonNull;
/**
* Simple implementation of {@link HttpJsonRequest} based on {@link HttpURLConnection}.
*
* <p>The implementation is not thread-safe, instance of this class must be created each time when it's needed.
*
* <p>The instance of this request is reusable, which means that
* it is possible to call {@link #request()} method more than one time per instance
*
* @author Yevhenii Voevodin
* @see DefaultHttpJsonRequestFactory
*/
public class DefaultHttpJsonRequest implements HttpJsonRequest {
private static final int DEFAULT_QUERY_PARAMS_LIST_SIZE = 5;
private static final Object[] EMPTY_ARRAY = new Object[0];
private final String url;
private int timeout;
private String method;
private Object body;
private List<Pair<String, ?>> queryParams;
private String authorizationHeaderValue;
protected DefaultHttpJsonRequest(String url, String method) {
this.url = requireNonNull(url, "Required non-null url");
this.method = method;
}
protected DefaultHttpJsonRequest(String url) {
this(url, HttpMethod.GET);
}
protected DefaultHttpJsonRequest(Link link) {
this(requireNonNull(link, "Required non-null link").getHref(), link.getMethod());
}
@Override
public HttpJsonRequest setMethod(@NotNull String method) {
this.method = requireNonNull(method, "Required non-null http method");
return this;
}
@Override
public HttpJsonRequest setBody(@NotNull Object body) {
this.body = requireNonNull(body, "Required non-null body");
return this;
}
@Override
public HttpJsonRequest setBody(@NotNull Map<String, String> map) {
this.body = new JsonStringMapImpl<>(requireNonNull(map, "Required non-null body"));
return this;
}
@Override
public HttpJsonRequest setBody(@NotNull List<?> list) {
this.body = new JsonArrayImpl<>(requireNonNull(list, "Required non-null body"));
return this;
}
@Override
public HttpJsonRequest addQueryParam(@NotNull String name, @NotNull Object value) {
requireNonNull(name, "Required non-null query parameter name");
requireNonNull(value, "Required non-null query parameter value");
if (queryParams == null) {
queryParams = new ArrayList<>(DEFAULT_QUERY_PARAMS_LIST_SIZE);
}
queryParams.add(Pair.of(name, value));
return this;
}
@Override
public HttpJsonRequest setAuthorizationHeader(@NotNull String value) {
requireNonNull(value, "Required non-null header value");
authorizationHeaderValue = value;
return this;
}
@Override
public HttpJsonRequest setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
@Override
public String getUrl() {
final UriBuilder ub = UriBuilder.fromUri(url);
if (queryParams != null) {
for (Pair<String, ?> parameter : queryParams) {
ub.queryParam(parameter.first, parameter.second);
}
}
return ub.build().toString();
}
@Override
public HttpJsonResponse request() throws IOException,
ServerException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
BadRequestException {
if (method == null) {
throw new IllegalStateException("Could not perform request, request method wasn't set");
}
return doRequest(timeout, url, method, body, queryParams, authorizationHeaderValue);
}
/**
* Makes this request using {@link HttpURLConnection}.
*
* <p>Uses {@link HttpHeaders#AUTHORIZATION} header with value from {@link EnvironmentContext}.
* <br>uses {@link HttpHeaders#ACCEPT} header with "application/json" value.
* <br>Encodes query parameters in "UTF-8".
*
* @param timeout
* request timeout, used only if it is greater than 0
* @param url
* request url
* @param method
* request method
* @param body
* request body, must be instance of {@link JsonSerializable}
* @param parameters
* query parameters, may be null
* @param authorizationHeaderValue
* value of authorization header, may be null
* @return response to this request
* @throws IOException
* when connection content type is not "application/json"
* @throws ServerException
* when response code is 500 or it is different from 400, 401, 403, 404, 409
* @throws ForbiddenException
* when response code is 403
* @throws NotFoundException
* when response code is 404
* @throws UnauthorizedException
* when response code is 401
* @throws ConflictException
* when response code is 409
* @throws BadRequestException
* when response code is 400
*/
protected DefaultHttpJsonResponse doRequest(int timeout,
String url,
String method,
Object body,
List<Pair<String, ?>> parameters,
String authorizationHeaderValue) throws IOException,
ServerException,
ForbiddenException,
NotFoundException,
UnauthorizedException,
ConflictException,
BadRequestException {
final String authToken = EnvironmentContext.getCurrent().getSubject().getToken();
final boolean hasQueryParams = parameters != null && !parameters.isEmpty();
if (hasQueryParams || authToken != null) {
final UriBuilder ub = UriBuilder.fromUri(url);
//remove sensitive information from url.
ub.replaceQueryParam("token", EMPTY_ARRAY);
if (hasQueryParams) {
for (Pair<String, ?> parameter : parameters) {
ub.queryParam(parameter.first, parameter.second);
}
}
url = ub.build().toString();
}
final HttpURLConnection conn = (HttpURLConnection)new URL(url).openConnection();
conn.setConnectTimeout(timeout > 0 ? timeout : 60000);
conn.setReadTimeout(timeout > 0 ? timeout : 60000);
try {
conn.setRequestMethod(method);
//drop a hint for server side that we want to receive application/json
conn.addRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
if (!isNullOrEmpty(authorizationHeaderValue)) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorizationHeaderValue);
} else if (authToken != null) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authToken);
}
if (body != null) {
conn.addRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
conn.setDoOutput(true);
if (HttpMethod.DELETE.equals(method)) { //to avoid jdk bug described here http://bugs.java.com/view_bug.do?bug_id=7157360
conn.setRequestMethod(HttpMethod.POST);
conn.setRequestProperty("X-HTTP-Method-Override", HttpMethod.DELETE);
}
try (OutputStream output = conn.getOutputStream()) {
output.write(DtoFactory.getInstance().toJson(body).getBytes());
}
}
final int responseCode = conn.getResponseCode();
if ((responseCode / 100) != 2) {
InputStream in = conn.getErrorStream();
if (in == null) {
in = conn.getInputStream();
}
final String str;
try (Reader reader = new InputStreamReader(in)) {
str = CharStreams.toString(reader);
}
final String contentType = conn.getContentType();
if (contentType != null && contentType.startsWith(MediaType.APPLICATION_JSON)) {
final ServiceError serviceError = DtoFactory.getInstance().createDtoFromJson(str, ServiceError.class);
if (serviceError.getMessage() != null) {
if (responseCode == Response.Status.FORBIDDEN.getStatusCode()) {
throw new ForbiddenException(serviceError);
} else if (responseCode == Response.Status.NOT_FOUND.getStatusCode()) {
throw new NotFoundException(serviceError);
} else if (responseCode == Response.Status.UNAUTHORIZED.getStatusCode()) {
throw new UnauthorizedException(serviceError);
} else if (responseCode == Response.Status.CONFLICT.getStatusCode()) {
throw new ConflictException(serviceError);
} else if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
throw new ServerException(serviceError);
} else if (responseCode == Response.Status.BAD_REQUEST.getStatusCode()) {
throw new BadRequestException(serviceError);
}
throw new ServerException(serviceError);
}
}
// Can't parse content as json or content has format other we expect for error.
throw new IOException(String.format("Failed access: %s, method: %s, response code: %d, message: %s",
UriBuilder.fromUri(url).replaceQuery("token").build(), method, responseCode, str));
}
final String contentType = conn.getContentType();
if (contentType != null && !contentType.startsWith(MediaType.APPLICATION_JSON)) {
throw new IOException(conn.getResponseMessage());
}
try (Reader reader = new InputStreamReader(conn.getInputStream())) {
return new DefaultHttpJsonResponse(CharStreams.toString(reader), responseCode);
}
} finally {
conn.disconnect();
}
}
@Override
public String toString() {
return "DefaultHttpJsonRequest{" +
"url='" + url + '\'' +
", timeout=" + timeout +
", method='" + method + '\'' +
", body=" + body +
", queryParams=" + queryParams +
'}';
}
}