fi.cosky.sdk.API.java Source code

Java tutorial

Introduction

Here is the source code for fi.cosky.sdk.API.java

Source

package fi.cosky.sdk;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringTokenizer;

import com.google.gson.*;

import org.apache.commons.codec.binary.Base64;

/*
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE.txt', which is part of this source code package.
 */

/**
 * API-class to handle the communication between SDK-user and CO-SKY
 * optimization's REST-API.
 */
public class API {
    private String baseUrl;
    private String ClientKey;
    private String ClientSecret;
    private ApiData apiData;
    private TokenData tokenData;
    private boolean timed;
    private ObjectCache objectCache;
    private boolean retry;
    private boolean useMimeTypes;
    private MimeTypeHelper helper;

    private static int RETRY_WAIT_TIME = 2000;

    static Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'").create();

    public API(String baseUrl) {
        this.baseUrl = baseUrl;
        this.objectCache = new ObjectCache();
        this.timed = false;
        this.retry = true;
        this.useMimeTypes = true; //change this when production will support mimetypes.
        this.helper = new MimeTypeHelper();

        //Delete-Verb causes connection to keep something somewhere that causes the next request to fail.
        //this hopefully helps with that.
        System.setProperty("http.keepAlive", "false");
    }

    private boolean authenticate() {
        return authenticate(this.ClientKey, this.ClientSecret);
    }

    public boolean authenticate(String username, String password) {
        this.ClientKey = username;
        this.ClientSecret = password;
        System.out.println("Authenticating API with username: " + username + " and pass: " + password);
        try {
            ResponseData result = navigate(ResponseData.class, getAuthLink());

            if (result == null || result.getItems() != null) {
                System.out.println(
                        "Could not authenticate, please check credentials and service status from http://status.nfleet.fi");
                return false;
            }

            TokenData authenticationData = navigate(TokenData.class, result.getLocation());

            this.tokenData = authenticationData;
        } catch (Exception e) {
            System.out.println(e.toString());
            return false;
        }
        return true;
    }

    /**
     * 
     * @param tClass return type.
     * @param l navigation link
     * @return object of type tClass
     * @throws  NFleetRequestException when there is data validation problems 
     * @throws IOException when there is problems with infrastructure (connection etc..)
     */
    public <T extends BaseData> T navigate(Class<T> tClass, Link l) throws IOException {
        return navigate(tClass, l, null);
    }

    @SuppressWarnings("unchecked")
    public <T extends BaseData> T navigate(Class<T> tClass, Link l, HashMap<String, String> queryParameters)
            throws IOException {
        Object result;
        retry = true;
        long start = 0;
        long end;

        if (isTimed()) {
            start = System.currentTimeMillis();
        }

        if (tClass.equals(TokenData.class)) {
            result = sendRequest(l, tClass, null);
            return (T) result;
        }

        if (l.getRel().equals("authenticate")) {
            HashMap<String, String> headers = new HashMap<String, String>();
            String authorization = "Basic "
                    + Base64.encodeBase64String((this.ClientKey + ":" + this.ClientSecret).getBytes());
            headers.put("authorization", authorization);
            result = sendRequestWithAddedHeaders(Verb.POST, this.baseUrl + l.getUri(), tClass, null, headers);
            return (T) result;
        }

        String uri = l.getUri();
        if (l.getMethod().equals("GET") && queryParameters != null && !queryParameters.isEmpty()) {
            StringBuilder sb = new StringBuilder(uri + "?");

            for (String key : queryParameters.keySet()) {
                sb.append(key + "=" + queryParameters.get(key) + "&");
            }
            sb.deleteCharAt(sb.length() - 1);
            uri = sb.toString();
        }

        if (l.getMethod().equals("GET") && !uri.contains(":")) {
            result = sendRequest(l, tClass, null);
        } else if (l.getMethod().equals("PUT")) {
            result = sendRequest(l, tClass, null);
        } else if (l.getMethod().equals("POST")) {
            result = sendRequest(l, tClass, null);
        } else if (l.getMethod().equals("DELETE")) {
            result = sendRequest(l, tClass, new Object());
        } else {
            result = sendRequest(l, tClass, null);
        }
        if (isTimed()) {
            end = System.currentTimeMillis();
            long time = end - start;
            System.out.println("Method " + l.getMethod() + " on " + l.getUri() + " doing " + l.getRel() + " took "
                    + time + " ms.");
        }
        return (T) result;
    }

    /**
     * Navigate method for sending data
     * @param tClass    return type.
     * @param l       navigation link
     * @param object   object to send
     * @return          object of type tClass
     * @throws    NFleetRequestException when there is problems with data validation, exception contains list of the errors.
     * @throws   IOException  when there is problems with infrastructure ( connection etc.. )
     */
    @SuppressWarnings("unchecked")
    public <T extends BaseData> T navigate(Class<T> tClass, Link l, Object object) throws IOException {
        long start = 0;
        long end;

        if (isTimed()) {
            start = System.currentTimeMillis();
        }

        Object result = sendRequest(l, tClass, object);

        if (isTimed()) {
            end = System.currentTimeMillis();
            long time = end - start;
            System.out.println("Method " + l.getMethod() + " on " + l.getUri() + " took " + time + " ms.");
        }
        return (T) result;
    }

    public Link getRoot() {
        return new Link("self", baseUrl, "GET", "", true);
    }

    public String getBaseUrl() {
        return baseUrl;
    }

    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    private <T extends BaseData> T sendRequest(Link l, Class<T> tClass, Object object) throws IOException {
        URL serverAddress;
        BufferedReader br;
        String result = "";
        HttpURLConnection connection = null;
        String url = l.getUri().contains("://") ? l.getUri() : this.baseUrl + l.getUri();
        try {
            String method = l.getMethod();
            String type = l.getType();

            serverAddress = new URL(url);
            connection = (HttpURLConnection) serverAddress.openConnection();
            boolean doOutput = doOutput(method);
            connection.setDoOutput(doOutput);
            connection.setRequestMethod(method);
            connection.setInstanceFollowRedirects(false);

            if (method.equals("GET") && useMimeTypes)
                if (type == null || type.equals("")) {
                    addMimeTypeAcceptToRequest(object, tClass, connection);
                } else {
                    connection.addRequestProperty("Accept", helper.getSupportedType(type));
                }
            if (!useMimeTypes)
                connection.setRequestProperty("Accept", "application/json");

            if (doOutput && useMimeTypes) {
                //this handles the case if the link is self made and the type field has not been set.
                if (type == null || type.equals("")) {
                    addMimeTypeContentTypeToRequest(l, tClass, connection);
                    addMimeTypeAcceptToRequest(l, tClass, connection);
                } else {
                    connection.addRequestProperty("Accept", helper.getSupportedType(type));
                    connection.addRequestProperty("Content-Type", helper.getSupportedType(type));
                }
            }

            if (!useMimeTypes)
                connection.setRequestProperty("Content-Type", "application/json");

            if (tokenData != null) {
                connection.addRequestProperty("Authorization",
                        tokenData.getTokenType() + " " + tokenData.getAccessToken());
            }

            addVersionNumberToHeader(object, url, connection);

            if (method.equals("POST") || method.equals("PUT")) {
                String json = object != null ? gson.toJson(object) : ""; //should handle the case when POST without object.
                connection.addRequestProperty("Content-Length", json.getBytes("UTF-8").length + "");
                OutputStreamWriter osw = new OutputStreamWriter(connection.getOutputStream());
                osw.write(json);
                osw.flush();
                osw.close();
            }

            connection.connect();

            if (connection.getResponseCode() == HttpURLConnection.HTTP_CREATED
                    || connection.getResponseCode() == HttpURLConnection.HTTP_SEE_OTHER) {
                ResponseData data = new ResponseData();
                Link link = parseLocationLinkFromString(connection.getHeaderField("Location"));
                link.setType(type);
                data.setLocation(link);
                connection.disconnect();
                return (T) data;
            }

            if (connection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                System.out.println(
                        "Authentication expired " + connection.getResponseMessage() + " trying to reauthenticate");
                if (retry && this.tokenData != null) {
                    this.tokenData = null;
                    retry = false;
                    if (authenticate()) {
                        System.out.println("Reauthentication success, will continue with " + l.getMethod()
                                + " request on " + l.getRel());
                        return sendRequest(l, tClass, object);
                    }
                } else
                    throw new IOException(
                            "Tried to reauthenticate but failed, please check the credentials and status of NFleet-API");
            }

            if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
                return (T) objectCache.getObject(url);
            }

            if (connection.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
                return (T) new ResponseData();
            }

            if (connection.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST
                    && connection.getResponseCode() < HttpURLConnection.HTTP_INTERNAL_ERROR) {
                System.out.println("ErrorCode: " + connection.getResponseCode() + " "
                        + connection.getResponseMessage() + " " + url + ", verb: " + method);

                String errorString = readErrorStreamAndCloseConnection(connection);
                throw (NFleetRequestException) gson.fromJson(errorString, NFleetRequestException.class);
            } else if (connection.getResponseCode() >= HttpURLConnection.HTTP_INTERNAL_ERROR) {
                if (retry) {
                    System.out.println("Request caused internal server error, waiting " + RETRY_WAIT_TIME
                            + " ms and trying again.");
                    return waitAndRetry(connection, l, tClass, object);
                } else {
                    System.out.println("Requst caused internal server error, please contact dev@nfleet.fi");
                    String errorString = readErrorStreamAndCloseConnection(connection);
                    throw new IOException(errorString);
                }
            }

            if (connection.getResponseCode() >= HttpURLConnection.HTTP_BAD_GATEWAY) {
                if (retry) {
                    System.out.println("Could not connect to NFleet-API, waiting " + RETRY_WAIT_TIME
                            + " ms and trying again.");
                    return waitAndRetry(connection, l, tClass, object);
                } else {
                    System.out.println(
                            "Could not connect to NFleet-API, please check service status from http://status.nfleet.fi and try again later.");
                    String errorString = readErrorStreamAndCloseConnection(connection);
                    throw new IOException(errorString);
                }

            }

            result = readDataFromConnection(connection);

        } catch (MalformedURLException e) {
            throw e;
        } catch (ProtocolException e) {
            throw e;
        } catch (UnsupportedEncodingException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        } catch (SecurityException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw e;
        } finally {
            assert connection != null;
            connection.disconnect();
        }
        Object newEntity = gson.fromJson(result, tClass);
        objectCache.addUri(url, newEntity);
        return (T) newEntity;
    }

    private <T extends BaseData> T sendRequestWithAddedHeaders(Verb verb, String url, Class<T> tClass,
            Object object, HashMap<String, String> headers) throws IOException {
        URL serverAddress;
        HttpURLConnection connection;
        BufferedReader br;
        String result = "";
        try {
            serverAddress = new URL(url);
            connection = (HttpURLConnection) serverAddress.openConnection();
            connection.setInstanceFollowRedirects(false);
            boolean doOutput = doOutput(verb);
            connection.setDoOutput(doOutput);
            connection.setRequestMethod(method(verb));
            connection.setRequestProperty("Authorization", headers.get("authorization"));
            connection.addRequestProperty("Accept", "application/json");

            if (doOutput) {
                connection.addRequestProperty("Content-Length", "0");
                OutputStreamWriter os = new OutputStreamWriter(connection.getOutputStream());
                os.write("");
                os.flush();
                os.close();
            }
            connection.connect();

            if (connection.getResponseCode() == HttpURLConnection.HTTP_SEE_OTHER
                    || connection.getResponseCode() == HttpURLConnection.HTTP_CREATED) {
                Link location = parseLocationLinkFromString(connection.getHeaderField("Location"));
                Link l = new Link("self", "/tokens", "GET", "", true);
                ArrayList<Link> links = new ArrayList<Link>();
                links.add(l);
                links.add(location);
                ResponseData data = new ResponseData();
                data.setLocation(location);
                data.setLinks(links);
                return (T) data;
            }

            if (connection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                System.out.println("Authentication expired: " + connection.getResponseMessage());
                if (retry && this.tokenData != null) {
                    retry = false;
                    this.tokenData = null;
                    if (authenticate()) {
                        System.out.println(
                                "Reauthentication success, will continue with " + verb + " request on " + url);
                        return sendRequestWithAddedHeaders(verb, url, tClass, object, headers);
                    }
                } else
                    throw new IOException(
                            "Tried to reauthenticate but failed, please check the credentials and status of NFleet-API");
            }

            if (connection.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST
                    && connection.getResponseCode() < HttpURLConnection.HTTP_INTERNAL_ERROR) {
                System.out.println("ErrorCode: " + connection.getResponseCode() + " "
                        + connection.getResponseMessage() + " " + url + ", verb: " + verb);

                String errorString = readErrorStreamAndCloseConnection(connection);
                throw (NFleetRequestException) gson.fromJson(errorString, NFleetRequestException.class);
            } else if (connection.getResponseCode() >= HttpURLConnection.HTTP_INTERNAL_ERROR) {
                if (retry) {
                    System.out.println("Server responded with internal server error, trying again in "
                            + RETRY_WAIT_TIME + " msec.");
                    try {
                        retry = false;
                        Thread.sleep(RETRY_WAIT_TIME);
                        return sendRequestWithAddedHeaders(verb, url, tClass, object, headers);
                    } catch (InterruptedException e) {

                    }
                } else {
                    System.out.println("Server responded with internal server error, please contact dev@nfleet.fi");
                }

                String errorString = readErrorStreamAndCloseConnection(connection);
                throw new IOException(errorString);
            }

            result = readDataFromConnection(connection);
        } catch (MalformedURLException e) {
            throw e;
        } catch (ProtocolException e) {
            throw e;
        } catch (UnsupportedEncodingException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        }
        return (T) gson.fromJson(result, tClass);
    }

    private Link getAuthLink() {
        return new Link("authenticate", "/tokens", "POST", "", true);
    }

    private String method(Verb verb) {
        switch (verb) {
        case GET:
            return "GET";
        case PUT:
            return "PUT";
        case POST:
            return "POST";
        case DELETE:
            return "DELETE";
        case PATCH:
            return "PATCH";
        }
        return "";
    }

    private Link parseLocationLinkFromString(String s) {
        if (!s.contains("/tokens"))
            s = s.substring(s.lastIndexOf("/users"));
        return new Link("location", s, "GET", "", true);
    }

    private boolean doOutput(Verb verb) {
        switch (verb) {
        case GET:
        case DELETE:
            return false;
        default:
            return true;
        }
    }

    private boolean doOutput(String verb) {
        return (verb.equals("POST") || verb.equals("PUT") || verb.equals("PATCH"));
    }

    private enum Verb {
        GET, PUT, POST, DELETE, PATCH
    }

    private <T> void addMimeTypeAcceptToRequest(Object object, Class<T> tClass, HttpURLConnection connection) {
        Field f = null;
        StringTokenizer st = null;
        StringBuilder sb = null;
        Field[] fields = null;
        try {
            fields = object != null ? fields = object.getClass().getDeclaredFields() : tClass.getDeclaredFields();

            for (Field field : fields) {
                if (field.getName().equals("MimeType"))
                    f = field;
            }
            if (f == null) {
                connection.setRequestProperty("Accept", "application/json");
                return;
            }

            String type = null;

            f.setAccessible(true);
            type = f.get(tClass).toString();

            if (type != null) {
                connection.addRequestProperty("Accept", helper.getSupportedType(type));
            }
        } catch (Exception e) {

        }
    }

    private <T> void addMimeTypeContentTypeToRequest(Object object, Class<T> tClass, HttpURLConnection connection) {
        Field f = null;
        StringTokenizer st = null;
        StringBuilder sb = null;
        Field[] fields = null;
        try {
            fields = object != null ? fields = object.getClass().getDeclaredFields() : tClass.getDeclaredFields();

            for (Field field : fields) {
                if (field.getName().equals("MimeType"))
                    f = field;
            }

            if (f == null) {
                connection.setRequestProperty("Content-Type", "application/json");
                return;
            }

            String type = null;

            f.setAccessible(true);
            type = f.get(object).toString();

            if (type != null) {
                connection.addRequestProperty("Content-Type", helper.getSupportedType(type));
            }
        } catch (Exception e) {

        }
    }

    private void addVersionNumberToHeader(Object object, String url, HttpURLConnection connection) {
        Field f = null;
        Object fromCache = null;
        if (objectCache.containsUri(url)) {
            try {
                fromCache = objectCache.getObject(url);
                f = fromCache.getClass().getDeclaredField("VersionNumber");
            } catch (NoSuchFieldException e) {
                // Object does not have versionnumber.
            }
            if (f != null) {
                f.setAccessible(true);
                int versionNumber = 0;
                try {
                    versionNumber = f.getInt(fromCache);
                } catch (IllegalAccessException e) {

                }
                connection.setRequestProperty("If-None-Match", versionNumber + "");
            }
        }
    }

    private String readErrorStreamAndCloseConnection(HttpURLConnection connection) {
        InputStream stream = connection.getErrorStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(stream));
        StringBuilder sb = new StringBuilder();
        String s;
        try {
            while ((s = br.readLine()) != null) {
                sb.append(s);
            }
        } catch (IOException e) {
            System.out.println("Could not read error stream from the connection.");
        } finally {
            connection.disconnect();
        }
        return sb.toString();
    }

    private String readDataFromConnection(HttpURLConnection connection) {
        InputStream is = null;
        BufferedReader br = null;
        StringBuilder sb = null;
        try {
            is = connection.getInputStream();
            br = new BufferedReader(new InputStreamReader(is));

            sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            String eTag = null;
            if ((eTag = connection.getHeaderField("ETag")) != null) {
                sb.insert(sb.lastIndexOf("}"), ",\"VersionNumber\":" + eTag + "");
            }
        } catch (IOException e) {
            System.out.println("Could not read data from connection");
        }
        return sb.toString();
    }

    private <T extends BaseData> T waitAndRetry(HttpURLConnection connection, Link l, Class<T> tClass,
            Object object) {
        try {
            retry = false;
            Thread.sleep(RETRY_WAIT_TIME);
            return sendRequest(l, tClass, object);
        } catch (InterruptedException e) {
            return null;
        } catch (IOException e) {
            return null;
        }
    }

    public TokenData getTokenData() {
        return this.tokenData;
    }

    public void setTokenData(TokenData data) {
        tokenData = data;
    }

    public ApiData getApiData() {
        return apiData;
    }

    public void setApiData(ApiData apiData) {
        this.apiData = apiData;
    }

    public boolean isTimed() {
        return timed;
    }

    public void setTimed(boolean timed) {
        this.timed = timed;
    }
}