org.sakaiproject.lrs.expapi.impl.TincanapiLearningResourceStoreProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.lrs.expapi.impl.TincanapiLearningResourceStoreProvider.java

Source

/**
 * Copyright 2013 Unicon (R) Licensed under the
 * Educational Community 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.osedu.org/licenses/ECL-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.
 */

package org.sakaiproject.lrs.expapi.impl;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;

import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthServiceProvider;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.commons.validator.routines.UrlValidator;
import org.azeckoski.reflectutils.transcoders.JSONTranscoder;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entitybroker.util.http.HttpClientWrapper;
import org.sakaiproject.entitybroker.util.http.HttpRESTUtils;
import org.sakaiproject.entitybroker.util.http.HttpRESTUtils.Method;
import org.sakaiproject.entitybroker.util.http.HttpResponse;
import org.sakaiproject.event.api.LearningResourceStoreProvider;
import org.sakaiproject.event.api.LearningResourceStoreService.LRS_Statement;
import org.sakaiproject.lrs.expapi.model.LRSKeys;
import org.sakaiproject.lrs.expapi.util.StatementMapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TinCanAPI LRS provider that understands how to handle LRS statements to a TincanAPI service.
 * 
 * @author Charles Hasegawa (chasegawa @ Unicon.net)
 * @author Aaron Zeckoski (azeckoski @ unicon.net) (azeckoski @ vt.edu)
 * @author Robert Long (rlong @ unicon.net)
 */
public class TincanapiLearningResourceStoreProvider implements LearningResourceStoreProvider, LRSKeys {

    private static final Logger logger = LoggerFactory.getLogger(TincanapiLearningResourceStoreProvider.class);

    private static final String apiVersion = "1.0.0";
    private static final HashMap<String, String> EMPTY_PARAMS = new HashMap<>(0);
    private static final FastDateFormat FORMATTER = DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
    private static final String TEST_CONN_MESSAGE = "{\"actor\": {\"mbox\": \"mailto:no-reply@sakaiTCAPI.com\",\"name\": \"Sakai startup connection test\",\"objectType\": \"Agent\"},\"verb\": {\"id\": \"http://adlnet.gov/expapi/verbs/interacted\",\"display\": {\"en-US\": \"interacted\"}},\"object\": {\"id\": \"http://www.example.com/tincan/activities/OyeZsHFR\",\"objectType\": \"Activity\",\"definition\": {\"name\": {\"en-US\": \"Example Activity\"}}}}";
    private static final boolean GUARANTEE_SSL = true;

    // config variables
    private String basicAuthString;
    private ServerConfigurationService serverConfigurationService;
    private String consumerKey;
    private String consumerSecret;
    private String id = "tincanapi";
    private String realm;
    private int timeout = 0;
    private String url;

    // calculated variables
    private String configPrefix;
    private HashMap<String, String> headers;
    private HttpClientWrapper httpClientWrapper;
    private JSONTranscoder jsonTranscoder;
    private OAuthServiceProvider oAuthServiceProvider;
    private OAuthAccessor oAuthAccessor;

    /**
     * @param {@link ServerConfigurationService}
     */
    public TincanapiLearningResourceStoreProvider(ServerConfigurationService configurationService) {
        this.serverConfigurationService = configurationService;
    }

    /**
     * Build up the pieces needed for getting the two-step OAuth header values
     */
    private void configForOAuth() {
        oAuthServiceProvider = new OAuthServiceProvider("notUsed", "notUsed", "notUsed");
        oAuthAccessor = new OAuthAccessor(
                new OAuthConsumer("notUsed", consumerKey, consumerSecret, oAuthServiceProvider));
    }

    /**
     * @return create a JSON string from the various elements of the supplied statement
     */
    private String convertLRS_StatementToJSON(LRS_Statement statement) {
        HashMap<String, Object> statementMap = new HashMap<>();

        // Actor, verb and object are required
        try {
            statementMap.put(LRSStatementKey.actor.toString(), StatementMapUtils.getActorMap(statement.getActor()));
            statementMap.put(LRSStatementKey.verb.toString(), StatementMapUtils.getVerbMap(statement.getVerb()));
            statementMap.put(LRSStatementKey.object.toString(),
                    StatementMapUtils.getObjectMap(statement.getObject()));
        } catch (Exception e) {
            logger.debug("Unable to handle supplied LRS_Statement", e);
            throw new IllegalArgumentException(
                    "Unable to handle supplied LRS_Statement.\nUnable to process Actor, Verb, or Object");
        }

        if (null != statement.getContext()) {
            statementMap.put(LRSStatementKey.context.toString(),
                    StatementMapUtils.getContextMap(statement.getContext()));
        }

        if (null != statement.getResult()) {
            statementMap.put(LRSStatementKey.result.toString(),
                    StatementMapUtils.getResultMap(statement.getResult()));
        }

        if (null != statement.getStored()) {
            statementMap.put(LRSStatementKey.stored.toString(), FORMATTER.format(statement.getStored()));
        }

        if (null != statement.getTimestamp()) {
            statementMap.put(LRSStatementKey.timestamp.toString(), FORMATTER.format(statement.getTimestamp()));
        }

        return jsonTranscoder.encode(statementMap, null, null);
    }

    /**
     * Shutdown the provider
     */
    public void destroy() {
        if (httpClientWrapper != null) {
            try {
                httpClientWrapper.shutdown();
            } catch (Exception e) {
                e.printStackTrace();
            }

            httpClientWrapper = null;
        }

        jsonTranscoder = null;
    }

    /**
     * Parse the data from the LRS statement and handle sending the request to the configured receiver. If there is an issue in
     * sending (due to endpoint being misconfigured or unavailable), we simply log the statement.
     * 
     * @see org.sakaiproject.event.api.LearningResourceStoreProvider#handleStatement(org.sakaiproject.event.api.LearningResourceStoreService.LRS_Statement)
     */
    public void handleStatement(LRS_Statement statement) {
        String data = null;

        if (statement.isPopulated()) {
            data = convertLRS_StatementToJSON(statement);
            logger.debug("LRS using populated statement: {}", data);
        } else if (statement.getRawMap() != null && !statement.getRawMap().isEmpty()) {
            data = jsonTranscoder.encode(statement.getRawMap(), null, null);
            logger.debug("LRS using raw Map statement: {}", data);
        } else {
            data = statement.getRawJSON();
            logger.debug("LRS using raw JSON statement: {}", data);
        }

        logger.debug("LRS Attempting to handle statement: {}", statement);

        try {
            HttpResponse response = postData(data);
            if (response.getResponseCode() >= 200 && response.getResponseCode() < 300) {
                logger.debug(id + " LRS provider successfully sent statement: {}", statement);
            } else {
                logger.warn(id + " LRS provider failed ({} {}) sending statement ({}) to ({}), response: {}",
                        response.getResponseCode(), response.getResponseMessage(), statement, url,
                        response.getResponseBody());
            }
        } catch (Exception e) {
            logger.error(
                    id + " LRS provider exception (" + e + "): Statement was not sent.\n Statement data: " + data);
        }
    }

    /**
     * Initialize the state of this provider, reading in the configuration.
     * 
     * @throws URISyntaxException
     * @throws IOException
     * @throws OAuthException
     */
    public void init() throws OAuthException, IOException, URISyntaxException {
        readConfig();
        // Don't allow api version to be configured... we only should be reporting it
        logger.info("{} LRS provider (version {}) configured: {}", id, apiVersion, url);

        headers = new HashMap<>(3);
        headers.put("Content-Type", "application/json");
        headers.put("X-Experience-API-Version", apiVersion);

        if (StringUtils.isNotEmpty(basicAuthString)) {
            // Note that the SCORM example javascript 64 encoder does use + and / in their output, so we do NOT use the URL safe
            // version of this method here to match their logic
            headers.put("Authorization", "Basic " + Base64.encodeBase64String((basicAuthString).getBytes()));
        } else {
            configForOAuth();
        }

        jsonTranscoder = new JSONTranscoder(true, true, false);
        httpClientWrapper = HttpRESTUtils.makeReusableHttpClient(true, timeout, null);
        HttpResponse response;

        try {
            response = postData(TEST_CONN_MESSAGE);

            if (response.getResponseCode() == 200) {
                logger.info("{} LRS provider configured and ready", id);
            } else {
                logger.error("{} LRS provider not configured properly OR LRS is offline - test message failed!",
                        id);
            }
        } catch (Exception e) {
            logger.error("{} LRS provider failure while trying to contact the LRS! Initialization test failed: ",
                    id, e);
        }

        logger.info("{} LRS provider INIT complete", id);
    }

    private HttpResponse postData(String data) throws OAuthException, IOException, URISyntaxException {
        if (StringUtils.isEmpty(basicAuthString)) {
            OAuthMessage message = oAuthAccessor.newRequestMessage(OAuthMessage.POST, url, null);
            message.sign(oAuthAccessor);
            String authHeader = message.getAuthorizationHeader(realm);
            headers.put("Authorization", authHeader);
        }
        HttpResponse response = HttpRESTUtils.fireRequest(httpClientWrapper, url, Method.POST, EMPTY_PARAMS,
                headers, data, GUARANTEE_SSL);

        return response;
    }

    /**
     * Read the setup from the configuration. All non-empty values will overwrite any values that may have been set due to DI
     * Values are prefixed with "lrs.[id]." so that multiple versions of this provider can be instantiated based on the id.
     */
    private void readConfig() {
        if (StringUtils.isEmpty(id)) {
            throw new IllegalStateException("Invalid " + id + " for LRS provider, cannot start");
        }

        configPrefix = "lrs." + id + ".";

        String value = serverConfigurationService.getConfig(configPrefix + "url", "");
        url = StringUtils.isNotEmpty(value) ? value : url;

        // ensure the URL is valid and formatted the same way (add "/" if not on the end for example)
        UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS);

        if (!urlValidator.isValid(url)) {
            throw new IllegalStateException("Invalid " + id + " LRS provider url (" + url + "), correct the "
                    + configPrefix + "url config value");
        } /* won't work with some LRS
          else {
          if (!url.endsWith("/")) {
              url = url + "/";
          }
          }*/

        value = serverConfigurationService.getConfig(configPrefix + "request.timeout", "");

        try {
            timeout = StringUtils.isNotEmpty(value) ? Integer.parseInt(value) : timeout; // allow setter to override
        } catch (NumberFormatException e) {
            timeout = 0;
            logger.debug("{} request.timeout must be an integer value - using default setting", configPrefix, e);
        }

        // basic auth
        value = serverConfigurationService.getConfig(configPrefix + "basicAuthUserPass", "");
        basicAuthString = StringUtils.isNotEmpty(value) ? value : basicAuthString;

        // oauth fields
        value = serverConfigurationService.getConfig(configPrefix + "consumer.key", "");
        consumerKey = StringUtils.isNotEmpty(value) ? value : consumerKey;
        value = serverConfigurationService.getConfig(configPrefix + "consumer.secret", "");
        consumerSecret = StringUtils.isNotEmpty(value) ? value : consumerSecret;
        value = serverConfigurationService.getConfig(configPrefix + "realm", "");
        realm = StringUtils.isNotEmpty(value) ? value : realm;

        if (StringUtils.isEmpty(basicAuthString) && (StringUtils.isEmpty(consumerKey)
                || StringUtils.isEmpty(consumerSecret) || StringUtils.isEmpty(realm))) {
            throw new IllegalStateException(
                    "No authentication configured properly for LRS provider, service cannot start. Please check the configuration");
        }
    }

    public void setBasicAuthString(String authString) {
        this.basicAuthString = authString;
    }

    public void setConsumerKey(String consumerKey) {
        this.consumerKey = consumerKey;
    }

    public void setConsumerSecret(String consumerSecret) {
        this.consumerSecret = consumerSecret;
    }

    /**
     * @see org.sakaiproject.event.api.LearningResourceStoreProvider#getID()
     */
    public String getID() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setRealm(String realm) {
        this.realm = realm;
    }

    public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
        this.serverConfigurationService = serverConfigurationService;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public void setUrl(String url) {
        this.url = url;
    }

}