/******************************************************************************* * 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 + '}'; } }