/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonServiceException.ErrorType;
import com.amazonaws.http.HttpResponse;
import com.amazonaws.http.HttpResponseHandler;
import com.amazonaws.util.json.JSONException;
import com.amazonaws.util.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Map.Entry;

public class StatusCodeErrorResponseHandler
        implements HttpResponseHandler<AmazonServiceException> {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private Map<Integer, Class<? extends AmazonServiceException>> exceptionClasses;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public StatusCodeErrorResponseHandler(Map<Integer, Class<? extends AmazonServiceException>> exceptionClasses) {
        this.exceptionClasses = exceptionClasses;
    }

    //-------------------------------------------------------------
    // Implementation - HttpResponseHandler
    //-------------------------------------------------------------

    public AmazonServiceException handle(HttpResponse response)
            throws Exception {
        JSONObject jsonBody = getBodyAsJson(response);
        Class<? extends AmazonServiceException> exceptionClass = exceptionClasses.get(response.getStatusCode());
        AmazonServiceException result;

        // Support other attribute names for the message?
        // TODO: Inspect exception type (caching details) and apply other values from the body
        String message = jsonBody.has("message") ? jsonBody.getString("message") : jsonBody.getString("Message");

        if (exceptionClass != null) {
            result = exceptionClass.getConstructor(String.class).newInstance(message);
        } else {
            result = AmazonServiceException.class.getConstructor(String.class).newInstance(message);
        }

        result.setServiceName(response.getRequest().getServiceName());
        result.setStatusCode(response.getStatusCode());

        if (response.getStatusCode() < 500) {
            result.setErrorType(ErrorType.Client);
        } else {
            result.setErrorType(ErrorType.Service);
        }

        for (Entry<String, String> headerEntry : response.getHeaders().entrySet()) {
            if (headerEntry.getKey().equalsIgnoreCase("X-Amzn-RequestId")) {
                result.setRequestId(headerEntry.getValue());
            }
        }

        return result;
    }

    public boolean needsConnectionLeftOpen() {
        return false;
    }

    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private JSONObject getBodyAsJson(HttpResponse response) {
        try (InputStream stream = response.getContent()) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
            StringBuilder sb = new StringBuilder();

            while (true) {
                String line = reader.readLine();

                if (line == null) {
                    break;
                }

                sb.append(line);
            }

            return new JSONObject(sb.length() == 0 ? "{}" : sb.toString());
        } catch (IOException e) {
            throw new AmazonClientException("Unable to read error response: " + e.getMessage(), e);
        } catch (JSONException e) {
            throw new AmazonClientException("Unable to parse error response: " + e.getMessage(), e);
        }
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

class HalJsonEmbeddedUnmarshaller
        implements Unmarshaller<Map<String, HalResource>, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonEmbeddedUnmarshaller instance = new HalJsonEmbeddedUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonEmbeddedUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public Map<String, HalResource> unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        Map<String, HalResource> embedded = new LinkedHashMap<>();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != JsonToken.END_OBJECT) {
            if (token == JsonToken.FIELD_NAME) {
                // Ignore the field name and move to the next token.  The item's key will be the embedded resource's selfHref.
                token = context.nextToken();

                if (token == JsonToken.START_ARRAY) {
                    List<HalResource> halResources = new HalJsonArrayUnmarshaller<>(HalJsonResourceUnmarshaller.getInstance()).unmarshall(context);

                    for (HalResource halResource : halResources) {
                        embedded.put(halResource._getSelfHref(), halResource);
                    }
                } else {
                    HalResource halResource = HalJsonResourceUnmarshaller.getInstance().unmarshall(context);

                    embedded.put(halResource._getSelfHref(), halResource);
                }
            }

            token = context.nextToken();
        }

        return embedded;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.hal.client.HalClient;
import com.amazonaws.http.HttpResponseHandler;
import com.amazonaws.http.JsonErrorResponseHandler;
import com.amazonaws.transform.JsonErrorUnmarshaller;

import java.util.Collections;
import java.util.Map;

/**
 * The HalService simplifies the configuration and access to an AWS HAL-based service API.  It is constructed
 * with service information, the interface class that describes the service's root resource, and an optional path
 * to the service root (if not specified it is assumed to be "/").  A HalService can be further configured,
 * using the builder pattern, to use a particular AWSCredentialsProvider or ClientConfiguration.
 *
 * Once configured, this class is used to retrieve the service's root resource.
 *
 * @param <T> The type of the root resource.
 */
public class HalService<T> {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private String endpoint;
    private String serviceName;
    private String regionId;
    private Class<T> rootClass;
    private String rootPath;
    private ClientConfiguration clientConfiguration;
    private AWSCredentialsProvider awsCredentialsProvider;
    private Map<String, Object> resourceCache;
    private HttpResponseHandler<AmazonServiceException> errorResponseHandler;
    private HalClient halClient;

    public static String DEFAULT_ROOT_PATH = "/";

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public HalService(String endpoint, String serviceName, String regionId, Class<T> rootClass,
                      HttpResponseHandler<AmazonServiceException> errorResponseHandler) {
        this(endpoint, serviceName, regionId, rootClass, DEFAULT_ROOT_PATH, errorResponseHandler);
    }

    public HalService(String endpoint, String serviceName, String regionId, Class<T> rootClass, String rootPath,
                      HttpResponseHandler<AmazonServiceException> errorResponseHandler) {
        this.endpoint = endpoint;
        this.serviceName = serviceName;
        this.regionId = regionId;
        this.rootClass = rootClass;
        this.rootPath = rootPath;
        this.errorResponseHandler = errorResponseHandler;
    }

    //-------------------------------------------------------------
    // Methods - Configuration
    //-------------------------------------------------------------

    public HalService<T> with(ClientConfiguration clientConfiguration) {
        setClientConfiguration(clientConfiguration);

        return this;
    }

    public void setClientConfiguration(ClientConfiguration clientConfiguration) {
        this.clientConfiguration = clientConfiguration;
    }

    public HalService<T> with(AWSCredentialsProvider awsCredentialsProvider) {
        setAwsCredentialsProvider(awsCredentialsProvider);

        return this;
    }

    public void setAwsCredentialsProvider(AWSCredentialsProvider awsCredentialsProvider) {
        this.awsCredentialsProvider = awsCredentialsProvider;
    }

    public HalService<T> with(Map<String, Object> resourceCache) {
        setResourceCache(resourceCache);

        return this;
    }

    public void setResourceCache(Map<String, Object> resourceCache) {
        this.resourceCache = resourceCache;
    }

    //-------------------------------------------------------------
    // Methods - Public
    //-------------------------------------------------------------

    public T getRootResource() {
        return getHalClient().getResource(rootClass, rootPath);
    }

    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private HalClient getHalClient() {
        if (halClient == null) {
            this.halClient = new HalClient(clientConfiguration == null ? new ClientConfiguration() : clientConfiguration,
                                           endpoint,
                                           serviceName,
                                           awsCredentialsProvider == null ? new DefaultAWSCredentialsProviderChain() : awsCredentialsProvider,
                                           resourceCache == null ? ImmediatelyExpiringCache.getInstance() : resourceCache,
                                           errorResponseHandler);

            if (regionId != null) {
                halClient.setSignerRegionOverride(regionId);
            }
        }

        return halClient;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

import java.util.ArrayList;
import java.util.List;

class HalJsonListUnmarshaller
        implements Unmarshaller<List<Object>, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonListUnmarshaller instance = new HalJsonListUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonListUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public List<Object> unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        List<Object> list = new ArrayList<>();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != JsonToken.END_ARRAY) {
            if (token.isScalarValue()) {
                list.add(JsonUnmarshallerUtil.getObjectForToken(token, context));
            } else if (token == JsonToken.START_OBJECT) {
                context.nextToken();
                list.add(HalJsonMapUnmarshaller.getInstance().unmarshall(context));
            } else if (token == JsonToken.START_ARRAY) {
                context.nextToken();
                list.add(HalJsonListUnmarshaller.getInstance().unmarshall(context));
            }

            token = context.nextToken();
        }

        return list;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

import java.util.HashMap;
import java.util.Map;

class HalJsonMapUnmarshaller
        implements Unmarshaller<Map<String, Object>, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonMapUnmarshaller instance = new HalJsonMapUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonMapUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public Map<String, Object> unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        Map<String, Object> map = new HashMap<>();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != JsonToken.END_OBJECT) {
            if (token == JsonToken.FIELD_NAME) {
                String property = context.readText();

                token = context.nextToken();
                if (token == JsonToken.START_OBJECT) {
                    context.nextToken();
                    map.put(property, HalJsonMapUnmarshaller.getInstance().unmarshall(context));
                } else if (token == JsonToken.START_ARRAY) {
                    context.nextToken();
                    map.put(property, HalJsonListUnmarshaller.getInstance().unmarshall(context));
                } else {
                    map.put(property, JsonUnmarshallerUtil.getObjectForToken(token, context));
                }
            }

            token = context.nextToken();
        }

        return map;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonWebServiceClient;
import com.amazonaws.AmazonWebServiceResponse;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.http.ExecutionContext;
import com.amazonaws.http.HttpMethodName;
import com.amazonaws.http.HttpResponseHandler;
import com.amazonaws.http.JsonErrorResponseHandler;
import com.amazonaws.http.JsonResponseHandler;
import com.amazonaws.transform.JsonErrorUnmarshaller;
import com.amazonaws.transform.VoidJsonUnmarshaller;
import com.amazonaws.util.AWSRequestMetrics;
import com.amazonaws.util.StringInputStream;
import com.amazonaws.util.json.JSONObject;

import java.lang.reflect.Proxy;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.amazonaws.http.HttpMethodName.GET;
import static com.amazonaws.http.HttpMethodName.PATCH;
import static com.amazonaws.http.HttpMethodName.POST;
import static com.amazonaws.http.HttpMethodName.PUT;
import static com.amazonaws.http.HttpMethodName.DELETE;

/**
 * The HalClient is a lower-level class for interacting with a HAL-based API.  Preferably, a set of interface
 * classes that describe the interaction with the service is used, and this class is only used behind the
 * scenes.
 */
public class HalClient extends AmazonWebServiceClient {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private AWSCredentialsProvider awsCredentialsProvider;
    private Map<String, Object> resourceCache;
    private HttpResponseHandler<AmazonServiceException> errorResponseHandler;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public HalClient(ClientConfiguration clientConfiguration, String endpoint, String serviceName,
                     AWSCredentialsProvider awsCredentialsProvider, Map<String, Object> resourceCache,
                     HttpResponseHandler<AmazonServiceException> errorResponseHandler) {
        super(clientConfiguration);

        this.setServiceNameIntern(serviceName);
        this.setEndpoint(endpoint);
        this.awsCredentialsProvider = awsCredentialsProvider;
        this.resourceCache = resourceCache;
        this.errorResponseHandler = errorResponseHandler;
        this.addRequestHandler(new AcceptHalJsonRequestHandler());
    }

    //-------------------------------------------------------------
    // Methods - Public
    //-------------------------------------------------------------

    public <T> T getResource(Class<T> resourceClass, String resourcePath) {
        return getResource(null, resourceClass, resourcePath, false);
    }

    public <T> T postResource(Class<T> resourceClass, String resourcePath, Object representation) {
        OptionalJsonResponseHandler<HalResource> responseHandler = getResponseHandler(resourceClass);
        HalResource halResource = invoke(POST, resourcePath, representation, responseHandler);
        Object cachedResource = resourceCache.get(resourcePath);

        String halResourcePath = getHalResourcePath(halResource, responseHandler);

        // Check if the cached resource we just POSTed to is the same resource we got back.  If yes, update the existing proxy's
        // invocation handler with the new data and return it.  If no, the target was likely a collection resource.  We leave the
        // cached data as is, letting the caching strategy update it when needed.
        // TODO: Review collection cache clearing strategy.
        if (cachedResource != null && resourcePath.equals(halResourcePath)) {
            HalResourceInvocationHandler invocationHandler = (HalResourceInvocationHandler) Proxy.getInvocationHandler(cachedResource);

            invocationHandler.resourceUpdated(halResource);
            // TODO: follow embedded resources and call resourceUpdated()

            return resourceClass.cast(cachedResource);
        }

        return halResource == null ? null : createAndCacheResource(resourceClass, resourcePath, halResource);
    }

    public <T> T putResource(Class<T> resourceClass, String resourcePath, Object representation) {
        OptionalJsonResponseHandler<HalResource> responseHandler = getResponseHandler(resourceClass);
        HalResource halResource = invoke(PUT, resourcePath, representation,responseHandler);
        Object cachedResource = resourceCache.get(resourcePath);

        // Per RFC2616, section 9.6 PUT, the cached resource should refer to the same resource we just received, so we update the
        // existing proxy's invocation handler with the new data.
        if (cachedResource != null) {
            HalResourceInvocationHandler invocationHandler = (HalResourceInvocationHandler) Proxy.getInvocationHandler(cachedResource);

            invocationHandler.resourceUpdated(halResource);
            // TODO: follow embedded resources and call resourceUpdated()

            return resourceClass.cast(cachedResource);
        }

        return halResource == null ? null : createAndCacheResource(resourceClass, resourcePath, halResource);
    }

    public <T> T deleteResource(Class<T> resourceClass, String resourcePath) {
        OptionalJsonResponseHandler<HalResource> responseHandler = getResponseHandler(resourceClass);
        HalResource halResource = invoke(DELETE, resourcePath, null, responseHandler);
        Object cachedResource = resourceCache.get(resourcePath);

        // Per RFC2616, section 9.7 DELETE, the resource should be removed from the cache.  We additionally clear the cached
        // resource in case references to the proxy exist elsewhere.
        if (cachedResource != null) {
            HalResourceInvocationHandler invocationHandler = (HalResourceInvocationHandler) Proxy.getInvocationHandler(cachedResource);

            invocationHandler.resourceUpdated(null);

            resourceCache.remove(resourcePath);
            // TODO: follow embedded resources and call remove() and resourceUpdated()
        }

        return halResource == null ? null : createResource(resourceClass, resourcePath, halResource);
    }

    public <T> T patchResource(Class<T> resourceClass, String resourcePath, Object representation) {
        OptionalJsonResponseHandler<HalResource> responseHandler = getResponseHandler(resourceClass);
        HalResource halResource = invoke(PATCH, resourcePath, representation, responseHandler);
        Object cachedResource = resourceCache.get(resourcePath);

        String halResourcePath = getHalResourcePath(halResource, responseHandler);

        // Check if the cached resource we just PATCHed to is the same resource we got back.  If yes, update the existing proxy's
        // invocation handler with the new data and return it.
        // TODO: Handle multi-resource patches (e.g. child resources of the current resource)
        // TODO: Review collection cache clearing strategy.
        if (cachedResource != null && resourcePath.equals(halResourcePath)) {
            HalResourceInvocationHandler invocationHandler = (HalResourceInvocationHandler) Proxy.getInvocationHandler(cachedResource);

            invocationHandler.resourceUpdated(halResource);
            // TODO: follow embedded resources and call resourceUpdated()

            return resourceClass.cast(cachedResource);
        }

        return halResource == null ? null : createAndCacheResource(resourceClass, resourcePath, halResource);
    }

    //-------------------------------------------------------------
    // Methods - Package
    //-------------------------------------------------------------

    <T> T getResource(HalResource sourceResource, Class<T> resourceClass, String resourcePath, boolean lazy) {
        if (resourceCache.containsKey(resourcePath)) {
            return resourceClass.cast(resourceCache.get(resourcePath));
        }

        HalResource halResource;

        if (sourceResource != null && sourceResource.getEmbedded().containsKey(resourcePath)) {
            halResource = sourceResource.getEmbedded().get(resourcePath);
        } else if (lazy) {
            halResource = null;
        } else {
            halResource = getHalResource(resourcePath);
        }

        return createAndCacheResource(resourceClass, resourcePath, halResource);
    }

    HalResource getHalResource(String resourcePath) {
        JsonResponseHandler<HalResource> responseHandler = new JsonResponseHandler<>(HalJsonResourceUnmarshaller.getInstance());

        return invoke(GET, resourcePath, null, responseHandler);
    }

    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private <T> T invoke(HttpMethodName httpMethodName, String resourcePath, Object representation,
                         HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler)
            throws AmazonClientException {
        ExecutionContext executionContext = createExecutionContext();
        AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();

        awsRequestMetrics.startEvent(AWSRequestMetrics.Field.RequestMarshallTime.name());
        Request request = buildRequest(httpMethodName, resourcePath, representation);
        awsRequestMetrics.endEvent(AWSRequestMetrics.Field.RequestMarshallTime.name());

        awsRequestMetrics.startEvent(AWSRequestMetrics.Field.CredentialsRequestTime.name());
        AWSCredentials credentials = awsCredentialsProvider.getCredentials();
        awsRequestMetrics.endEvent(AWSRequestMetrics.Field.CredentialsRequestTime.name());

        executionContext.setCredentials(credentials);

        awsRequestMetrics.startEvent(AWSRequestMetrics.Field.ClientExecuteTime.name());
        Response<T> response = client.execute(request, responseHandler, errorResponseHandler, executionContext);
        awsRequestMetrics.endEvent(AWSRequestMetrics.Field.ClientExecuteTime.name());

        awsRequestMetrics.log();

        return response.getAwsResponse();
    }

    private Request buildRequest(HttpMethodName httpMethodName, String resourcePath, Object representation) {
        Request request = new DefaultRequest(null);

        request.setHttpMethod(httpMethodName);
        request.setEndpoint(endpoint);

        populateResourcePathAndParameters(request, resourcePath);

        if (representation != null) {
            assignContent(request, representation);
        }

        return request;
    }

    private void populateResourcePathAndParameters(Request request, String resourcePath) {
        int questionIndex = resourcePath.indexOf("?");

        if (questionIndex < 0) {
            request.setResourcePath(resourcePath);

            return;
        }

        request.setResourcePath(resourcePath.substring(0, questionIndex));

        for (String parameterPair : resourcePath.substring(questionIndex + 1).split("&")) {
            int equalIndex = parameterPair.indexOf("=");

            if (equalIndex < 0) { // key with no value
                //noinspection deprecation
                request.addParameter(URLDecoder.decode(parameterPair), null);
            } else { // key=value
                //noinspection deprecation
                request.addParameter(URLDecoder.decode(parameterPair.substring(0, equalIndex)),
                                     URLDecoder.decode(parameterPair.substring(equalIndex + 1)));
            }
        }
    }

    private void assignContent(Request request, Object representation) {
        String contentString = new JSONObject(representation).toString();

        if (contentString == null) {
            throw new AmazonClientException("Unable to marshall representation to JSON: " + representation);
        }

        try {
            byte[] contentBytes = contentString.getBytes("UTF-8");

            request.setContent(new StringInputStream(contentString));
            request.addHeader("Content-Length", Integer.toString(contentBytes.length));
            request.addHeader("Content-Type", "application/json");
        } catch(Throwable t) {
            throw new AmazonClientException("Unable to marshall request to JSON: " + t.getMessage(), t);
        }
    }

    private <T> OptionalJsonResponseHandler<HalResource> getResponseHandler(Class<T> resourceClass) {
        if (resourceClass.equals(Void.class) || resourceClass.equals(void.class)) {
            return new OptionalJsonResponseHandler<>(new VoidJsonUnmarshaller<HalResource>());
        }

        return new OptionalJsonResponseHandler<>(HalJsonResourceUnmarshaller.getInstance());
    }

    private <T> T createAndCacheResource(Class<T> resourceClass, String resourcePath, HalResource halResource) {
        T t = createResource(resourceClass, resourcePath, halResource);

        resourceCache.put(resourcePath, t);

        return t;
    }

    private <T> T createResource(Class<T> resourceClass, String resourcePath, HalResource halResource) {
        Object proxy = Proxy.newProxyInstance(resourceClass.getClassLoader(),
                                              new Class<?>[] { resourceClass },
                                              new HalResourceInvocationHandler(halResource, resourcePath, this));

        return resourceClass.cast(proxy);
    }

    private String getHalResourcePath(HalResource halResource, OptionalJsonResponseHandler<HalResource> responseHandler) {
        String resourcePath;

        if (halResource != null && halResource.isDefined()) {
            resourcePath = halResource._getSelfHref();
        } else {
            String endpointString = endpoint.toString();
            String location = responseHandler.getLocation();

            // Will throw an NPE if no location is present.  This is okay, since it means we don't know what this resource is
            // or where to find it.
            if (location.startsWith(endpointString)) {
                resourcePath = location.substring(endpointString.length());
            } else {
                resourcePath = location;
            }
        }

        return resourcePath;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import java.lang.reflect.Type;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import static com.amazonaws.hal.client.ConversionUtil.convert;

public class ConvertingMap
        implements Map {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private Type type;
    private Map backingMap;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public ConvertingMap(Type type, Map backingMap) {
        this.type = type;
        this.backingMap = backingMap;
    }

    //-------------------------------------------------------------
    // Implementation - Map
    //-------------------------------------------------------------

    @Override
    public int size() {
        return backingMap.size();
    }

    @Override
    public boolean isEmpty() {
        return backingMap.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return backingMap.containsKey(key);
    }

    // TODO: test w/ map of Integer...
    @Override
    public Object get(Object key) {
        Object value = backingMap.get(key);

        // When a value is accessed, it's intended type can either be a
        // class or some other type (like a ParameterizedType).
        //
        // If the target type is a class and the value is of that type,
        // we return it.  If the value is not of that type, we convert
        // it and store the converted value (trusting it was converted
        // properly) back to the backing store.
        //
        // If the target type is not a class, it may be ParameterizedType
        // like List<T> or Map<K, V>.  We check if the value is already
        // a converting type and if so, we return it.  If the value is
        // not, we convert it and if it's now a converting type, we store
        // the new value in the backing store.

        if (type instanceof Class) {
            if (!((Class) type).isInstance(value)) {
                value = convert(type, value);

                //noinspection unchecked
                backingMap.put(key, value);
            }
        } else {
            if (!(value instanceof ConvertingMap) && !(value instanceof ConvertingList)) {
                value = convert(type, value);

                if (value instanceof ConvertingMap || value instanceof ConvertingList) {
                    //noinspection unchecked
                    backingMap.put(key, value);
                }
            }
        }

        return value;
    }

    @Override
    public Set keySet() {
        return backingMap.keySet();
    }

    @Override
    public Collection values() {
        //noinspection unchecked
        return new ConvertingList(type, new ArrayList(backingMap.values()));
    }

    @Override
    public Set<Entry> entrySet() {
        return new EntrySet();
    }

    @Override
    public Object put(Object key, Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object remove(Object key) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void putAll(Map m) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException();
    }

    //-------------------------------------------------------------
    // Inner Classes
    //-------------------------------------------------------------

    private final class EntrySet extends AbstractSet<Entry> {
        public Iterator<Entry> iterator() {
            return new ConvertingEntryIterator();
        }

        public boolean contains(Object o) {
            throw new UnsupportedOperationException();
        }

        public boolean remove(Object o) {
            throw new UnsupportedOperationException();
        }

        public int size() {
            return backingMap.size();
        }

        public void clear() {
            throw new UnsupportedOperationException();
        }
    }

    private final class ConvertingEntryIterator
            implements Iterator<Entry> {
        @SuppressWarnings("unchecked")
        Iterator<Entry> backingIterator = backingMap.entrySet().iterator();

        @Override
        public boolean hasNext() {
            return backingIterator.hasNext();
        }

        @Override
        public Entry next() {
            Entry entry = backingIterator.next();
            return new ConvertingEntry(entry.getKey(), entry.getValue());
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    private final class ConvertingEntry
            implements Entry {
        private Object key;
        private Object value;

        private ConvertingEntry(Object key, Object value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public Object getKey() {
            return key;
        }

        @Override
        public Object getValue() {
            if (!(type instanceof Class) || !value.getClass().isAssignableFrom((Class) type)) {
                value = convert(type, value);

                // TODO: Re-store this in the backingIterator, but beware ConcurrentModificationException
//                backingMap.put(key, value);
            }

            return value;
        }

        @Override
        public Object setValue(Object value) {
            throw new UnsupportedOperationException();
        }
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;

import com.fasterxml.jackson.core.JsonToken;

import java.io.IOException;

class JsonUnmarshallerUtil {

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    private JsonUnmarshallerUtil() {
    }

    //-------------------------------------------------------------
    // Methods - Package
    //-------------------------------------------------------------

    static Object getObjectForToken(JsonToken token, JsonUnmarshallerContext context)
            throws IOException {
        switch (token) {
        case VALUE_STRING:
            return context.getJsonParser().getText();
        case VALUE_NUMBER_FLOAT:
        case VALUE_NUMBER_INT:
            return context.getJsonParser().getNumberValue();
        case VALUE_FALSE:
            return Boolean.FALSE;
        case VALUE_TRUE:
            return Boolean.TRUE;
        case VALUE_NULL:
            return null;
        default:
            throw new RuntimeException("We expected a VALUE token but got: " + token);
        }
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

class HalJsonLinksUnmarshaller
        implements Unmarshaller<Map<String, HalLink>, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonLinksUnmarshaller instance = new HalJsonLinksUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonLinksUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public Map<String, HalLink> unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        Map<String, HalLink> links = new LinkedHashMap<>();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != JsonToken.END_OBJECT) {
            if (token == JsonToken.FIELD_NAME) {
                if (context.testExpression("curie")) {
                    context.nextToken();
                    HalJsonCurieUnmarshaller.getInstance().unmarshall(context);
                } else {
                    String relation = context.readText();
                    token = context.nextToken();

                    if (token == JsonToken.START_ARRAY) {
                        List<HalLink> halLinks = new HalJsonArrayUnmarshaller<>(HalJsonLinkUnmarshaller.getInstance()).unmarshall(context);

                        int i = 0;
                        for (HalLink halLink : halLinks) {
                            links.put(relation + "_" + i++, halLink);
                        }
                    } else {
                        links.put(relation, HalJsonLinkUnmarshaller.getInstance().unmarshall(context));
                    }
                }
            }

            token = context.nextToken();
        }

        return links;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

import java.util.ArrayList;
import java.util.List;

import static com.fasterxml.jackson.core.JsonToken.END_ARRAY;

class HalJsonArrayUnmarshaller<T>
        implements Unmarshaller<List<T>, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Constants
    //-------------------------------------------------------------

    private final Unmarshaller<T, JsonUnmarshallerContext> itemUnmarshaller;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public HalJsonArrayUnmarshaller(Unmarshaller<T, JsonUnmarshallerContext> itemUnmarshaller) {
        this.itemUnmarshaller = itemUnmarshaller;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public List<T> unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        List<T> list = new ArrayList<>();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != END_ARRAY) {
            if (token == JsonToken.START_OBJECT) {
                list.add(itemUnmarshaller.unmarshall(context));
            }

            token = context.nextToken();
        }

        return list;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.http.JsonResponseHandler;
import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import java.util.Map;

class OptionalJsonResponseHandler<T> extends JsonResponseHandler<T> {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private String location;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    OptionalJsonResponseHandler(Unmarshaller<T, JsonUnmarshallerContext> responseUnmarshaller) {
        super(responseUnmarshaller);
    }

    //-------------------------------------------------------------
    // Methods - Getter/Setter
    //-------------------------------------------------------------

    String getLocation() {
        return location;
    }

    //-------------------------------------------------------------
    // Methods - Protected
    //-------------------------------------------------------------

    @Override
    protected void registerAdditionalMetadataExpressions(JsonUnmarshallerContext unmarshallerContext) {
        Map<String, String> headers = unmarshallerContext.getHttpResponse().getHeaders();

        if (headers.containsKey("Location")) {
            location = headers.get("Location");
        } else if (headers.containsKey("location")) {
            location = headers.get("location");
        }
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

class HalJsonResourceUnmarshaller
        implements Unmarshaller<HalResource, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonResourceUnmarshaller instance = new HalJsonResourceUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonResourceUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public HalResource unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        HalResource halResource = new HalResource();
        JsonToken token = context.getCurrentToken();

        if (token == null) {
            token = context.nextToken();
        }

        while (token != null && token != JsonToken.END_OBJECT) {
            if (token == JsonToken.FIELD_NAME) {
                if (context.testExpression("_links")) {
                    context.nextToken();
                    halResource.setLinks(HalJsonLinksUnmarshaller.getInstance().unmarshall(context));
                } else if (context.testExpression("_embedded")) {
                    context.nextToken();
                    halResource.setEmbedded(HalJsonEmbeddedUnmarshaller.getInstance().unmarshall(context));
                } else {
                    String property = context.readText();

                    token = context.nextToken();

                    if (token == JsonToken.START_OBJECT) {
                        context.nextToken();
                        halResource.addProperty(property, HalJsonMapUnmarshaller.getInstance().unmarshall(context));
                    } else if (token == JsonToken.START_ARRAY) {
                        context.nextToken();
                        halResource.addProperty(property, HalJsonListUnmarshaller.getInstance().unmarshall(context));
                    } else {
                        halResource.addProperty(property, JsonUnmarshallerUtil.getObjectForToken(token, context));
                    }
                }
            }

            token = context.nextToken();
        }

        return halResource;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import static com.amazonaws.hal.client.ConversionUtil.convert;

public class ConvertingList
        implements List {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private Type type;
    private List backingList;

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    public ConvertingList(Type type, List backingList) {
        this.type = type;
        this.backingList = backingList;
    }

    //-------------------------------------------------------------
    // Implementation - Collection
    //-------------------------------------------------------------

    @Override
    public int size() {
        return backingList.size();
    }

    @Override
    public boolean isEmpty() {
        return backingList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object[] toArray() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object[] toArray(Object[] a) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean add(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean containsAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean addAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean removeAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean retainAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException();
    }

    //-------------------------------------------------------------
    // Implementation - Iterable
    //-------------------------------------------------------------

    @Override
    public Iterator iterator() {
        return new ConvertingIterator();
    }

    //-------------------------------------------------------------
    // Implementation - List
    //-------------------------------------------------------------

    @Override
    public Object get(int index) {
        Object value = backingList.get(index);

        // When a value is accessed, it's intended type can either be a
        // class or some other type (like a ParameterizedType).
        //
        // If the target type is a class and the value is of that type,
        // we return it.  If the value is not of that type, we convert
        // it and store the converted value (trusting it was converted
        // properly) back to the backing store.
        //
        // If the target type is not a class, it may be ParameterizedType
        // like List<T> or Map<K, V>.  We check if the value is already
        // a converting type and if so, we return it.  If the value is
        // not, we convert it and if it's now a converting type, we store
        // the new value in the backing store.

        if (type instanceof Class) {
            if (!((Class) type).isInstance(value)) {
                value = convert(type, value);

                //noinspection unchecked
                backingList.set(index, value);
            }
        } else {
            if (!(value instanceof ConvertingMap) && !(value instanceof ConvertingList)) {
                value = convert(type, value);

                if (value instanceof ConvertingMap || value instanceof ConvertingList) {
                    //noinspection unchecked
                    backingList.set(index, value);
                }
            }
        }

        return value;
    }

    @Override
    public List subList(int fromIndex, int toIndex) {
        return new ConvertingList(type, backingList.subList(fromIndex, toIndex));
    }

    @Override
    public ListIterator listIterator() {
        throw new UnsupportedOperationException();
    }

    @Override
    public ListIterator listIterator(int index) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean addAll(int index, Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object set(int index, Object element) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void add(int index, Object element) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object remove(int index) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int indexOf(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int lastIndexOf(Object o) {
        throw new UnsupportedOperationException();
    }

    //-------------------------------------------------------------
    // Inner Classes
    //-------------------------------------------------------------

    private final class ConvertingIterator
            implements Iterator {
        Iterator backingIterator = backingList.iterator();

        @Override
        public boolean hasNext() {
            return backingIterator.hasNext();
        }

        @Override
        public Object next() {
            // TODO: Re-store this in the backingIterator, but beware ConcurrentModificationException
            return convert(type, backingIterator.next());
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.bind.DatatypeConverter;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;

class ConversionUtil {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private static final Log log = LogFactory.getLog(ConversionUtil.class);

    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    private ConversionUtil() {
    }

    //-------------------------------------------------------------
    // Methods - Package
    //-------------------------------------------------------------

    static Type getCollectionType(Type type, int index, Class defaultClass) {
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;

            return parameterizedType.getActualTypeArguments()[index];
        } else {
            return defaultClass;
        }
    }

    static String getPropertyName(String original) {
        StringBuilder propertyName = new StringBuilder(original.substring("get".length()));

        propertyName.setCharAt(0, Character.toLowerCase(propertyName.charAt(0)));

        return propertyName.toString();
    }

    static Object convert(Type type, Object value) {
        if (value == null) {
            return convertFromNull(type);
        } else if (value instanceof Number) {
            return convertFromNumber((Class) type, (Number) value);
        } else if (value instanceof Boolean) {
            return value;
        } else if (value instanceof String) {
            return convertFromString((Class) type, (String) value);
        } else if (value instanceof Map) {
            return convertFromMap(type, (Map) value);
        } else if (value instanceof List) {
            return convertFromList(type, (List) value);
        } else {
            throw new RuntimeException("Not sure how to convert " + value + " to a " + type);
        }
    }

    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private static Object convertFromNull(Type type) {
        if (!(type instanceof Class)) {
            return null;
        }

        Class<?> clazz = (Class) type;

        if (!clazz.isPrimitive()) {
            return null;
        }

        if (int.class.isAssignableFrom(clazz)) {
            return 0;
        } else if (long.class.isAssignableFrom(clazz)) {
            return 0L;
        } else if (short.class.isAssignableFrom(clazz)) {
            return 0;
        } else if (double.class.isAssignableFrom(clazz)) {
            return 0.0;
        } else if (float.class.isAssignableFrom(clazz)) {
            return 0.0F;
        } else if (boolean.class.isAssignableFrom(clazz)) {
            return Boolean.FALSE;
        } else if (char.class.isAssignableFrom(clazz)) {
            return 0;
        } else if (byte.class.isAssignableFrom(clazz)) {
            return 0;
        } else {
            throw new RuntimeException("Unexpected primitive type: " + clazz.getSimpleName());
        }
    }

    private static Object convertFromNumber(Class<?> clazz, Number value) {
        if (String.class.isAssignableFrom(clazz)) {
            return value.toString();
        } else if (int.class.isAssignableFrom(clazz) || Integer.class.isAssignableFrom(clazz)) {
            return value.intValue();
        } else if (long.class.isAssignableFrom(clazz) || Long.class.isAssignableFrom(clazz)) {
            return value.longValue();
        } else if (short.class.isAssignableFrom(clazz) || Short.class.isAssignableFrom(clazz)) {
            return value.shortValue();
        } else if (double.class.isAssignableFrom(clazz) || Double.class.isAssignableFrom(clazz)) {
            return value.doubleValue();
        } else if (float.class.isAssignableFrom(clazz) || Float.class.isAssignableFrom(clazz)) {
            return value.floatValue();
        } else if (boolean.class.isAssignableFrom(clazz) || Boolean.class.isAssignableFrom(clazz)) {
            return Boolean.valueOf(value.toString());
        } else if (char.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz)) {
            if (value.longValue() <= 255) {
                return (char)value.longValue();
            } else {
                throw new RuntimeException("Not sure how to convert " + value + " to a " + clazz.getSimpleName());
            }
        } else if (byte.class.isAssignableFrom(clazz) || Byte.class.isAssignableFrom(clazz)) {
            return value.byteValue();
        } else if (BigDecimal.class.isAssignableFrom(clazz)) {
            return new BigDecimal(value.toString());
        } else if (BigInteger.class.isAssignableFrom(clazz)) {
            // Necessary because BigInteger(long) is a private method and we need to convert the Number to a long to
            // prevent the constructor from throwing a NumberFormatException Example: BigInteger(1.2)
            return new BigInteger(String.valueOf(value.longValue()));
        } else if (Date.class.isAssignableFrom(clazz)) {
            return new Date(value.longValue());
        } else if (clazz.isEnum()) {
            try {
                //noinspection unchecked
                return Enum.valueOf((Class<Enum>) clazz, value.toString());
            } catch (IllegalArgumentException e) {
                log.error(String.format("'%s' is not a recognized enum value for %s.  Returning default of %s instead.",
                                        value, clazz.getName(), clazz.getEnumConstants()[0]));

                return clazz.getEnumConstants()[0];
            }
        } else {
            throw new RuntimeException("Not sure how to convert " + value + " to a " + clazz.getSimpleName());
        }
    }

    private static Object convertFromString(Class<?> clazz, String value) {
        if (String.class.isAssignableFrom(clazz)) {
            return value;
        } else if (int.class.isAssignableFrom(clazz) || Integer.class.isAssignableFrom(clazz)) {
            return new Integer(value);
        } else if (long.class.isAssignableFrom(clazz) || Long.class.isAssignableFrom(clazz)) {
            return new Long(value);
        } else if (short.class.isAssignableFrom(clazz) || Short.class.isAssignableFrom(clazz)) {
            return new Short(value);
        } else if (double.class.isAssignableFrom(clazz) || Double.class.isAssignableFrom(clazz)) {
            return new Double(value);
        } else if (float.class.isAssignableFrom(clazz) || Float.class.isAssignableFrom(clazz)) {
            return new Float(value);
        } else if (boolean.class.isAssignableFrom(clazz) || Boolean.class.isAssignableFrom(clazz)) {
            return Boolean.valueOf(value);
        } else if (char.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz)) {
            return value.charAt(0);
        } else if (byte.class.isAssignableFrom(clazz) || Byte.class.isAssignableFrom(clazz)) {
            return new Byte(value);
        } else if (BigDecimal.class.isAssignableFrom(clazz)) {
            return new BigDecimal(value);
        } else if (BigInteger.class.isAssignableFrom(clazz)) {
            return new BigInteger(value);
        } else if (Date.class.isAssignableFrom(clazz)) {
            try {
                return new Date(Long.parseLong(value));
            } catch (NumberFormatException e) {
                try {
                    return DatatypeConverter.parseDateTime(value).getTime();
                } catch (IllegalArgumentException e1) {
                    throw new RuntimeException("Unexpected date format: " + value + ".  We currently parse xsd:datetime and milliseconds.");
                }
            }
        } else if (clazz.isEnum()) {
            try {
                //noinspection unchecked
                return Enum.valueOf((Class<Enum>) clazz, value);
            } catch (IllegalArgumentException e) {
                log.error(String.format("'%s' is not a recognized enum value for %s.  Returning default of %s instead.",
                                        value, clazz.getName(), clazz.getEnumConstants()[0]));

                return clazz.getEnumConstants()[0];
            }
        } else {
            throw new RuntimeException("Not sure how to convert " + value + " to a " + clazz.getSimpleName());
        }
    }

    private static Object convertFromMap(Type type, Map value) {
        if (type instanceof Class && !Map.class.isAssignableFrom((Class) type)) {
            Class typeClass = (Class) type;

            return Proxy.newProxyInstance(typeClass.getClassLoader(),
                                          new Class<?>[] { typeClass },
                                          new MapBackedInvocationHandler(type, value));
        } else {
            return new ConvertingMap(getCollectionType(type, 1, Object.class), value);
        }
    }

    private static Object convertFromList(Type type, List value) {
        return new ConvertingList(getCollectionType(type, 0, Object.class), value);
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

class HalJsonLinkUnmarshaller
        implements Unmarshaller<HalLink, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonLinkUnmarshaller instance = new HalJsonLinkUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonLinkUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public HalLink unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        HalLink halLink = new HalLink();
        JsonToken token = context.getCurrentToken();

        while (token != null && token != JsonToken.END_OBJECT) {
            if (token == JsonToken.FIELD_NAME) {
                if (context.testExpression("href")) {
                    context.nextToken();
                    halLink.setHref(context.readText());
                } else if (context.testExpression("name")) {
                    context.nextToken();
                    halLink.setName(context.readText());
                } else if (context.testExpression("title")) {
                    context.nextToken();
                    halLink.setTitle(context.readText());
                } else if (context.testExpression("templated")) {
                    context.nextToken();
                    halLink.setTemplated(Boolean.valueOf(context.readText()));
                } else if (context.testExpression("deprecation")) {
                    context.nextToken();
                    halLink.setDeprecation(context.readText());
                } else {
                    // Ignore this.  Likely one of hreflang, profile, type
                    context.nextToken();
                }
            }

            token = context.nextToken();
        }

        return halLink;
    }
}


/*
 * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.hal.client;

import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.Unmarshaller;

import com.fasterxml.jackson.core.JsonToken;

class HalJsonCurieUnmarshaller
        implements Unmarshaller<HalLink, JsonUnmarshallerContext> {

    //-------------------------------------------------------------
    // Variables - Private - Static
    //-------------------------------------------------------------

    private static HalJsonCurieUnmarshaller instance = new HalJsonCurieUnmarshaller();

    //-------------------------------------------------------------
    // Methods - Package - Static
    //-------------------------------------------------------------

    static HalJsonCurieUnmarshaller getInstance() {
        return instance;
    }

    //-------------------------------------------------------------
    // Implementation - Unmarshaller
    //-------------------------------------------------------------

    @Override
    public HalLink unmarshall(JsonUnmarshallerContext context)
            throws Exception {
        HalLink halLink = new HalLink();
        JsonToken token = context.getCurrentToken();

        // Ignore curies for now.
        while (token != null && token != JsonToken.END_OBJECT) {
            token = context.nextToken();
        }

        return halLink;
    }
}


/*
 * Copyright (c) 2011-2015 Amedia Utvikling AS.
 *
 * 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 no.api.meteo.service.locationforecast;

import no.api.meteo.MeteoException;
import no.api.meteo.client.MeteoClient;
import no.api.meteo.client.MeteoData;
import no.api.meteo.client.MeteoResponse;
import no.api.meteo.entity.core.MeteoServiceVersion;
import no.api.meteo.entity.core.service.locationforecast.LocationForecast;
import no.api.meteo.service.AbstractMeteoService;
import no.api.meteo.service.MeteoDataParser;

import static no.api.meteo.util.MeteoConstants.PARAM_ALTITUDE;
import static no.api.meteo.util.MeteoConstants.PARAM_LATITUDE;
import static no.api.meteo.util.MeteoConstants.PARAM_LONGITUDE;

/**
 * Service object for fetching data for the LocationforecastLTS service in the MET API.
 */
public final class LocationforecastLTSService extends AbstractMeteoService {

    private static final String MET_SERVICE_NAME = "locationforecastlts";

    private final MeteoDataParser<LocationForecast> parser;

    /**
     * Constructor requiring an MeteoClient instance.
     *
     * @param meteoClient
     *         An instance of MeteoClient that the service will use to fetch the data.
     */
    public LocationforecastLTSService(MeteoClient meteoClient) {
        super(meteoClient, MET_SERVICE_NAME, new MeteoServiceVersion(1, 3));
        parser = new LocationforcastLTSParser();
    }

    /**
     * Fetch a LocationforecastLTS from the MET API, based on longitude, latitude and altitude.
     *
     * @param longitude
     *         The longitude of the location where the forecast should be fetched for.
     * @param latitude
     *         The latitude of the location where the forecast should be fetched for.
     * @param altitude
     *         The altitude of the location where the forecast should be fetched for.
     *
     * @return A MeteoData object containing response information and data converted into a LocationForcast object.
     *
     * @throws MeteoException
     *         If a problem occurred while fetching or parsing the MET data.
     */
    public MeteoData<LocationForecast> fetchContent(double longitude, double latitude, int altitude)
            throws MeteoException {
        MeteoResponse response = getMeteoClient().fetchContent(
                createServiceUriBuilder()
                        .addParameter(PARAM_LATITUDE, latitude)
                        .addParameter(PARAM_LONGITUDE, longitude)
                        .addParameter(PARAM_ALTITUDE, altitude).build()
        );
        return new MeteoData<>(parser.parse(response.getData()), response);
    }

    /**
     * Fetch a LocationforecastLTS from the MET API, based on longitude, and latitude.
     *
     * @param longitude
     *         The longitude of the location where the forecast should be fetched for.
     * @param latitude
     *         The latitude of the location where the forecast should be fetched for.
     *
     * @return A MeteoData object containing response information and data converted into a LocationForcast object.
     *
     * @throws MeteoException
     *         If a problem occurred while fetching or parsing the MET data.
     */
    public MeteoData<LocationForecast> fetchContent(double longitude, double latitude) throws MeteoException {
        MeteoResponse response = getMeteoClient().fetchContent(
                createServiceUriBuilder()
                        .addParameter(PARAM_LATITUDE, latitude)
                        .addParameter(PARAM_LONGITUDE, longitude).build()
        );
        return new MeteoData<>(parser.parse(response.getData()), response);
    }

}


/*
 * Copyright (c) 2011-2015 Amedia Utvikling AS.
 *
 * 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 no.api.meteo.service.sunrise;

import no.api.meteo.MeteoException;
import no.api.meteo.client.MeteoClient;
import no.api.meteo.client.MeteoData;
import no.api.meteo.client.MeteoResponse;
import no.api.meteo.entity.core.MeteoServiceVersion;
import no.api.meteo.entity.core.service.sunrise.Sunrise;
import no.api.meteo.service.AbstractMeteoService;
import no.api.meteo.service.MeteoDataParser;

import java.time.LocalDate;

import static no.api.meteo.util.MeteoConstants.PARAM_DATE;
import static no.api.meteo.util.MeteoConstants.PARAM_FROM;
import static no.api.meteo.util.MeteoConstants.PARAM_LATITUDE;
import static no.api.meteo.util.MeteoConstants.PARAM_LONGITUDE;
import static no.api.meteo.util.MeteoConstants.PARAM_TO;
import static no.api.meteo.util.MeteoDateUtils.zonedDateTimeToYyyyMMdd;

/**
 * Service object for fetching data for the Sunrise service in the MET API.
 *
 * https://api.met.no/weatherapi/sunrise/1.0/documentation
 */
public final class SunriseService extends AbstractMeteoService {

    private final MeteoDataParser<Sunrise> parser;

    /**
     * Constructor requiring an MeteoClient instance.
     *
     * @param meteoClient
     *         An instance of MeteoClient that the service will use to fetch the data.
     */
    public SunriseService(MeteoClient meteoClient) {
        super(meteoClient, "sunrise", new MeteoServiceVersion(1, 1));
        parser = new SunriseParser();
    }

    /**
     * Fetch a Sunrise from the MET API based on a given longitude, latitude and date.
     *
     * @param longitude
     *         The longitude of the location where the Sunrise should be fetched from.
     * @param latitude
     *         The latitude of the location where the Sunrise should be fetched from.
     * @param date
     *         The date to fetch the Sunrise object for.
     *
     * @return A MeteoData object containing response information and sunrise result converted into a Sunrise object.
     *
     * @throws MeteoException
     *         If a problem occurred while fetching or parsing the MET data.
     */
    public MeteoData<Sunrise> fetchContent(double longitude, double latitude, LocalDate date) throws MeteoException {
        MeteoResponse response = getMeteoClient().fetchContent(
                createServiceUriBuilder()
                        .addParameter(PARAM_LATITUDE, latitude)
                        .addParameter(PARAM_LONGITUDE, longitude)
                        .addParameter(PARAM_DATE, zonedDateTimeToYyyyMMdd(date)).build());
        return new MeteoData<>(parser.parse(response.getData()), response);
    }

    /**
     * Fetch a Sunrise from the MET API based on a given longitude, latitude and date range.
     *
     * @param longitude
     *         The longitude of the location where the Sunrise should be fetched from.
     * @param latitude
     *         The latitude of the location where the Sunrise should be fetched from.
     * @param from
     *         The date to fetch the Sunrise object from.
     * @param to
     *         The date to fetch the Sunrise object to.
     *
     * @return A MeteoData object containing response information and sunrise result converted into a Sunrise object.
     *
     * @throws MeteoException
     *         If a problem occurred while fetching or parsing the MET data.
     */
    public MeteoData<Sunrise> fetchContent(double longitude, double latitude, LocalDate from, LocalDate to)
            throws MeteoException {
        MeteoResponse response = getMeteoClient().fetchContent(
                createServiceUriBuilder()
                        .addParameter(PARAM_LATITUDE, latitude)
                        .addParameter(PARAM_LONGITUDE, longitude)
                        .addParameter(PARAM_FROM, zonedDateTimeToYyyyMMdd(from))
                        .addParameter(PARAM_TO, zonedDateTimeToYyyyMMdd(to)).build()
        );
        return new MeteoData<>(parser.parse(response.getData()), response);
    }

}


/*
 * Copyright (c) 2011-2015 Amedia Utvikling AS.
 *
 * 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 no.api.meteo.service.locationforecast.extras;

import lombok.extern.slf4j.Slf4j;
import no.api.meteo.entity.core.service.locationforecast.Forecast;
import no.api.meteo.entity.core.service.locationforecast.LocationForecast;
import no.api.meteo.entity.core.service.locationforecast.PeriodForecast;
import no.api.meteo.entity.core.service.locationforecast.PointForecast;
import no.api.meteo.entity.extras.MeteoExtrasForecast;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static no.api.meteo.util.MeteoDateUtils.METZONE;
import static no.api.meteo.util.MeteoDateUtils.cloneZonedDateTime;
import static no.api.meteo.util.MeteoDateUtils.getNow;
import static no.api.meteo.util.MeteoDateUtils.toZeroMSN;

/**
 * Helper class that will save you from the dirty job of interpreting the forecast data yourself.
 *
 *
Interpreting the location forecast data can be pretty hard. This is an implementation of how we at Amedia
 * Utvikling chooses to do it.

 */
@Slf4j
public final class LocationForecastHelper extends AbstractForecastHelper {

    /**
     * Construct a new instance of this helper without any title set.
     *
     * @param locationForecast
     *         The location forecast this helper will work on.
     */
    public LocationForecastHelper(LocationForecast locationForecast) {
        super(locationForecast);
    }

    /**
     * Get all point forecasts from now and to the given hours ahead.
     *
     * @param hoursAhead
     *         The number of hours to look ahead for point forecasts.
     *
     * @return List of found forecasts.
     */
    public List<MeteoExtrasForecast> findHourlyPointForecastsFromNow(int hoursAhead) {
        List<MeteoExtrasForecast> pointExtrasForecasts = new ArrayList<>();
        ZonedDateTime now = getNow();
        for (int i = 0; i < hoursAhead; i++) {
            ZonedDateTime ahead = now.plusHours(i);
            Optional<PointForecast> pointForecast = getIndexer().getPointForecast(ahead);
            pointForecast.ifPresent(pof -> {
                Optional<PeriodForecast> periodForecast =
                        getIndexer().getTightestFitPeriodForecast(pof.getFrom());
                periodForecast.ifPresent(pef -> pointExtrasForecasts
                        .add(new MeteoExtrasForecast(pof, pef)));
            });
        }
        return pointExtrasForecasts;
    }

    /**
     * Get the most accurate forecast for the given date.
     *
     * @param dateTime
     *         The date to get the forecast for.
     *
     * @return Optional containing the forecast if found, else {@link Optional#empty()}
     */
    public Optional<MeteoExtrasForecast> findNearestForecast(ZonedDateTime dateTime) {
        ZonedDateTime dt = toZeroMSN(dateTime.withZoneSameInstant(METZONE));
        PointForecast chosenForecast = null;
        for (Forecast forecast : getLocationForecast().getForecasts()) {
            if (forecast instanceof PointForecast) {
                PointForecast pointForecast = (PointForecast) forecast;
                if (isDateMatch(dt, cloneZonedDateTime(pointForecast.getFrom()))) {
                    chosenForecast = pointForecast;
                    break;
                } else if (chosenForecast == null) {
                    chosenForecast = pointForecast;
                } else if (isNearerDate(pointForecast.getFrom(), dt, chosenForecast.getFrom())) {
                    chosenForecast = pointForecast;
                }
            }
        }
        if (chosenForecast == null) {
            return Optional.empty();
        }
        return Optional.of(
                new MeteoExtrasForecast(
                        chosenForecast,
                        getIndexer().getWidestFitPeriodForecast(chosenForecast.getFrom()).orElse(null)));
    }

    private boolean isNearerDate(ZonedDateTime pointTime, ZonedDateTime dateTime, ZonedDateTime chosenTime) {
        return Math.abs(pointTime.toInstant().getEpochSecond() - dateTime.toInstant().getEpochSecond())
                < Math.abs(chosenTime.toInstant().getEpochSecond() - dateTime.toInstant().getEpochSecond());
    }

    private boolean isDateMatch(ZonedDateTime requestedDate, ZonedDateTime actualDate) {
        return requestedDate.getYear() == actualDate.getYear() &&
                requestedDate.getMonthValue() == actualDate.getMonthValue()
                && requestedDate.getDayOfMonth() == actualDate.getDayOfMonth() &&
                requestedDate.getHour() == actualDate.getHour();
    }

}