Java tutorial
/** * Copyright 2012 Terremark Worldwide Inc. * * Licensed 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.terremark.impl; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Locale; import java.util.Map; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import org.apache.commons.lang3.StringUtils; import org.apache.commons.net.util.SubnetUtils; import org.apache.http.conn.util.InetAddressUtils; import org.apache.wink.client.ClientWebException; import org.apache.wink.client.Resource; import org.apache.wink.client.RestClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.terremark.api.TerremarkError; import com.terremark.config.ContentType; import com.terremark.config.RetryHandler; import com.terremark.config.Version; import com.terremark.exception.AccessDeniedException; import com.terremark.exception.AuthenticationDeniedException; import com.terremark.exception.InternalServerException; import com.terremark.exception.InvalidRequestException; import com.terremark.exception.NotFoundException; import com.terremark.exception.NotImplementedException; import com.terremark.exception.RequestFailedException; import com.terremark.exception.ServiceUnavailableException; import com.terremark.exception.TerremarkException; /** * Abstract class extended by all handler implementations. Provides implementations for generic * {@code get/post/put/delete} HTTP calls. Performs query argument validation, if necessary. And is also responsible for * exception/retry handling. * * @author <a href="mailto:spasam@terremark.com">Seshu Pasam</a> */ @SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod") abstract class AbstractAPIImpl { /** Logger */ private static final Logger LOG = LoggerFactory.getLogger("com.terremark"); /** HTML content-type */ private static final String HTML_CONTENT_TYPE = "text/html"; /** Rest client instance */ private final RestClient client; /** Client configuration */ private final ClientConfiguration properties; /** API version */ private final Version clientVersion; /** * Default constructor. * * @param client Rest client instance. * @param properties Client configuration. */ protected AbstractAPIImpl(final RestClient client, final ClientConfiguration properties) { this.client = client; this.properties = properties; this.clientVersion = properties.getVersion(); } /** * HTTP get call. Delegates the call to {@link #get(Version, String, Map, Map, Class, Object...)}. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param responseClass Expected response type. * @param arguments API call arguments. * @return Response. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ <T> T get(final Version version, final String relativePath, final Class<T> responseClass, final Object... arguments) throws TerremarkException { return get(version, relativePath, null, null, responseClass, arguments); } /** * HTTP get call. Checks for API version mis-match. Validates the arguments, if necessary. Retries the call, if a * retry handler is configured and the API invocation fails. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param queryParams Query parameters. * @param extraHeaders Additional headers. * @param responseClass Expected response type. * @param arguments API call arguments. * @return Response. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") <T> T get(final Version version, final String relativePath, final Map<String, String> queryParams, final Map<String, String> extraHeaders, final Class<T> responseClass, final Object... arguments) throws TerremarkException { checkVersion(version); validateArguments(arguments); if (responseClass == null) { throw new IllegalArgumentException("Invalid response type"); } int failureCount = 0; final RetryHandler retryHandler = properties.getRetryHandler(); do { try { return getResource(relativePath, queryParams, extraHeaders, arguments).get(responseClass); } catch (final Exception ex) { failureCount++; try { handleException(ex); } catch (TerremarkException te) { if (te.getMajorErrorCode() > 499 && retryHandler != null && retryHandler.shouldRetry( failureCount, te, relativePath, queryParams, extraHeaders, responseClass, arguments)) { LOG.warn("Retrying request: {}. Failure count: {}. HTTP status: {}. Code: {}. Message: {}", new Object[] { relativePath, Integer.valueOf(failureCount), Integer.valueOf(te.getMajorErrorCode()), te.getMinorErrorCode(), te.getErrorMessage() }); continue; } throw te; } } } while (true); } /** * HTTP put call. Checks for API version mis-match. Validates the arguments, if necessary. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param responseClass Expected response type. * @param requestEntity Request entity to send a body. * @param arguments API call arguments. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ <R, S> S put(final Version version, final String relativePath, final Class<S> responseClass, final R requestEntity, final Object... arguments) throws TerremarkException { checkVersion(version); validateArguments(arguments); if (requestEntity == null) { throw new IllegalArgumentException("Invalid request entity argument"); } try { return getResource(relativePath, requestEntity, arguments).put(responseClass, requestEntity); } catch (final Exception ex) { handleException(ex); return null; } } /** * HTTP post call. Checks for API version mis-match. Validates the arguments, if necessary. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param responseClass Expected response type. * @param requestEntity Request entity to send a body. * @param arguments API call arguments. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ <R, S> S post(final Version version, final String relativePath, final Class<S> responseClass, final R requestEntity, final Object... arguments) throws TerremarkException { checkVersion(version); validateArguments(arguments); try { return getResource(relativePath, requestEntity, arguments).post(responseClass, requestEntity); } catch (final Exception ex) { handleException(ex); return null; } } /** * HTTP delete call. Checks for API version mis-match. Validates the arguments, if necessary. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param responseClass Expected response type. * @param arguments API call arguments. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ void delete(final Version version, final String relativePath, final Object... arguments) throws TerremarkException { checkVersion(version); validateArguments(arguments); try { getResource(relativePath, null, arguments).delete(); } catch (final Exception ex) { handleException(ex); } } /** * HTTP get call. Checks for API version mis-match. Validates the arguments, if necessary. * * @param version API version this method call was implemented in. * @param relativePath Relative path. * @param responseClass Expected response type. * @param arguments API call arguments. * @return Response. * @throws TerremarkException If an error occurs or if an Terremark error is returned. */ <S> S delete(final Version version, final String relativePath, final Class<S> responseClass, final Object... arguments) throws TerremarkException { checkVersion(version); validateArguments(arguments); try { return getResource(relativePath, null, arguments).delete(responseClass); } catch (final Exception ex) { handleException(ex); return null; } } /** * Returns the ISO 8601 format date/time. * * @param time Date/time. * @return ISO 8601 format date/time. */ protected static String getISO8601Time(final Date time) { if (time == null) { throw new IllegalArgumentException("Invalid date/time argument"); } final SimpleDateFormat sdf = new SimpleDateFormat(TerremarkConstants.ISO_8601_DATE_FORMAT, Locale.getDefault()); sdf.setTimeZone(TerremarkConstants.GMT_TIME_ZONE); return sdf.format(time); } /** * Validates the query arguments against the metadata. {@link java.lang.IllegalArgumentException} is thrown if the * arguments does not match the metadata information. * * @param filterArguments Query arguments. Can be null. * @param metadata Metadata for the query arguments. */ @SuppressWarnings({ "unused", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) protected static void validateQueryArguments(final Map<String, String> filterArguments, final Map<String, QueryArgument> metadata) { if (filterArguments == null) { return; } for (Map.Entry<String, String> entry : filterArguments.entrySet()) { final String key = entry.getKey(); final String value = entry.getValue(); if (key == null) { throw new IllegalArgumentException("Invalid filter argument key"); } if (StringUtils.isEmpty(value)) { throw new IllegalArgumentException("Invalid filter argument value for " + key); } final QueryArgument argInfo = metadata.get(key); if (argInfo == null) { throw new IllegalArgumentException("Invalid filter argument: " + key); } switch (argInfo.getType()) { case INTEGER: int i; try { i = Integer.parseInt(value); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". Must be a valid integer", ex); } if (argInfo.getMinValue() != Integer.MAX_VALUE && argInfo.getMaxValue() != Integer.MIN_VALUE && (i < argInfo.getMinValue() || i > argInfo.getMaxValue())) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". It should be between " + argInfo.getMinValue() + " and " + argInfo.getMaxValue()); } break; case LIST: boolean found = false; for (String str : argInfo.getArgs()) { if (value.equalsIgnoreCase(str)) { found = true; break; } } if (!found) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". It should be one of: " + Arrays.asList(argInfo.getArgs())); } break; case ISO8601_DATE: final SimpleDateFormat sdf = new SimpleDateFormat(TerremarkConstants.ISO_8601_DATE_FORMAT, Locale.getDefault()); try { sdf.parse(value); } catch (ParseException ex) { throw new IllegalArgumentException( "Invalid filter argument value for '" + key + "': " + value + ". Must be a valid date/time in ISO 8601 format: yyyy-MM-dd'T'HH:mm:'00Z'", ex); } break; case HOSTNAME: try { InetAddress.getByName(value); } catch (UnknownHostException ex) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". Must be a valid hostname/IP address", ex); } break; case IP_ADDRESS: if (!InetAddressUtils.isIPv4Address(value)) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". Must be a valid IPv4 address"); } break; case SUBNET: new SubnetUtils(value); break; case URI: try { new URI(value); } catch (URISyntaxException ex) { throw new IllegalArgumentException("Invalid filter argument value for '" + key + "': " + value + ". Must be a valid relative URI", ex); } break; default: break; } } } /** * Compares the API method version against the client configured version. If the client is configured to use older * version and a newer API method is invoked, {@link NotImplementedException} is thrown. * * @param apiVersion Version the API method was implemented in. * @throws NotImplementedException If client is configured to use older version and a newer API method is invoked */ private void checkVersion(final Version apiVersion) throws NotImplementedException { if (clientVersion == null) { return; } if (clientVersion.ordinal() < apiVersion.ordinal()) { throw new NotImplementedException("Terremark client is configured to use API version " + clientVersion.name() + "/" + clientVersion.toString() + ". The API method you are invoking is supported in version " + clientVersion.name() + "/" + clientVersion.toString() + " or later"); } } /** * Validates arguments. If the argument is null, {@link java.lang.IllegalArgumentException} is thrown. * * @param arguments Can be null or zero size. */ private static void validateArguments(final Object... arguments) { if (arguments == null || arguments.length < 1) { return; } for (Object arg : arguments) { if (arg == null) { throw new IllegalArgumentException("Invalid input argument"); } } } /** * Method to process exception. For all 4XX/5XX error codes, an exception is thrown by the REST implementation. In * most cases, an appropriate Terremark error is also returned, which contains more details on why the API call * failed. This method, throws specific exceptions for the various error conditions. * * @param exception Root cause. * @throws TerremarkException More specific exception. */ @SuppressWarnings("PMD") private static void handleException(final Exception exception) throws TerremarkException { if (!(exception instanceof ClientWebException)) { throw new TerremarkException(exception); } TerremarkError error = null; ClientWebException ex = (ClientWebException) exception; if (ex.getResponse() != null) { String contentType = ex.getResponse().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); try { if (contentType != null && contentType.startsWith(HTML_CONTENT_TYPE)) { LOG.error("Got {} error from Terremark with text/html response", Integer.valueOf(ex.getResponse().getStatusCode())); } else { error = ex.getResponse().getEntity(TerremarkError.class); } } catch (Exception ignore) { // We don't want this to mask the root cause LOG.error( "Terremark Java API error. Please report this to the developers. Exception retrieving Terremark error. " + "HTTP status: {}. HTTP message: {}. HTTP Headers: {}", new Object[] { Integer.toString(ex.getResponse().getStatusCode()), ex.getResponse().getMessage(), ex.getResponse().getHeaders(), ignore }); } finally { ex.getResponse().consumeContent(); } } if (ex.getResponse() != null) { switch (ex.getResponse().getStatusCode()) { case 400: // Replay attack, clock skew etc case 412: // Invalid version case 415: // Unsupported media type throw new InvalidRequestException(error, ex); case 401: // Authentication failed case 407: // Proxy authentication required throw new AuthenticationDeniedException(error, ex); case 403: throw new AccessDeniedException(error, ex); case 404: // Request object not found throw new NotFoundException(error, ex); case 409: case 420: case 421: throw new RequestFailedException(error, ex); case 500: throw new InternalServerException(error, ex); case 501: throw new NotImplementedException(error, ex); case 503: throw new ServiceUnavailableException(error, ex); default: // Just to make PMD happy throw new TerremarkException(error, ex); } } throw new TerremarkException(error, ex); } /** * Generic method used for all HTTP calls. This is responsible for constructing rest client request and returning * the response. * * @param relativePath Relative path. * @param queryParams Query arguments. * @param extraHeaders Additional headers. * @param arguments Arguments for the path. * @return Resource that can be deserialized as a response. */ private Resource getResource(final String relativePath, final Map<String, String> queryParams, final Map<String, String> extraHeaders, final Object... arguments) { final UriBuilder builder = UriBuilder.fromPath(properties.getUri() + relativePath); if (queryParams != null) { for (final Map.Entry<String, String> entry : queryParams.entrySet()) { if (entry.getValue() != null) { builder.queryParam(entry.getKey(), entry.getValue()); } } } Resource resource = client.resource(builder.build(arguments)).accept(getContentType()); if (extraHeaders != null) { for (final Map.Entry<String, String> entry : extraHeaders.entrySet()) { if (entry.getValue() != null) { resource = resource.header(entry.getKey(), entry.getValue()); } } } return resource; } /** * Used by {@code put}/{@code post}/{@code delete} requests. This just calls * {@link #getResource(String, Map, Map, Object...)}. Sets {@code Content-Type} header if the request entity is not * null. If the request entity is null, this method sets the {@code Content-Length} header to zero. * * @param relativePath Relative path of the request. * @param arguments Arguments for building the URL. * @return The resource on which to execute the HTTP request. */ @SuppressWarnings("PMD.UnusedPrivateMethod") private <T> Resource getResource(final String relativePath, final T requestEntity, final Object... arguments) { final Resource resource = getResource(relativePath, null, null, arguments); if (requestEntity == null) { return resource.header(HttpHeaders.CONTENT_LENGTH, "0"); } return resource.header(HttpHeaders.CONTENT_TYPE, getContentType()); } /** * Returns the content type as configured by the user. * * @return Content type. */ private String getContentType() { if (properties.getContentType() == ContentType.XML) { return MediaType.APPLICATION_XML; } return MediaType.APPLICATION_JSON; } }