CloudManagerAPI.java Source code

Java tutorial

Introduction

Here is the source code for CloudManagerAPI.java

Source

/**
 * TargetsAPI shows a simple example how to interact with the Wikitude Cloud
 * Targets API.
 *
 * This example is published under Apache License, Version 2.0
 * http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * @author Wikitude
 *
 */

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;

import javax.net.ssl.HttpsURLConnection;

// please find library at https://code.google.com/p/org-json-java/downloads/list and include in your project's build path
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class CloudManagerAPI {
    public class APIException extends Exception {

        /**
         *
         */
        private static final long serialVersionUID = 1L;

        private final int code;

        public APIException(String message, int code) {
            super(message);
            this.code = code;
        }

        public String getMessage() {
            final String message = super.getMessage();

            return String.format("(%s): %s", code, message);
        }

        public int getCode() {
            return this.code;
        }
    }

    public class ServiceException extends APIException {

        /**
         *
         */
        private static final long serialVersionUID = 1L;

        private final String reason;

        public ServiceException(String message, int code, String reason) {
            super(message, code);
            this.reason = reason;
        }

        public String getMessage() {
            final String message = super.getMessage();
            final int code = this.getCode();

            return String.format("%s (%s): %s", reason, code, message);
        }

        public String getReason() {
            return this.reason;
        }

    }

    // The endpoint where the Wikitude Cloud Targets API resides.
    // *********************************************************************
    // ***************************** IMPORTANT *****************************
    // *********************************************************************
    // In case you get the following error when running the code:
    // "unable to find valid certification path to requested target",
    // your JVM does not recognize the SSL Certificate Authority (CA) Wikitude
    // is using. In this case, you either need to switch to http connections
    // (which causes your API Token to be transmitted in plain text over the
    // net), or you add the certificate to your certificate store. A tutorial
    // how to do that can be found at
    // https://blogs.oracle.com/gc/entry/unable_to_find_valid_certification
    private static final String API_ENDPOINT_ROOT = "https://api.wikitude.com";
    private static final int API_DEFAULT_POLL_INTERVAL = 10000;

    private static final String PLACEHOLDER_TC_ID = "${TC_ID}";
    private static final String PLACEHOLDER_TARGET_ID = "${TARGET_ID}";

    private static final String PATH_ADD_TC = "/cloudrecognition/targetCollection";
    private static final String PATH_GET_TC = "/cloudrecognition/targetCollection/" + PLACEHOLDER_TC_ID;
    private static final String PATH_GENERATE_TC = "/cloudrecognition/targetCollection/" + PLACEHOLDER_TC_ID
            + "/generation/cloudarchive";

    private static final String PATH_ADD_TARGET = "/cloudrecognition/targetCollection/" + PLACEHOLDER_TC_ID
            + "/target";
    private static final String PATH_ADD_TARGETS = "/cloudrecognition/targetCollection/" + PLACEHOLDER_TC_ID
            + "/targets";
    private static final String PATH_GET_TARGET = "/cloudrecognition/targetCollection/" + PLACEHOLDER_TC_ID
            + "/target/" + PLACEHOLDER_TARGET_ID;

    private static final String HEADER_KEY_TOKEN = "X-Token";
    private static final String HEADER_KEY_VERSION = "X-Version";

    private static final String STATUS_COMPLETED = "COMPLETED";

    // The token to use when connecting to the endpoint
    private final String token;
    // The version of the API we will use
    private final int version;
    // interval used to poll status of asynchronous operations
    private final int pollInterval;

    private enum Method {
        GET, POST, DELETE
    }

    /**
     * Creates a new TargetsAPI object that offers the service to interact with
     * the Wikitude Cloud Targets API.
     *
     * @param token
     *            The token to use when connecting to the endpoint
     * @param version
     *            The version of the API we will use
     */
    public CloudManagerAPI(String token, int version) {
        this(token, version, API_DEFAULT_POLL_INTERVAL);
    }

    /**
     * Creates a new TargetsAPI object that offers the service to interact with
     * the Wikitude Cloud Targets API.
     *
     * @param token
     *            The token to use when connecting to the endpoint
     * @param version
     *            The version of the API we will use
     * @param pollInterval
     *            The interval for polling asynchronous endpoints
     */
    public CloudManagerAPI(String token, int version, int pollInterval) {
        this.token = token;
        this.version = version;
        this.pollInterval = pollInterval;
    }

    /**
     * Create target Collection with given name.
     * @param tcName target collection's name. Note that response contains an "id" attribute, which acts as unique identifier
     * @return JSON representation of the created empty target collection
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject createTargetCollection(final String tcName) throws IOException, JSONException, APIException {
        final JSONObject tcJSONObject = new JSONObject();
        tcJSONObject.put("name", tcName);
        final String response = sendRequest(Method.POST, PATH_ADD_TC, tcJSONObject);

        return new JSONObject(response);
    }

    /**
     * Retrieve all created and active target collections
     * @return JSONArray containing JSONObjects of all targetCollection that were created
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONArray getAllTargetCollections() throws IOException, JSONException, APIException {
        final String response = this.sendRequest(Method.GET, PATH_ADD_TC);

        return new JSONArray(response);
    }

    /**
     * Rename existing target collection
     * @param tcId id of target collection
     * @param newName new name to use for this target collection
     * @return the updated JSON representation of the modified target collection
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject renameTargetCollection(final String tcId, final String newName)
            throws IOException, JSONException, APIException {
        final JSONObject tcJSONObject = new JSONObject();
        tcJSONObject.put("name", newName);

        final String path = PATH_GET_TC.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));
        final String responseString = this.sendRequest(Method.POST, path, tcJSONObject);
        return new JSONObject(responseString);
    }

    /**
     * Receive JSON representation of existing target collection (without making any modifications)
     * @param tcId id of the target collection
     * @return JSON representation of target collection
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject getTargetCollection(final String tcId) throws IOException, JSONException, APIException {
        final String path = PATH_GET_TC.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));
        final String responseString = this.sendRequest(Method.GET, path);

        return new JSONObject(responseString);
    }

    /**
     * deletes existing target collection by id (NOT name)
     * @param tcId id of target collection
     * @return true on successful deletion, false otherwise
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public boolean deleteTargetCollection(final String tcId) throws IOException, JSONException, APIException {
        final String path = PATH_GET_TC.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));
        this.sendRequest(Method.DELETE, path);

        return true;
    }

    /**
     * Receive target collection's target images
     * @param tcId id of target collection
     * @return JSONArray of targets within given target collectino
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONArray getAllTargets(final String tcId) throws IOException, JSONException, APIException {
        final String path = PATH_ADD_TARGET.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));
        final String responseString = this.sendRequest(Method.GET, path);

        return new JSONArray(responseString);
    }

    /**
     * adds a target to an existing target collection
     * @param tcId id of target collection
     * @param target JSON representation of target, e.g. {"name" : "foo", "imageUrl": "http://myserver.com/path/img.jpg"}
     * @return JSON representation of created target (includes unique "id"-attribute)
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject addTarget(final String tcId, final JSONObject target)
            throws IOException, JSONException, APIException {
        final String path = PATH_ADD_TARGET.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));
        final String responseString = this.sendRequest(Method.POST, path, target);

        return new JSONObject(responseString);
    }

    /**
     * adds multiple targets to an existing target collection
     * @param tcId id of target collection
     * @param targets JSON representation of targets, e.g. {"name" : "foo", "imageUrl": "http://myserver.com/path/img.jpg"}
     * @return JSON representation of the status of the operation
     *      Note: this method will wait until the operation is finished, depending on the amount of targets this
     *      operation may take seconds to minutes
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     * @throws InterruptedException thrown in case polling is interrupted
     */
    public JSONObject addTargets(final String tcId, final JSONArray targets)
            throws IOException, JSONException, APIException, InterruptedException {
        final String path = PATH_ADD_TARGETS.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));

        return this.sendAsyncRequest(Method.POST, path, targets);
    }

    /**
     * Get target JSON of existing targetId and targetCollectionId
     * @param tcId id of target collection
     * @param targetId id of target
     * @return JSON representation of target
     * @throws FileNotFoundException in case target does not exist
     * @throws UnsupportedEncodingException in case utf-8 encoder is not possible in your JRE
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject getTarget(final String tcId, final String targetId)
            throws IOException, JSONException, APIException {
        final String path = PATH_GET_TARGET.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"))
                .replace(PLACEHOLDER_TARGET_ID, URLEncoder.encode(targetId, "UTF-8"));
        final String responseString = this.sendRequest(Method.GET, path);

        return new JSONObject(responseString);
    }

    /**
     * Update target JSON properties of existing targetId and targetCollectionId
     * @param tcId id of target collection
     * @param targetId id of target
     * @param target JSON representation of the target's properties that shall be updated, e.g. { "physicalHeight": 200 }
     * @return JSON representation of target as an array
     * @throws FileNotFoundException in case target does not exist
     * @throws UnsupportedEncodingException in case utf-8 encoder is not possible in your JRE
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public JSONObject updateTarget(final String tcId, final String targetId, final JSONObject target)
            throws IOException, JSONException, APIException {
        final String path = PATH_GET_TARGET.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"))
                .replace(PLACEHOLDER_TARGET_ID, URLEncoder.encode(targetId, "UTF-8"));
        final String responseString = this.sendRequest(Method.POST, path, target);

        return new JSONObject(responseString);
    }

    /**
     * Delete existing target from a collection
     * @param tcId id of target collection
     * @param targetId id of target
     * @return true after successful deletion
     * @throws FileNotFoundException in case target does not exist
     * @throws UnsupportedEncodingException in case utf-8 encoder is not possible in your JRE
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     */
    public boolean deleteTarget(final String tcId, final String targetId)
            throws IOException, JSONException, APIException {
        final String requestUrl = PATH_GET_TARGET.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"))
                .replace(PLACEHOLDER_TARGET_ID, URLEncoder.encode(targetId, "UTF-8"));
        this.sendRequest(Method.DELETE, requestUrl);

        return true;
    }

    /***
     * Gives command to start generation of given target collection. Note: Added targets will only be analized after generation.
     * @param tcId id of target collection
     * @return JSON representation of the status of the operation
     *      Note: this method will wait until the operation is finished, depending on the amount of targets this
     *      operation may take seconds to minutes
     * @throws FileNotFoundException in case target does not exist
     * @throws UnsupportedEncodingException in case utf-8 encoder is not possible in your JRE
     * @throws IOException thrown in case of network problems
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     * @throws InterruptedException thrown in case polling is interrupted
     */
    public JSONObject generateTargetCollection(final String tcId)
            throws IOException, JSONException, APIException, InterruptedException {
        final String requestUrl = PATH_GENERATE_TC.replace(PLACEHOLDER_TC_ID, URLEncoder.encode(tcId, "UTF-8"));

        return sendAsyncRequest(Method.POST, requestUrl);
    }

    /**
     * HELPER method to send request to the Wikitude Cloud Targets API.
     *
     * <b>Remark</b>: We are not using any external libraries for sending HTTP
     * requests, to be as independent as possible. Libraries like Apache
     * HttpComponents make it a lot easier to interact with HTTP connections.
     *
     * @param method
     *            the HTTP-method which will be used when sending the request
     * @param path
     *          the path to the service which is defined in the private variables
     * @param payload
     *          The JSONObject to send in body, set null if none should be used
     * @return The response from the server, in JSON format
     *
     * @throws IOException
     *             when the server cannot serve the request for any reason, or
     *             anything went wrong during the communication between client
     *             and server.
     * @throws JSONException thrown in case server response is no valid JSON
     * @throws APIException thrown in case service responds with an error
     *
     */
    private <TPayload> String sendRequest(final Method method, final String path, final TPayload payload)
            throws IOException, JSONException, APIException {
        HttpURLConnection connection = sendAPIRequest(method, path, payload);
        String response = null;

        if (hasJsonContent(connection)) {
            response = readInput(connection);
        }

        return response;
    }

    private String sendRequest(final Method method, final String path)
            throws IOException, JSONException, APIException {
        return sendRequest(method, path, null);
    }

    private <TPayload> HttpURLConnection sendAPIRequest(final Method method, final String path,
            final TPayload payload) throws IOException, JSONException, APIException {
        URL url = new URL(API_ENDPOINT_ROOT + path);

        HttpURLConnection connection = openConnection(url, method);

        // append JSON body, if set
        if (payload != null) {
            writePayload(connection, payload.toString());
        }

        if (isResponseSuccess(connection)) {
            return connection;
        } else {
            throw readAPIException(connection);
        }
    }

    private HttpURLConnection openConnection(final URL url, final Method method) throws IOException {
        // open the connection
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // use supplied method and configure the connection
        connection.setRequestMethod(method.toString());
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setUseCaches(false);

        // set the request headers
        connection.setRequestProperty(HEADER_KEY_TOKEN, token);
        connection.setRequestProperty(HEADER_KEY_VERSION, "" + version);

        return connection;
    }

    private void writePayload(HttpURLConnection connection, final String payload) throws IOException {
        OutputStreamWriter writer = null;
        try {
            final String contentLength = String.valueOf(payload.length());
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Content-Length", contentLength);

            // construct the writer and write request
            writer = new OutputStreamWriter(connection.getOutputStream());
            writer.write(payload);
            writer.flush();
        } finally {
            closeStream(writer);
        }
    }

    private void closeStream(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (Exception e) {
            // intentionally left blank
        }
    }

    private boolean isResponseSuccess(HttpURLConnection connection) throws IOException {
        int statusCode = connection.getResponseCode();

        return statusCode == HttpsURLConnection.HTTP_OK || statusCode == HttpsURLConnection.HTTP_ACCEPTED
                || statusCode == HttpURLConnection.HTTP_NO_CONTENT;
    }

    private String readInput(HttpURLConnection connection) throws IOException {
        return readBody(connection.getInputStream());
    }

    private String readBody(InputStream inputStream) throws IOException {
        BufferedReader reader = null;
        String response = null;
        try {
            // listen on the server response
            reader = new BufferedReader(new InputStreamReader(inputStream));
            // construct the server response and return
            StringBuilder sb = new StringBuilder();
            for (String line; (line = reader.readLine()) != null;) {
                sb.append(line);
            }
            response = sb.toString();
        } finally {
            closeStream(reader);
        }

        return response;
    }

    private APIException readAPIException(HttpURLConnection connection) throws IOException, JSONException {
        if (hasJsonContent(connection)) {
            return readServiceException(connection);
        } else {
            return readGeneralError(connection);
        }
    }

    private boolean hasJsonContent(HttpURLConnection connection) {
        final String contentType = connection.getHeaderField("Content-Type");
        final String contentLength = connection.getHeaderField("Content-Length");

        return "application/json".equals(contentType) && !"0".equals(contentLength);
    }

    private APIException readServiceException(HttpURLConnection connection) throws IOException, JSONException {
        final String strError = readError(connection);
        final JSONObject error = new JSONObject(strError);
        final int code = error.getInt("code");
        final String reason = error.getString("reason");
        final String message = error.getString("message");

        return new ServiceException(message, code, reason);
    }

    private String readError(HttpURLConnection connection) throws IOException {
        return readBody(connection.getErrorStream());
    }

    private APIException readGeneralError(HttpURLConnection connection) throws IOException, JSONException {
        final String message = readError(connection);
        int code = connection.getResponseCode();

        return new APIException(message, code);
    }

    private JSONObject sendAsyncRequest(final Method method, final String path)
            throws IOException, JSONException, APIException, InterruptedException {
        return sendAsyncRequest(method, path, null);
    }

    private <TPayload> JSONObject sendAsyncRequest(final Method method, final String path, final TPayload payload)
            throws IOException, JSONException, APIException, InterruptedException {
        final HttpURLConnection connection = sendAPIRequest(method, path, payload);
        final String location = getLocation(connection);
        int initialDelay = pollInterval;

        if (hasJsonContent(connection)) {
            final JSONObject status = readJsonObjectBody(connection);
            initialDelay = status.getInt("estimatedLatency");
        }
        wait(initialDelay);

        return pollStatus(location);
    }

    private String getLocation(HttpURLConnection connection) {
        return connection.getHeaderField("Location");
    }

    private void wait(int milliseconds) throws InterruptedException {
        Thread.sleep(milliseconds);
    }

    private JSONObject pollStatus(final String location)
            throws InterruptedException, APIException, IOException, JSONException {
        while (true) {
            final JSONObject status = readStatus(location);
            if (isCompleted(status)) {
                return status;
            }
            wait(pollInterval);
        }
    }

    private JSONObject readStatus(final String location) throws JSONException, APIException, IOException {
        HttpURLConnection connection = sendAPIRequest(Method.GET, location, null);

        return readJsonObjectBody(connection);
    }

    private JSONObject readJsonObjectBody(final HttpURLConnection connection) throws IOException, JSONException {
        final String body = readInput(connection);

        return new JSONObject(body);
    }

    private boolean isCompleted(final JSONObject status) throws JSONException {
        return STATUS_COMPLETED.equals(status.getString("status"));
    }
}