org.opencommercesearch.client.ProductApi.java Source code

Java tutorial

Introduction

Here is the source code for org.opencommercesearch.client.ProductApi.java

Source

package org.opencommercesearch.client;

/*
* Licensed to OpenCommerceSearch under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. OpenCommerceSearch licenses this
* file to you 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.
*/

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;

import org.apache.commons.lang.StringUtils;
import org.opencommercesearch.client.impl.DefaultProduct;
import org.opencommercesearch.client.request.BrandCategoryRequest;
import org.opencommercesearch.client.request.BrandRequest;
import org.opencommercesearch.client.request.BrowseBrandCategoryRequest;
import org.opencommercesearch.client.request.BrowseBrandRequest;
import org.opencommercesearch.client.request.BrowseCategoryRequest;
import org.opencommercesearch.client.request.CategoryRequest;
import org.opencommercesearch.client.request.CategoryBrandRequest;
import org.opencommercesearch.client.request.ProductRequest;
import org.opencommercesearch.client.request.Request;
import org.opencommercesearch.client.request.SearchRequest;
import org.opencommercesearch.client.request.SimilarProductRequest;
import org.opencommercesearch.client.response.CategoryBrandResponse;
import org.opencommercesearch.client.response.Response;
import org.opencommercesearch.client.response.BrandResponse;
import org.opencommercesearch.client.response.BrowseResponse;
import org.opencommercesearch.client.response.CategoryResponse;
import org.opencommercesearch.client.response.ProductResponse;
import org.opencommercesearch.client.response.SearchResponse;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.data.Form;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Client class that handles requests to the product API.
 * <p/>
 * This class is not thread safe.
 *
 * @author jmendez
 */
public class ProductApi {
    private static Logger logger = LoggerFactory.getLogger(ProductApi.class);

    private static String RESTLET_HTTP_HEADER = "org.restlet.http.headers";
    private final static Set<Status> OK_STATUS_SET = new HashSet<Status>();

    static {
        OK_STATUS_SET.add(Status.SUCCESS_OK);
        OK_STATUS_SET.add(Status.SUCCESS_CREATED);
    }

    /**
     * Supported request methods by the product API client.
     */
    public enum RequestMethod {
        GET, PUT, POST, DELETE
    }

    /**
     * The max number of times to attempt a request before throwing an exception.
     */
    private int maxRetries = 2;

    /**
     * The API host name and the port
     */
    private String host = "localhost:9000";

    /**
     * The API version that should be used.
     */
    private String version = "v1";

    /**
     * The base URL
     */
    private String baseUrl = "http://" + host + "/" + version;

    /**
     * Whether or not this client should retrieve preview data.
     */
    private Boolean preview = false;

    /**
     * Configuration properties for the product API. For example HTTP client properties.
     */
    private Properties configProperties = new Properties();

    /**
     * The restlet client used to send request to the product API.
     */
    private Client client;

    /**
     * JSON object mapper
     */
    private ObjectMapper mapper;

    /**
     * Cache enables
     */
    private Boolean cacheEnabled;

    /**
     * Creates a new ProductApi instance with properties existing in the class path. If no properties are found, an exception is thrown.
     *
     * @throws IOException if no properties are found in the class path.
     */
    public ProductApi() throws IOException {
        this.configProperties = loadProperties();
        cacheEnabled = true;

        initProperties();
        initMapper();
    }

    /**
     * Creates a new ProductApi instance with the given properties.
     *
     * @param properties Map of configuration properties understood by the ApiClient.
     */
    public ProductApi(Properties properties) {
        this.configProperties = properties;
        cacheEnabled = true;

        initProperties();
        initMapper();
    }

    /**
     * Creates a new ProductApi instance with the given properties.
     *
     * @param properties Map of configuration properties understood by the ApiClient.
     * @param cacheEnabled
     */
    public ProductApi(Properties properties, boolean cacheEnabled) {
        this.configProperties = properties;
        this.cacheEnabled = cacheEnabled;

        initProperties();
        initMapper();
    }

    /**
     * Initializes some default properties from the Product API client
     */
    protected void initProperties() {
        if (configProperties.getProperty("host") != null) {
            setHost(configProperties.getProperty("host"));
        }

        if (configProperties.getProperty("version") != null) {
            setVersion(configProperties.getProperty("version"));
        }

        if (configProperties.getProperty("preview") != null) {
            setPreview(Boolean.valueOf(configProperties.getProperty("preview")));
        }

        if (configProperties.getProperty("maxRetries") != null) {
            setMaxRetries(Integer.parseInt(configProperties.getProperty("maxRetries")));
        }
    }

    private void initMapper() {
        mapper = new ObjectMapper().setDateFormat(new ISO8601DateFormat())
                .setSerializationInclusion(JsonInclude.Include.NON_NULL);
        SimpleModule module = new SimpleModule("InterfaceMapper");
        module.addAbstractTypeMapping(Product.class, DefaultProduct.class);
        mapper.registerModule(module);
    }

    protected ObjectMapper getMapper() {
        return mapper;
    }

    /**
     * Finds one or more products by ids
     * @param request is the product request
     * @return a product response
     * @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public ProductResponse findProducts(ProductRequest request) throws ProductApiException {
        return (ProductResponse) handle(request, ProductResponse.class);
    }

    /**
     * Finds similar products by a productId
     * @param request is the product request
     * @return a product response
     * @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public ProductResponse findSimilarProducts(SimilarProductRequest request) throws ProductApiException {
        return (ProductResponse) handle(request, ProductResponse.class);
    }

    /**
     * Finds one category
     * @param request is the category request
     * @return a category response
     * @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public CategoryResponse findCategory(CategoryRequest request) throws ProductApiException {
        return (CategoryResponse) handle(request, CategoryResponse.class);
    }

    /**
     * Finds one brand
     * @param request is the brand request
     * @return a brand response
     @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public BrandResponse findBrand(BrandRequest request) throws ProductApiException {
        return (BrandResponse) handle(request, BrandResponse.class);
    }

    /**
     * Find the categories from a given brand
     * @param request is the brand category request
     * @return a category response
     * @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public CategoryResponse findBrandCategories(BrandCategoryRequest request) throws ProductApiException {
        return (CategoryResponse) handle(request, CategoryResponse.class);
    }

    /**
     * Find the brands from a given category
     * @param request is the category brands request
     * @return a category brand response
     * @throws ProductApiException if there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public CategoryBrandResponse findCategoryBrands(CategoryBrandRequest request) throws ProductApiException {
        return (CategoryBrandResponse) handle(request, CategoryBrandResponse.class);
    }

    /**
     * Performs a search for products.
     *
     * @param request A search request.
     * @return A search response with products matching the given search request.
     * @throws ProductApiException If there are underlying HTTP communication issues, if the API responded with errors or if there were response parsing problems.
     */
    public SearchResponse search(SearchRequest request) throws ProductApiException {
        return (SearchResponse) handle(request, SearchResponse.class);
    }

    /**
     * Browse products by brand categories.
     *
     * @param request A brand category browse request.
     * @return A browse response with products matching the given browse request.
     * @throws ProductApiException If there are underlying HTTP communication issues, if the API responded with errors or if there response parsing problems.
     */
    public BrowseResponse browse(BrowseBrandCategoryRequest request) throws ProductApiException {
        return (BrowseResponse) handle(request, BrowseResponse.class);
    }

    /**
     * Browse products by brand.
     *
     * @param request A brand browse request.
     * @return A browse response with products matching the given browse request.
     * @throws ProductApiException If there are underlying HTTP communication issues, if the API responded with errors or if there response parsing problems.
     */
    public BrowseResponse browseByBrand(BrowseBrandRequest request) throws ProductApiException {
        return (BrowseResponse) handle(request, BrowseResponse.class);
    }

    /**
     * Browse products by category.
     *
     * @param request A category browse request.
     * @return A browse response with products matching the given browse request.
     * @throws ProductApiException If there are underlying HTTP communication issues, if the API responded with errors or if there response parsing problems.
     */
    public BrowseResponse browseByCategory(BrowseCategoryRequest request) throws ProductApiException {
        return (BrowseResponse) handle(request, BrowseResponse.class);
    }

    /**
     * 
     * @param restletRequest Restlet request whose header to be modified
     * @param name Name of the parameter to be added
     * @param value Value of the parameter
     */
    public void setHttpHeaderParameter(org.restlet.Request restletRequest, String name, String value) {
        try {
            Map<String, Object> attrs = restletRequest.getAttributes();
            Form headers = (Form) attrs.get(RESTLET_HTTP_HEADER);
            if (headers == null) {
                headers = new Form();
                headers.add(name, value);
            }
            attrs.put(RESTLET_HTTP_HEADER, headers);
            restletRequest.setAttributes(attrs);
        } catch (Exception ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Not able to set parameter in http header", ex);
            }
        }
    }

    /**
     * Receives an API request, translates it into a valid HTTP request which is sent to the product API server and then processes the result by unmarshalling any
     * data into a valid ApiResponse subclass.
     *
     * @param request The API request to be handled.
     * @param clazz   The API response class that should be returned.
     * @param <T>     The API response type that will be returned.
     * @return A API response with data returned from the product API server.
     * @throws ProductApiException If there are underlying HTTP communication issues, if the API responded with errors or if there response parsing problems.
     */
    protected <T extends Response> Response handle(Request request, Class<T> clazz) throws ProductApiException {
        int retries = 0;
        Exception lastException = null;

        do {
            org.restlet.Request restletRequest = null;
            org.restlet.Response response = null;

            try {
                String url = getRequestUrl(request);
                if (url == null) {
                    throw new IllegalArgumentException("Request " + request.getClass() + " has a null endpoint");
                }

                restletRequest = convertToRestlet(url, request);

                if (!getCacheEnabled()) {
                    setHttpHeaderParameter(restletRequest, "X-Cache-Refresh", "true");
                }

                if (request.getHeaderParams() != null) {
                    for (Map.Entry<String, String> entry : request.getHeaderParams().entrySet()) {
                        setHttpHeaderParameter(restletRequest, entry.getKey(), entry.getValue());
                    }
                }

                if (logger.isDebugEnabled()) {
                    logger.debug("Sending API request with base URL: " + restletRequest.getResourceRef());
                }

                response = client.handle(restletRequest);

                if (logger.isDebugEnabled()) {
                    logger.debug("API response is '" + response.getStatus() + "'");
                }

                Response apiResponse = unmarshall(response, clazz);
                if (OK_STATUS_SET.contains(response.getStatus())) {
                    return apiResponse;
                }

                //Handle special errors
                int statusCode = response.getStatus().getCode();
                String requestUrl = url + "&" + request.getQueryString();
                String message = getResponseMessage(apiResponse, response);

                if (statusCode >= 400 && statusCode < 500) {
                    throw new ProductApiException(statusCode, url, message);
                } else {
                    lastException = new ProductApiException(statusCode, url, message, lastException);
                }
            } catch (JsonProcessingException e) {
                throw new ProductApiException("Found invalid JSON format in the response.", e);
            } catch (IOException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Failed to handle API request, attempt " + retries);
                }

                e.initCause(lastException);
                lastException = e;
            } finally {
                if (response != null) {
                    response.release();
                }
                if (restletRequest != null) {
                    restletRequest.release();
                }
            }
        } while (retries++ < maxRetries);

        throw new ProductApiException("Failed to handle API request.", lastException);
    }

    private String getResponseMessage(Response apiResponse, org.restlet.Response restletResponse) {
        if (apiResponse != null) {
            return apiResponse.getMessage();
        } else if (restletResponse != null) {
            return restletResponse.getStatus().getDescription();
        }
        return null;
    }

    /**
     * Starts the underlying HTTP connection.
     *
     * @throws ProductApiException If the underlying HTTP connection can't initialize.
     */
    public void start() throws ProductApiException {

        if (client != null) {
            stop();
        }

        Context context = new Context();

        for (Map.Entry<Object, Object> entry : configProperties.entrySet()) {
            context.getParameters().add(entry.getKey().toString(), entry.getValue().toString());
        }

        client = new Client(context, Protocol.HTTP);

        try {
            client.start();
            logger.info("Started product API client");
        } catch (Exception e) {
            throw new ProductApiException("Failed to initialize underlying HTTP client.", e);
        }
    }

    /**
     * Stops the underlying HTTP connection and releases any allocated resources.
     *
     * @throws ProductApiException If the underlying HTTP connection can't be stopped.
     */
    public void stop() throws ProductApiException {
        if (client != null) {
            try {
                client.stop();
                logger.info("Stopped product API client");
            } catch (Exception e) {
                throw new ProductApiException("Failed to stop underlying HTTP client.", e);
            } finally {
                client = null;
            }
        }
    }

    /**
     * Converts a request to a valid HTTP Restlet request.
     * <p/>
     * The returned Restlet request may vary depending on the input request method.
     *
     * @param url     The URL for the HTTP request.
     * @param request The actual parameters to be included in the Restlet request.
     * @return A new Restlet request with the given URL and parameters set.
     * @throws ProductApiException 
     */
    org.restlet.Request convertToRestlet(String url, Request request) throws ProductApiException {
        switch (request.getMethod()) {
        case GET:
            return new org.restlet.Request(Method.GET, url + "&" + request.getQueryString());
        }

        throw new IllegalArgumentException("Method " + request.getMethod() + " not supported.");
    }

    /**
     * Reads a plain HTTP response and transforms it into a user friendly ApiResponse according to its type.
     *
     * @param response The HTTP response.
     * @return An API response that matches the given HTTP data.
     */
    private <T extends Response> Response unmarshall(org.restlet.Response response, Class<T> clazz)
            throws IOException {
        if (response != null && response.getEntity() != null) {
            return mapper.readValue(response.getEntity().getStream(), clazz);
        }
        return null;
    }

    /**
     * Get the proper URL to use based on the request endpoint and current API settings.
     * <p/>
     * <p/>
     * An example URL will look like: http://localhost:9000/v1/products?preview=false
     *
     * @param request Request used to calculate the URL from.
     * @return A properly formed request URL for the API.
     */
    protected String getRequestUrl(Request request) {
        String endpoint = request.getEndpoint();
        if (StringUtils.isNotEmpty(endpoint)) {
            StringBuilder url = new StringBuilder();
            url.append(baseUrl);

            if (endpoint.charAt(0) != '/') {
                url.append("/");
            }

            url.append(endpoint).append("?preview=").append(getPreview());
            return url.toString();
        } else {
            return null;
        }
    }

    /**
     * Loads configuration properties defined in a file "config.properties" on the class path.
     *
     * @return A properties instance with any configuration options that are needed.
     * @throws IOException If the file config.properties can't be read.
     */
    protected Properties loadProperties() throws IOException {
        Properties properties = new Properties();
        InputStream in = ClassLoader.getSystemResourceAsStream("config.properties");

        if (in == null) {
            throw new IOException("File config.properties not found on classpath.");
        }

        properties.load(in);
        in.close();
        return properties;
    }

    protected Logger getLogger() {
        return ProductApi.logger;
    }

    public String getHost() {
        return host;
    }

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

    private void setBaseUrl() {
        this.baseUrl = "http://" + host + "/" + version;
    }

    public String getVersion() {
        return version;
    }

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

    public Boolean getPreview() {
        return preview;
    }

    public void setPreview(Boolean preview) {
        this.preview = preview;
    }

    public Properties getConfigProperties() {
        return configProperties;
    }

    public void setConfigProperties(Properties configProperties) {
        this.configProperties = configProperties;
    }

    protected Client getClient() {
        return client;
    }

    protected void setClient(Client client) {
        this.client = client;
    }

    public int getMaxRetries() {
        return maxRetries;
    }

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public Boolean getCacheEnabled() {
        return cacheEnabled;
    }

    public void setCacheEnabled(Boolean cacheEnabled) {
        this.cacheEnabled = cacheEnabled;
    }

}