com.snaplogic.snaps.lunex.BaseService.java Source code

Java tutorial

Introduction

Here is the source code for com.snaplogic.snaps.lunex.BaseService.java

Source

/*
 * SnapLogic - Data Integration
 *
 * Copyright (C) 2014, SnapLogic, Inc. All rights reserved.
 *
 * This program is licensed under the terms of
 * the SnapLogic Commercial Subscription agreement
 *
 * "SnapLogic" is a trademark of SnapLogic, Inc.
 */
package com.snaplogic.snaps.lunex;

import com.fasterxml.jackson.core.JsonParseException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.jayway.jsonpath.JsonPath;
import com.snaplogic.account.api.Account;
import com.snaplogic.account.api.capabilities.Accounts;
import com.snaplogic.account.api.capabilities.MultiAccountBinding;
import com.snaplogic.api.ConfigurationException;
import com.snaplogic.api.ExecutionException;
import com.snaplogic.common.SnapType;
import com.snaplogic.common.properties.SnapProperty;
import com.snaplogic.common.properties.Suggestions;
import com.snaplogic.common.properties.builders.PropertyBuilder;
import com.snaplogic.common.properties.builders.SuggestionBuilder;
import com.snaplogic.snap.api.Document;
import com.snaplogic.snap.api.ExpressionProperty;
import com.snaplogic.snap.api.PropertyValues;
import com.snaplogic.snap.api.SimpleSnap;
import com.snaplogic.snap.api.SnapCategory;
import com.snaplogic.snap.api.SnapDataException;
import com.snaplogic.snap.api.capabilities.Category;
import com.snaplogic.snap.api.capabilities.Inputs;
import com.snaplogic.snap.api.capabilities.Outputs;
import com.snaplogic.snap.api.capabilities.Version;
import com.snaplogic.snap.api.capabilities.ViewType;
import com.snaplogic.snaps.lunex.Constants.CResource;
import com.snaplogic.snaps.lunex.Constants.HttpMethodNames;
import com.snaplogic.snaps.lunex.Constants.LunexSnaps;
import com.snaplogic.snaps.lunex.bean.AccountBean;
import com.snaplogic.util.JsonPathBuilder;
import com.snaplogic.util.JsonPathUtil;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.snaplogic.snaps.lunex.Constants.*;
import static com.snaplogic.snaps.lunex.Messages.*;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.ADDRESS_JSONOBJ;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.CR_PARAM_LIST;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.DR_PARAM_LIST;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.REQ_BODY_PARAM_INFO;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.RR_PARAM_LIST;
import static com.snaplogic.snaps.lunex.ServiceURIInfo.UR_PARAM_LIST;

/**
 * Abstract base class for Lunex snap pack which contains common snap properties, authentication and
 * request handling.
 *
 * @author svatada
 */
@Inputs(min = 0, max = 1, accepts = { ViewType.DOCUMENT })
@Outputs(min = 1, max = 1, offers = { ViewType.DOCUMENT })
@Version(snap = 1)
@Category(snap = SnapCategory.TRANSFORM)
@Accounts(provides = { LunexBasicAuthAccount.class }, optional = true)
public abstract class BaseService extends SimpleSnap implements MultiAccountBinding<Account<AccountBean>> {
    private static final int INIT_HEADER_SIZE = 4;
    private static final String PARAM_NAME_JSON = new JsonPathBuilder(PARAM_NAME_PROP).appendValueElement().build();
    private static final String FIELD_NAME_JSON = new JsonPathBuilder(FIELD_NAME_PROP).appendValueElement().build();
    private static final String HEADER_KEY_JSON = new JsonPathBuilder(HEADER_KEY_PROP).appendValueElement().build();
    private static final String HEADER_VALUE_JSON = new JsonPathBuilder(HEADER_VALUE_PROP).appendValueElement()
            .build();
    private List<Pair<String, ExpressionProperty>> queryParams;
    private List<Pair<String, ExpressionProperty>> requestContentInfo;
    private List<Pair<String, String>> headersProperties;
    @Inject
    private Account<AccountBean> account;
    private ExpressionProperty username, password;
    private final LunexSnaps snapsType;
    @Inject
    private JsonPathUtil jsonPathUtil;
    private String resourceType;
    private String lunexHost;
    private boolean isAuthHeaderSet;
    private boolean isCredentialsSet;
    private static final Logger log = LoggerFactory.getLogger(BaseService.class);
    private final HttpMethodNames httpMethod;

    /* Constructor to initialise the respective snap */
    public BaseService(LunexSnaps snaps, HttpMethodNames httpMethod) {
        this.snapsType = snaps;
        this.httpMethod = httpMethod;
    }

    @Override
    public Module getManagedAccountModule(final Account<AccountBean> account) {
        return new AbstractModule() {
            @Override
            protected void configure() {
                TypeLiteral<Account<AccountBean>> typeLiteral = new TypeLiteral<Account<AccountBean>>() {
                };
                if (account == null) {
                    bind(Key.get(typeLiteral)).to(DefaultAccount.class).in(Scopes.SINGLETON);
                } else {
                    bind(Key.get(typeLiteral)).toInstance(account);
                }
            }
        };
    }

    @Override
    public void defineProperties(final PropertyBuilder propertyBuilder) {
        propertyBuilder.describe(SERVICE_DOMAIN_PROP, SERVICE_DOMAIN_LABEL, SERVICE_DOMAIN_DESCRIPTION).required()
                .add();
        propertyBuilder.describe(USERNAME_PROP, USERNAME_LABEL, USERNAME_DESCRIPTION)
                .expression(SnapProperty.DecoratorType.ACCEPTS_SCHEMA).add();
        propertyBuilder.describe(PASSWORD_PROP, PASSWORD_LABEL, PASSWORD_DESCRIPTION)
                .expression(SnapProperty.DecoratorType.ACCEPTS_SCHEMA).add();
        propertyBuilder.describe(RESOURCE_PROP, RESOURCE_LABEL, RESOURCE_DESC);
        switch (snapsType) {
        case Create:
            propertyBuilder.withAllowedValues(CR_PARAM_LIST.keySet()).add();
            break;
        case Read:
            propertyBuilder.withAllowedValues(RR_PARAM_LIST.keySet()).add();
            break;
        case Delete:
            propertyBuilder.withAllowedValues(DR_PARAM_LIST.keySet()).add();
            break;
        case Update:
            propertyBuilder.withAllowedValues(UR_PARAM_LIST.keySet()).add();
            break;
        }
        SnapProperty headerKey = propertyBuilder.describe(HEADER_KEY_PROP, HEADER_KEY_LABEL, HEADER_KEY_DESCRIPTION)
                .build();
        SnapProperty headerValue = propertyBuilder
                .describe(HEADER_VALUE_PROP, HEADER_VALUE_LABEL, HEADER_VALUE_DESCRIPTION).build();
        propertyBuilder.describe(HTTP_HEADER_PROP, HTTP_HEADER_LABEL, HTTP_HEADER_DESCRIPTION).type(SnapType.TABLE)
                .withEntry(headerKey).withEntry(headerValue).add();
        SnapProperty paramPath = propertyBuilder.describe(PARAM_PATH_PROP, PARAM_PATH_LABEL, PARAM_PATH_DESC)
                .expression(SnapProperty.DecoratorType.ACCEPTS_SCHEMA).build();
        SnapProperty paramName = propertyBuilder.describe(PARAM_NAME_PROP, PARAM_NAME_LABEL, PARAM_NAME_DESC)
                .withSuggestions(new Suggestions() {
                    @Override
                    public void suggest(final SuggestionBuilder suggestionBuilder,
                            final PropertyValues propertyValues) {
                        switch (snapsType) {
                        case Create:
                            setSuggestionBuilder(suggestionBuilder, propertyValues, PARAM_TABLE_MAPPINGS_PROP,
                                    PARAM_NAME_PROP, CR_PARAM_LIST);
                            break;
                        case Read:
                            setSuggestionBuilder(suggestionBuilder, propertyValues, PARAM_TABLE_MAPPINGS_PROP,
                                    PARAM_NAME_PROP, RR_PARAM_LIST);
                            break;
                        case Delete:
                            setSuggestionBuilder(suggestionBuilder, propertyValues, PARAM_TABLE_MAPPINGS_PROP,
                                    PARAM_NAME_PROP, DR_PARAM_LIST);
                            break;
                        case Update:
                            setSuggestionBuilder(suggestionBuilder, propertyValues, PARAM_TABLE_MAPPINGS_PROP,
                                    PARAM_NAME_PROP, UR_PARAM_LIST);
                            break;
                        }
                    }
                }).build();
        propertyBuilder.describe(PARAM_TABLE_MAPPINGS_PROP, PARAM_TABLE_MAPPINGS_LABEL, PARAM_TABLE_MAPPINGS_DESC)
                .type(SnapType.TABLE).withEntry(paramPath).withEntry(paramName).required().add();
        if (snapsType != LunexSnaps.Read) {
            SnapProperty fieldPath = propertyBuilder.describe(FIELD_PATH_PROP, FIELD_PATH_LABEL, FIELD_PATH_DESC)
                    .expression(SnapProperty.DecoratorType.ACCEPTS_SCHEMA).build();
            SnapProperty fieldName = propertyBuilder.describe(FIELD_NAME_PROP, FIELD_NAME_LABEL, FIELD_NAME_DESC)
                    .withSuggestions(new Suggestions() {
                        @Override
                        public void suggest(final SuggestionBuilder suggestionBuilder,
                                final PropertyValues propertyValues) {
                            suggestionBuilder.node(FIELD_TABLE_MAPPINGS_PROP).over(FIELD_NAME_PROP)
                                    .suggestions(REQ_BODY_PARAM_INFO.keySet().toArray(new String[0]));
                        }
                    }).build();
            propertyBuilder
                    .describe(FIELD_TABLE_MAPPINGS_PROP, FIELD_TABLE_MAPPINGS_LABEL, FIELD_TABLE_MAPPINGS_DESC)
                    .type(SnapType.TABLE).withEntry(fieldPath).withEntry(fieldName).add();
        }
    }

    @Override
    public void configure(final PropertyValues propertyValues) throws ConfigurationException {
        lunexHost = propertyValues.get(SERVICE_DOMAIN_PROP);
        resourceType = propertyValues.get(RESOURCE_PROP).toString();
        queryParams = processTableProperty(propertyValues, PARAM_NAME_JSON, PARAM_TABLE_MAPPINGS_PROP,
                PARAM_PATH_PROP);
        requestContentInfo = processTableProperty(propertyValues, FIELD_NAME_JSON, FIELD_TABLE_MAPPINGS_PROP,
                FIELD_PATH_PROP);
        username = propertyValues.getAsExpression(USERNAME_PROP);
        password = propertyValues.getAsExpression(PASSWORD_PROP);
        List<Map<String, Object>> httpHeader = propertyValues.get(HTTP_HEADER_PROP);
        int size = INIT_HEADER_SIZE;
        if (CollectionUtils.isNotEmpty(httpHeader)) {
            size += httpHeader.size();
        }
        /*
         * <p> Snap considers the authentication header based on the following: -> Gives the first
         * priority to Username and Password properties from the snap. -> If they are empty then
         * checks for Account class, -> finally it will checks in header table for Authentication
         * header.<p>
         */
        if (!username.isEmpty() && !password.isEmpty()) {
            isCredentialsSet = true;
        }
        headersProperties = Lists.newArrayListWithExpectedSize(size);
        setDefaultHeaders();
        if (CollectionUtils.isNotEmpty(httpHeader)) {
            String key, value;
            for (Map<String, Object> item : httpHeader) {
                if (null != item) {
                    key = jsonPathUtil.nullableRead(HEADER_KEY_JSON, item);
                    if (isAuthHeaderSet && key.equalsIgnoreCase(AUTHORIZATION)) {
                        continue;
                    } else {
                        value = jsonPathUtil.nullableRead(HEADER_VALUE_JSON, item);
                        headersProperties.add(Pair.of(key, value));
                        if (key.equalsIgnoreCase(AUTHORIZATION)) {
                            isAuthHeaderSet = true;
                        }
                    }
                }
            }
        }
        /* No credentials and no auth header */
        if (!isAuthHeaderSet && !isCredentialsSet) {
            throw new ExecutionException(AUTH_ERROR).withReason(AUTH_ERROR_REASON)
                    .withResolution(AUTH_ERROR_RESOLUTION);
        }
    }

    @Override
    protected void process(Document document, String inputViewName) {
        try {
            if (isCredentialsSet) {
                /* Updating Auth header using credentials from input document */
                setAuthHeaderProp(username.eval(document).toString(), password.eval(document).toString());
            }
            RequestBuilder rBuilder = new RequestBuilder().addDoc(document).addEndPointIP(lunexHost)
                    .addHeaders(headersProperties).addQueryParams(queryParams)
                    .addRequestBody(prepareJson(requestContentInfo, document)).addResource(resourceType)
                    .addSnapTye(snapsType).addMethod(httpMethod);
            String response = RequestProcessor.getInstance().execute(rBuilder);
            int statusCode = RequestProcessor.getInstance().getStatusCode();
            /*
             * Writing HTTP STATUS codes in between 100-299 to success view and other codes will
             * move to Error view.
             *
             * Success: HTTP 1XX-Request received, continuing process. HTTP 2XX-Action requested by
             * the client was received, understood, accepted and processed successfully.
             *
             * Error: HTTP 3XX-The client must take additional action to complete the request. HTTP
             * 4XX-Intended for cases in which the client seems to have errored. HTTP 5XX-The server
             * failed to fulfill an apparently valid request.
             */
            if (statusCode >= 100 && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
                Map<String, Object> map = new HashMap<String, Object>();
                map.put(STATUS_CODE_TAG, statusCode);
                map.put(resourceType, OBJECT_MAPPER.readValue(response, MAP_TYPE_REFERENCE));
                outputViews.write(documentUtility.newDocument(map));
            } else {
                writeToErrorView(statusCode, HTTP_ERROR_REASON, HTTP_ERROR_RESOLUTION, response);
            }
        } catch (MalformedURLException ex) {
            writeToErrorView(ex.getLocalizedMessage(), MALFORMEDURL_ERROR_REASON, MALFORMEDURL_ERROR_RESOLUTION,
                    ex);
        } catch (IllegalStateException ex) {
            writeToErrorView(CONTENT_STREAM_ERROR, ILLEGAL_STATE_REASON, ILLEGAL_STATE_RESOLUTION, ex);
        } catch (JsonParseException ex) {
            writeToErrorView(JSON_PARSING_ERROR, JSON_PARSING_REASON, JSON_PARSING_RESOLUTION, ex);
        } catch (IOException ex) {
            writeToErrorView(IO_ERROR, IO_ERROR_REASON, IO_ERROR_RESOLUTION, ex);
        } catch (Exception ex) {
            writeToErrorView(ERRORMSG, ERROR_REASON, ERROR_RESOLUTION, ex);
        }
    }

    @Override
    public void cleanup() throws ExecutionException {
        // NO OP
    }

    /* This will read the table property and returned the collection of key value pair */
    private List<Pair<String, ExpressionProperty>> processTableProperty(final PropertyValues propertyValues,
            final String JSON_PATH, final String TABLE_PROP_NAME, final String VALUE_PROP_NAME) {
        List<Pair<String, ExpressionProperty>> paramKeyValuePair = null;
        List<Map<String, String>> params = propertyValues.get(TABLE_PROP_NAME);
        if (CollectionUtils.isNotEmpty(params)) {
            ExpressionProperty queryParamValue;
            String queryParam;
            paramKeyValuePair = Lists.newArrayListWithExpectedSize(params.size());
            for (Map<String, String> param : params) {
                queryParam = JsonPath.read(param, JSON_PATH);
                queryParamValue = propertyValues.getExpressionPropertyFor(param, VALUE_PROP_NAME);
                paramKeyValuePair.add(Pair.of(queryParam, queryParamValue));
            }
        }
        return paramKeyValuePair;
    }

    /* Creates the request body based on the selected resource and request parameters */
    private StringBuilder prepareJson(List<Pair<String, ExpressionProperty>> requestContent, Document document) {
        StringBuilder json = new StringBuilder();
        StringBuilder subJson = new StringBuilder();
        boolean isSubJsonRequired = false, isEmptyJson = true;
        if (requestContent != null) {
            if (resourceType.equals(CResource.NewOrder.toString())
                    || resourceType.equals(CResource.PreOrder.toString())) {
                subJson.append(QUOTE).append(ADDRESS).append(QUOTE).append(COLON).append(OPENTAG);
                isSubJsonRequired = true;
            }
            for (Pair<String, ExpressionProperty> paramPair : requestContent) {
                if (isSubJsonRequired && ADDRESS_JSONOBJ.contains(paramPair.getKey())) {
                    subJson.append(getJsonSlice(paramPair, document));
                    isEmptyJson = false;
                } else {
                    json.append(getJsonSlice(paramPair, document));
                }
            }
            if (!isEmptyJson) {
                subJson.append(CLOSETAG).append(COMMA);
            }
            return new StringBuilder().append(OPENTAG).append(subJson).append(json.deleteCharAt(json.length() - 1))
                    .append(CLOSETAG);
        }
        return new StringBuilder("");
    }

    /* Prepares the sub Json object by evaluating the input document */
    private StringBuilder getJsonSlice(Pair<String, ExpressionProperty> paramPair, Document document) {
        String key = paramPair.getKey();
        StringBuilder jsonSlice = new StringBuilder();
        if (((HashMap) document.get()).containsKey(paramPair.getLeft())) {
            if (REQ_BODY_PARAM_INFO.get(key) == 1) {
                jsonSlice.append(QUOTE).append(key).append(QUOTE).append(COLON)
                        .append(paramPair.getRight().eval(document)).append(COMMA);
            } else {
                jsonSlice.append(QUOTE).append(key).append(QUOTE).append(COLON).append(QUOTE)
                        .append(paramPair.getRight().eval(document)).append(QUOTE).append(COMMA);
            }
        }
        return jsonSlice;
    }

    /* configure the basic auth header */
    private void setAuthHeaderProp(String username, String passcode) {
        headersProperties.add(Pair.of(AUTHORIZATION,
                new StringBuilder().append(BASIC)
                        .append(Base64.encodeBase64String(
                                (new StringBuilder().append(username).append(COLON).append(passcode).toString())
                                        .getBytes()))
                        .toString()));
    }

    /* configure the HTTP Lunex request headers */
    private void setDefaultHeaders() {
        headersProperties.add(Pair.of(CONTENT_TYPE, APPLICATION_JSON));
        headersProperties.add(Pair.of(ACCEPT, APPLICATION_JSON));
        headersProperties.add(Pair.of(CONTENT_LENGTH, DEFAULT_CONTENT_LENGTH));
        AccountBean bean = account.connect();
        if (!isCredentialsSet && bean != null) {
            isAuthHeaderSet = true;
            setAuthHeaderProp(bean.getUsername(), bean.getPassword());
        }
    }

    /* Prepare the suggestion builder for table property */
    private void setSuggestionBuilder(final SuggestionBuilder suggestionBuilder, PropertyValues propertyValues,
            final String TABLE_PROP_NAME, final String VALUE_PROP_NAME,
            ImmutableMap<String, ImmutableSet<String>> PARAM_LIST) {
        suggestionBuilder.node(TABLE_PROP_NAME).over(VALUE_PROP_NAME)
                .suggestions(PARAM_LIST.get(propertyValues.get(RESOURCE_PROP)).toArray(new String[0]));
    }

    /* Writes the exception records to error view */
    private void writeToErrorView(final String errMsg, final String errReason, final String errResoulution,
            Throwable ex) {
        log.error(ex.getMessage(), ex);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put(ERROR_TAG, errMsg);
        map.put(MESSAGE_TAG, ex.getMessage());
        map.put(REASON_TAG, errReason);
        map.put(RESOLUTION_TAG, errResoulution);
        SnapDataException snapException = new SnapDataException(documentUtility.newDocument(map), ex.getMessage())
                .withReason(errReason).withResolution(errResoulution);
        errorViews.write(snapException);
    }

    /* Writes the error records to error view */
    private void writeToErrorView(final int httpErrCode, final String errReason, final String errResoulution,
            final String errResponse) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put(ERROR_TAG, httpErrCode);
        map.put(MESSAGE_TAG, errResponse);
        map.put(REASON_TAG, errReason);
        map.put(RESOLUTION_TAG, errResoulution);
        SnapDataException snapException = new SnapDataException(documentUtility.newDocument(map), errResponse)
                .withReason(errReason).withResolution(errResoulution);
        errorViews.write(snapException);
    }
}