//*********************************************************************************************************************
//Copyright 2013 Amazon Technologies, 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://aws.amazon.com/apache2.0 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.
//*********************************************************************************************************************

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class AGCODSampleCode {

    /**
     * Types of API this sample code supports
     */
    public enum AGCODServiceOperation {
        ActivateGiftCard,
        DeactivateGiftCard,
        ActivationStatusCheck,
        CreateGiftCard,
        CancelGiftCard,
        GetGiftCardActivityPage
    }

    /**
     * An enumeration of supported formats for the payload
     */
    public enum PayloadType {
        JSON,
        XML
    }

    //Static headers used in the request
    private static final String ACCEPT_HEADER = "accept";
    private static final String CONTENT_HEADER = "content-type"; 
    private static final String HOST_HEADER = "host";
    private static final String XAMZDATE_HEADER = "x-amz-date"; 
    private static final String XAMZTARGET_HEADER = "x-amz-target";
    private static final String AUTHORIZATION_HEADER = "Authorization"; 

    //Static format parameters
    private static final String DATE_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
    private static final String DATE_TIMEZONE = "UTC";
    private static final String UTF8_CHARSET = "UTF-8";

    //Signature calculation related parameters
    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    private static final String HASH_SHA256_ALGORITHM = "SHA-256";
    private static final String AWS_SHA256_ALGORITHM = "AWS4-HMAC-SHA256";
    private static final String KEY_QUALIFIER = "AWS4";
    private static final String TERMINATION_STRING = "aws4_request";

    //User and instance parameters
    private static final String awsKeyID = ""; // Your KeyID
    private static final String awsSecretKey = ""; // Your Key

    //Service and target (API) parameters
    private static final String regionName = "us-east-1";//lowercase!
    private static final String serviceName = "AGCODService";

    //Payload parameters
    private static final String partnerID = "";
    private static final String requestID = "";
    private static final String cardNumber = "";
    
    //Additional payload parameters for ActivateGiftCard
    private static final String currencyCode = "";
    private static final String amount = "";

    //Additional payload parameters for CancelGiftCard
    private static final String gcId = "";
    
  //Additional payload parameters for GetGiftCardActivityPage
    private static final long pageIndex = 0;
    private static final long pageSize = 1;
    private static final String utcStartDate = ""; //"yyyy-MM-ddTHH:mm:ss eg. 2013-06-01T23:10:10"
    private static final String utcEndDate = "";//"yyyy-MM-ddTHH:mm:ss eg. 2013-06-01T23:15:10"
    private static final boolean showNoOps = true;
    
    //Parameters that specify what format the payload should be in and what fields will be in the payload
    private static final PayloadType msgPayloadType = PayloadType.JSON;
    //private static final PayloadType msgPayloadType = PayloadType.XML;
    
    private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.ActivationStatusCheck;
    //private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.ActivateGiftCard;
    //private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.DeactivateGiftCard;
    //private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.CreateGiftCard;
    //private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.CancelGiftCard;
    //private static final AGCODServiceOperation serviceOperation = AGCODServiceOperation.GetGiftCardActivityPage;

    //Parameters used in the message header
    private static String dateTimeString = null; // use null for service call. Code below will add current x-amz-date time stamp 
    private static final String host = "agcod-v2-gamma.amazon.com";
    private static final String protocol = "https";
    private static final String queryString = "";

    private static String contentType;
    private static final StringBuilder requestURI = new StringBuilder("/").append(serviceOperation.toString());
    private static final StringBuilder serviceTarget = new StringBuilder().append("com.amazonaws.agcod.AGCODService").append(".").append(serviceOperation.toString()); 
    private static final StringBuilder hostName = new StringBuilder().append(protocol).append("://").append(host).append(requestURI);
    
    /**
     * Does all logic to create a message payload, establish a connection, set headers, and authenticate.
     * 
     * @param args - None required, all should be set above as a static variables
     */
    public static void main(String[] args) {

        try {     
            //Initialize whole payload in the specified format for the given operation and set additional headers based on these settings
            StringBuilder payload = new StringBuilder();
            setPayload(payload);
            
            //Create the URL connection to the host
            URL hostConnection = new URL(hostName.toString());
            HttpURLConnection conn = (HttpURLConnection)hostConnection.openConnection();

            //Set the message to be a POST
            conn.setRequestMethod("POST");

            //Create Authentication signature and set the connection parameters
            signRequestAWSv4(conn, payload);

            //Indicate that the connection will write data to the URL connection
            conn.setDoOutput(true);

            //Write the output message to the connection, if it errors it will generate an IOException
            OutputStream output = conn.getOutputStream();
            output.write(payload.toString().getBytes(UTF8_CHARSET));
            
            //Read input or error stream based on response code
            BufferedReader in = null;
            String inputLine;
            if (conn.getResponseCode() != 200) {
                in = new BufferedReader(new InputStreamReader(conn.getErrorStream())); 
            } else {
                in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            }
            System.out.println("\nRESPONSE:");
            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);  
            }
            conn.disconnect();
        } 
        catch (Exception e) {
            //If any element of the signing, payload creation, or connection throws an exception then terminate since we cannot continue.
            System.out.println(e.toString());
            System.exit(1);
        }  
    }

    /**
     * Creates a canonical request based on set static parameters
     * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
     * 
     * @param payload - The payload to be hashed as part of the canonical request
     * 
     * @return The whole canonical request string to be used in Task 2
     * 
     * @throws NoSuchAlgorithmException
     * @throws UnsupportedEncodingException
     */
    private static String buildCanonicalRequest(StringBuilder payload) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        //Create a SHA256 hash of the payload, used in authentication
        String payloadHash = hash(payload.toString());

        //Canonical request headers should be sorted by lower case character code
        StringBuilder canonicalRequest = new StringBuilder();
        canonicalRequest.append("POST\n")
            .append(requestURI).append("\n")
            .append(queryString).append("\n")
            .append(ACCEPT_HEADER).append(":").append(contentType).append("\n")
            .append(CONTENT_HEADER).append(":").append(contentType).append("\n")
            .append(HOST_HEADER).append(":").append(host).append("\n")
            .append(XAMZDATE_HEADER).append(":").append(dateTimeString).append("\n")
            .append(XAMZTARGET_HEADER).append(":").append(serviceTarget).append("\n")
            .append("\n")
            .append(ACCEPT_HEADER).append(";").append(CONTENT_HEADER).append(";").append(HOST_HEADER).append(";").append(XAMZDATE_HEADER).append(";").append(XAMZTARGET_HEADER).append("\n")
            .append(payloadHash);
        return canonicalRequest.toString();
    }

    /**
     * Uses the previously calculated canonical request to create a single "String to Sign" for the request
     * http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
     * 
     * @param canonicalRequestHash - SHA256 hash of the canonical request
     * @param dateString - The short 8 digit format for an x-amz-date
     * 
     * @return The "String to Sign" used in Task 3
     */
    private static String buildStringToSign(String canonicalRequestHash, String dateString) {
        StringBuilder stringToSign = new StringBuilder();
        stringToSign.append(AWS_SHA256_ALGORITHM).append("\n")
            .append(dateTimeString).append("\n")
            .append(dateString).append("/").append(regionName).append("/").append(serviceName).append("/").append(TERMINATION_STRING).append("\n")
            .append(canonicalRequestHash);
        return stringToSign.toString();
    }

    /**
     * This function uses given parameters to create a derived key based on the secret key and parameters related to the call
     * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
     *
     * @param dateString - The short 8 digit format for an x-amz-date
     *
     * @return The derived key used in creating the final signature
     * 
     * @throws UnsupportedEncodingException 
     * @throws IllegalStateException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     */
    private static byte[] buildDerivedKey(String dateString) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
        StringBuilder signatureAWSKey = new StringBuilder();
        signatureAWSKey.append(KEY_QUALIFIER);
        signatureAWSKey.append(awsSecretKey);
        
        //Calculate the derived key from given values
        byte[] derivedKey = hmac(TERMINATION_STRING, 
                hmac(serviceName, 
                hmac(regionName, 
                hmac(dateString, signatureAWSKey.toString().getBytes(UTF8_CHARSET)))));
        return derivedKey;
    }
    /**
     * Calculates the signature to put in the POST message header 'Authorization'
     * http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
     * 
     * @param stringToSign - The entire "String to Sign" calculated in Task 2
     * @param dateString - The short 8 digit format for an x-amz-date
     * 
     * @return The whole field to be used in the Authorization header for the message
     * 
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     * @throws UnsupportedEncodingException
     */
    private static String buildAuthSignature(String stringToSign, String dateString) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
        //Use derived key and "String to Sign" to make the final signature
        byte[] derivedKey = buildDerivedKey(dateString);
        
        byte[] finalSignature = hmac(stringToSign.toString(), derivedKey);
                
        String signatureString = bytesToHex(finalSignature);
        StringBuilder authorizationValue = new StringBuilder();
        authorizationValue.append(AWS_SHA256_ALGORITHM)
            .append(" Credential=").append(awsKeyID).append("/")
            .append(dateString).append("/")
            .append(regionName).append("/")
            .append(serviceName).append("/")
            .append(TERMINATION_STRING).append(",")
            .append(" SignedHeaders=").append(ACCEPT_HEADER).append(";")
            .append(CONTENT_HEADER).append(";")
            .append(HOST_HEADER).append(";")
            .append(XAMZDATE_HEADER).append(";")
            .append(XAMZTARGET_HEADER).append(",")
            .append(" Signature=").append(signatureString);
        
        return authorizationValue.toString();
    }
    
    /**
     * Creates a printout of all information sent to the AGCOD service
     * 
     * @param payload - The payload sent to the AGCOD service
     * @param canonicalRequest - The entire canonical request calculated in Task 1
     * @param canonicalRequestHash - SHA256 hash of canonical request
     * @param stringToSign - The entire "String to Sign" calculated in Task 2
     * @param authorizationValue - The entire authorization calculated in Task 3
     * @param dateString - The short 8 digit format for an x-amz-date
     * 
     * @throws Exception
     */
    private static void printRequestInfo(StringBuilder payload, String canonicalRequest, String canonicalRequestHash, String stringToSign, String authorizationValue, String dateString) throws Exception {
      //Print everything to be sent:
        System.out.println("\nPAYLOAD:");
        System.out.println(payload.toString());
        System.out.println("\nHASHED PAYLOAD:");
        System.out.println(hash(payload.toString()));
        System.out.println("\nCANONICAL REQUEST:");
        System.out.println(canonicalRequest);
        System.out.println("\nHASHED CANONICAL REQUEST:");
        System.out.println(canonicalRequestHash);
        System.out.println("\nSTRING TO SIGN:");
        System.out.println(stringToSign);
        System.out.println("\nDERIVED SIGNING KEY:");
        System.out.println(bytesToHex(buildDerivedKey(dateString)));
        System.out.println("\nSIGNATURE:");
        
        //Check that the signature is moderately well formed to do string manipulation on
        if (authorizationValue.indexOf("Signature=") < 0 || authorizationValue.indexOf("Signature=") + 10 >= authorizationValue.length()) {
            throw new Exception("Malformed Signature");
        }
        
        //Get the text from after the word "Signature=" to the end of the authorization signature
        System.out.println(authorizationValue.substring(authorizationValue.indexOf("Signature=") + 10)); 
        System.out.println("\nENDPOINT:");
        System.out.println(host);
        System.out.println("\nSIGNED REQUEST");
        System.out.println("POST " + requestURI + " HTTP/1.1");
        System.out.println(ACCEPT_HEADER+":" + contentType);
        System.out.println(CONTENT_HEADER+":" + contentType);
        System.out.println(HOST_HEADER + ":" + host);
        System.out.println(XAMZDATE_HEADER + ":" + dateTimeString);
        System.out.println(XAMZTARGET_HEADER + ":" + serviceTarget.toString());
        System.out.println(AUTHORIZATION_HEADER + ":" + authorizationValue);
        System.out.println(payload.toString());
        
    }
    /**
     * Creates the authentication signature used with AWS v4 and sets the appropriate properties within the connection 
     * based on the parameters used for AWS signing. Tasks described below can be found at 
     * http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
     * 
     * @param conn - URL connection to host
     * @param payload - The payload to be used in the AWS signing that is sent to the AGCOD service
     * 
     * @throws Exception 
     */
    private static void signRequestAWSv4(URLConnection conn, StringBuilder payload) throws Exception {
        if (conn == null) {
            throw new ConnectException();
        }

        //If no date provided or the one provided is too short then create new x-amz-date.
        if (dateTimeString == null || dateTimeString.length() < 8) {
            dateTimeString = timestamp();
        }
        //Convert full date to x-amz-date by ignoring fields we don't need
        //dateString only needs digits for the year(4), month(2), and day(2).
        String dateString = dateTimeString.substring(0,8);

        //Set proper request properties for the connection, these correspond to what was used creating a canonical request
        //and the final Authorization
        conn.setRequestProperty(ACCEPT_HEADER, contentType);
        conn.setRequestProperty(CONTENT_HEADER, contentType);
        conn.setRequestProperty(HOST_HEADER, host);
        conn.setRequestProperty(XAMZDATE_HEADER, dateTimeString);
        conn.setRequestProperty(XAMZTARGET_HEADER, serviceTarget.toString());

        //Begin Task 1: Creating a Canonical Request
        String canonicalRequest = buildCanonicalRequest(payload);
        String canonicalRequestHash = hash(canonicalRequest);

        //Begin Task 2: Creating a String to Sign
        String stringToSign = buildStringToSign(canonicalRequestHash, dateString);

        //Begin Task 3: Creating a Signature 
        String authorizationValue = buildAuthSignature(stringToSign, dateString);

        //set final connection header
        conn.setRequestProperty(AUTHORIZATION_HEADER, authorizationValue);
               
        //Print everything to be sent:
        printRequestInfo(payload, canonicalRequest, canonicalRequestHash, stringToSign, authorizationValue, dateString);
    }

    /**
     * Sets the payload to be the requested encoding and creates the payload based on the static parameters. 
     * 
     * @param payload - The payload to be set that is sent to the AGCOD service
     * 
     */
    private static void setPayload(StringBuilder payload) throws IllegalArgumentException {
        //Set payload based on operation and format
        if (msgPayloadType == PayloadType.XML) {
            contentType = "charset=UTF-8";
            switch(serviceOperation) {
            case ActivateGiftCard: 
                payload.append("<ActivateGiftCardRequest><activationRequestId>").append(requestID)
                    .append("</activationRequestId> <partnerId>").append(partnerID)
                    .append("</partnerId><cardNumber>").append(cardNumber)
                    .append("</cardNumber><value><currencyCode>").append(currencyCode)
                    .append("</currencyCode><amount>").append(amount)
                    .append("</amount></value></ActivateGiftCardRequest>");
                break;
            case DeactivateGiftCard:
                payload.append("<DeactivateGiftCardRequest><activationRequestId>").append(requestID)
                    .append("</activationRequestId> <partnerId>").append(partnerID)
                    .append("</partnerId><cardNumber>").append(cardNumber)
                    .append("</cardNumber></DeactivateGiftCardRequest>");
                break;
            case ActivationStatusCheck: 
                payload.append("<ActivationStatusCheckRequest><statusCheckRequestId>").append(requestID)
                    .append("</statusCheckRequestId> <partnerId>").append(partnerID)
                    .append("</partnerId><cardNumber>").append(cardNumber)
                    .append("</cardNumber></ActivationStatusCheckRequest>");
                break;
            case CreateGiftCard: 
                payload.append("<CreateGiftCardRequest><creationRequestId>").append(requestID)
                    .append("</creationRequestId><partnerId>").append(partnerID)
                    .append("</partnerId><value><currencyCode>").append(currencyCode)
                    .append("</currencyCode><amount>").append(amount)
                    .append("</amount></value></CreateGiftCardRequest>");
                break;
            case CancelGiftCard: 
                payload.append("<CancelGiftCardRequest><creationRequestId>").append(requestID)
                    .append("</creationRequestId><partnerId>").append(partnerID)
                    .append("</partnerId><gcId>").append(gcId)
                    .append("</gcId></CancelGiftCardRequest>");
                break;
            case GetGiftCardActivityPage: 
                payload.append("<GetGiftCardActivityPageRequest><requestId>").append(requestID)
                    .append("</requestId> <partnerId>").append(partnerID)
                    .append("</partnerId><utcStartDate>").append(utcStartDate)
                    .append("</utcStartDate><utcEndDate>").append(utcEndDate)
                    .append("</utcEndDate><pageIndex>").append(pageIndex)
                    .append("</pageIndex><pageSize>").append(pageSize)
                    .append("</pageSize><showNoOps>").append(showNoOps)
                    .append("</showNoOps></GetGiftCardActivityPageRequest>");
                break;
            default:
                throw new IllegalArgumentException();
            }
        }
        else if (msgPayloadType == PayloadType.JSON) {
            contentType = "application/json";
            switch(serviceOperation) {
            case ActivateGiftCard: 
                payload.append("{\"activationRequestId\": \"").append(requestID)
                    .append("\", \"partnerId\": \"").append(partnerID)
                    .append("\", \"cardNumber\": \"").append(cardNumber)
                    .append("\", \"value\": {\"currencyCode\": \"").append(currencyCode)
                    .append("\", \"amount\": ").append(amount)
                    .append("}}");
                break;
            case DeactivateGiftCard:
                payload.append("{\"activationRequestId\": \"").append(requestID)
                    .append("\", \"partnerId\": \"").append(partnerID)
                    .append("\", \"cardNumber\": \"").append(cardNumber)
                    .append("\"}");
                break;
            case ActivationStatusCheck: 
                payload.append("{\"statusCheckRequestId\": \"").append(requestID)
                    .append("\", \"partnerId\": \"").append(partnerID)
                    .append("\", \"cardNumber\": \"").append(cardNumber)
                    .append("\"}");
                break;
            case CreateGiftCard: 
                payload.append("{\"creationRequestId\": \"").append(requestID)
                    .append("\", \"partnerId\": \"").append(partnerID)
                    .append("\", \"value\": {\"currencyCode\": \"").append(currencyCode)
                    .append("\", \"amount\": ").append(amount)
                    .append("}}");
                break;
            case CancelGiftCard: 
                payload.append("{\"creationRequestId\": \"").append(requestID)
                    .append("\", \"partnerId\": \"").append(partnerID)
                    .append("\", \"gcId\": \"").append(gcId)
                    .append("\"}");
                break;
            case GetGiftCardActivityPage: 
                payload.append("{\"requestId\": \"").append(requestID)
                .append("\", \"partnerId\": \"").append(partnerID)
                .append("\", \"utcStartDate\": \"").append(utcStartDate)
                .append("\", \"utcEndDate\": \"").append(utcEndDate)
                .append("\", \"pageIndex\": ").append(pageIndex)
                .append(", \"pageSize\": ").append(pageSize)
                .append(", \"showNoOps\": \"").append(showNoOps)
                .append("\"}");
                break;
            default:
                throw new IllegalArgumentException();
            }
        }
        else {
            throw new IllegalArgumentException();
        };
    }

    /**
     * Used to hash the payload and hash each previous step in the AWS signing process
     * 
     * @param toHash - String to be hashed
     * 
     * @return SHA256 hashed version of the input
     * 
     * @throws NoSuchAlgorithmException 
     * @throws UnsupportedEncodingException 
     */
    private static String hash(String toHash) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        String result = null;
        MessageDigest md = MessageDigest.getInstance(HASH_SHA256_ALGORITHM);
        md.update(toHash.getBytes(UTF8_CHARSET));
        byte[] hashed = md.digest();
        result = bytesToHex(hashed);
        return result;
    }
    /**
     * Used to create a series of Hash-based Message Authentication Codes for use in the final signature
     * 
     * @param data - String to be Hashed
     * @param key - Key used in signing
     * 
     * @return Byte string of resultant hash
     * 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws UnsupportedEncodingException 
     * @throws IllegalStateException 
     */
    private static byte[] hmac(String data, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {  
        byte[] result = null;
        Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        mac.init(new SecretKeySpec(key, HMAC_SHA256_ALGORITHM));
        result = mac.doFinal(data.getBytes(UTF8_CHARSET));
        return result;
    }

    /**
     * Create timestamp for current time
     * 
     * @return Stamp of current time
     */
    private static String timestamp() {
        String timestamp = null;
        Calendar cal = Calendar.getInstance();
        DateFormat dfm = new SimpleDateFormat(DATE_FORMAT);
        dfm.setTimeZone(TimeZone.getTimeZone(DATE_TIMEZONE));
        timestamp = dfm.format(cal.getTime());
        return timestamp;
    }
    /**
     * Gets the lowercase hexadecimal representation of a byte array
     * 
     * @param data - byte array to be converted
     * 
     * @return Hex string representing input
     */
    private static String bytesToHex(byte[] data) {
        StringBuilder sb = new StringBuilder();
        for (byte b : data) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
