org.imsglobal.lti.toolProvider.ResourceLink.java Source code

Java tutorial

Introduction

Here is the source code for org.imsglobal.lti.toolProvider.ResourceLink.java

Source

package org.imsglobal.lti.toolProvider;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.imsglobal.lti.LTIMessage;
import org.imsglobal.lti.LTIUtil;
import org.imsglobal.lti.toolProvider.dataConnector.DataConnector;
import org.imsglobal.lti.toolProvider.service.Membership;
import org.imsglobal.lti.toolProvider.service.ToolSettings;
import org.jdom2.Document;
import org.jdom2.Element;
import org.joda.time.DateTime;

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

public class ResourceLink implements LTISource {

    /**
     * Class to represent a tool consumer resource link
     *
     * @author  Stephen P Vickers <svickers@imsglobal.org>
     * @copyright  IMS Global Learning Consortium Inc
     * @date  2016
     * @version 3.0.2
     * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
     */

    /**
     * Read action.
     */
    public static final int EXT_READ = 1;
    /**
     * Write (create/update) action.
     */
    public static final int EXT_WRITE = 2;
    /**
     * Delete action.
     */
    public static final int EXT_DELETE = 3;
    /**
     * Create action.
     */
    public static final int EXT_CREATE = 4;
    /**
     * Update action.
     */
    public static final int EXT_UPDATE = 5;

    /**
     * Decimal outcome type.
     */
    public static final String EXT_TYPE_DECIMAL = "decimal";
    /**
     * Percentage outcome type.
     */
    public static final String EXT_TYPE_PERCENTAGE = "percentage";
    /**
     * Ratio outcome type.
     */
    public static final String EXT_TYPE_RATIO = "ratio";
    /**
     * Letter (A-F) outcome type.
     */
    public static final String EXT_TYPE_LETTER_AF = "letteraf";
    /**
     * Letter (A-F) with optional +/- outcome type.
     */
    public static final String EXT_TYPE_LETTER_AF_PLUS = "letterafplus";
    /**
     * Pass/fail outcome type.
     */
    public static final String EXT_TYPE_PASS_FAIL = "passfail";
    /**
     * Free text outcome type.
     */
    public static final String EXT_TYPE_TEXT = "freetext";

    private static final int TIMEOUT = 30000;

    /**
     * Context title.
     *
     * @var string title
     */
    private String title = null;
    /**
     * Resource link ID as supplied in the last connection request.
     *
     * @var String ltiResourceLinkId
     */
    private String ltiResourceLinkId = null;
    /**
     * User group sets (null if the consumer does not support the groups enhancement)
     *
     * @var array groupSets
     */
    private Map<String, GroupSet> groupSets = null;
    /**
     * User groups (null if the consumer does not support the groups enhancement)
     *
     * @var array groups
     */
    private Map<String, Group> groups = null;
    /**
     * Request for last service request.
     *
     * @var string extRequest
     */
    private String extRequest = null;
    /**
     * Request headers for last service request.
     *
     * @var array extRequestHeaders
     */
    private Map<String, String> extRequestHeaders = null;
    /**
     * Response from last service request.
     *
     * @var string extResponse
     */
    private String extResponse = null;
    /**
     * Consumer key value for resource link being shared (if any).
     */
    private String primaryConsumerKey = null;
    /**
     * Response header from last service request.
     *
     * @var array extResponseHeaders
     */
    private Map<String, String> extResponseHeaders = null;
    /**
     * Consumer key value for resource link being shared (if any).
     *
     * @var int primaryResourceLinkId
     */
    private int primaryResourceLinkId = 0;
    /**
     * Whether the sharing request has been approved by the primary resource link.
     *
     * @var boolean shareApproved
     */
    private Boolean shareApproved = null;
    /**
     * Date/time when the object was created.
     *
     * @var int created
     */
    private DateTime created = null;
    /**
     * Date/time when the object was last updated.
     *
     * @var int updated
     */
    private DateTime updated = null;

    /**
     * Record ID for this resource link.
     *
     * @var int id
     */
    private int id;
    /**
     * Tool Consumer for this resource link.
     *
     * @var ToolConsumer consumer
     */
    private ToolConsumer consumer = null;
    /**
     * Tool Consumer ID for this resource link.
     *
     * @var int consumerId
     */
    private int consumerId;
    /**
     * Context for this resource link.
     *
     * @var Context context
     */
    private Context context = null;
    /**
     * Context ID for this resource link.
     *
     * @var int contextId
     */
    private int contextId = 0;
    /**
     * Setting values (LTI parameters, custom parameters and local parameters).
     *
     * @var array settings
     */
    private Map<String, List<String>> settings = null;
    /**
     * Whether the settings value have changed since last saved.
     *
     * @var boolean settingsChanged
     */
    private boolean settingsChanged = false;
    /**
     * XML document for the last extension service request.
     *
     * @var string extDoc
     */
    private Document extDoc = null;
    /**
     * XML node array for the last extension service request.
     *
     * @var array extNodes
     */
    private Map<String, List<String>> extNodes = null;
    /**
     * Data connector object or string.
     *
     * @var mixed dataConnector
     */
    private DataConnector dataConnector = null;

    /**
     * Class constructor.
     */
    public ResourceLink() {

        this.initialize();

    }

    /**
     * Initialise the resource link.
     */
    public void initialize() {

        //this.ltiContextId = null;
        this.ltiResourceLinkId = null;
        this.title = "";
        this.settings = new HashMap<String, List<String>>();
        this.groupSets = new HashMap<String, GroupSet>();
        this.groups = new HashMap<String, Group>();
        //this.primaryConsumerKey = null;
        this.primaryResourceLinkId = 0;
        this.created = null;
        this.updated = null;

    }

    /**
     * Initialise the resource link.
     *
     * Pseudonym for initialize().
     */
    public void initialise() {

        this.initialize();

    }

    /**
     * Save the resource link to the database.
     *
     * @return boolean True if the resource link was successfully saved.
     */
    public boolean save() {

        boolean ok = this.getDataConnector().saveResourceLink(this);
        if (ok) {
            this.settingsChanged = false;
        }

        return ok;

    }

    /**
     * Delete the resource link from the database.
     *
     * @return boolean True if the resource link was successfully deleted.
     */
    public boolean delete() {

        return this.getDataConnector().deleteResourceLink(this);

    }

    /**
     * Get tool consumer.
     *
     * @return ToolConsumer Tool consumer object for this resource link.
     */
    public ToolConsumer getConsumer() {

        if (consumer == null) {
            if ((context != null) || (contextId != 0)) {
                consumer = getContext().getConsumer();
            } else {
                consumer = ToolConsumer.fromRecordId(consumerId, this.getDataConnector());
            }
        }

        return consumer;

    }

    /**
     * Set tool consumer ID.
     *
     * @param int consumerId   Tool Consumer ID for this resource link.
     */
    public void setConsumerId(int consumerId) {

        this.consumer = null;
        this.consumerId = consumerId;

    }

    /**
     * Get context.
     *
     * @return object LTIContext object for this resource link.
     */
    public Context getContext() {

        if (context == null && contextId != 0) {
            this.context = Context.fromRecordId(this.contextId, this.getDataConnector());
        }

        return this.context;

    }

    /**
     * Get context record ID.
     *
     * @return int Context record ID for this resource link.
     */
    public int getContextId() {

        return this.contextId;

    }

    /**
     * Set context ID.
     *
     * @param int contextId   Context ID for this resource link.
     */
    public void setContextId(int contextId) {

        this.context = null;
        this.contextId = contextId;

    }

    /**
     * Get tool consumer key.
     *
     * @return string Consumer key value for this resource link.
     */
    public String getKey() {

        return this.getConsumer().getKey();

    }

    /**
     * Get resource link ID.
     *
     * @return int ID for this resource link.
     */
    public String getId() {

        return this.ltiResourceLinkId;

    }

    /**
     * Get resource link record ID.
     *
     * @return int Record ID for this resource link.
     */
    public int getRecordId() {

        return this.id;

    }

    /**
     * Set resource link record ID.
     *
     * @param int id  Record ID for this resource link.
     */
    public void setRecordId(int id) {

        this.id = id;

    }

    /**
     * Get the data connector.
     *
     * @return mixed Data connector object or string
     */
    public DataConnector getDataConnector() {

        return this.dataConnector;

    }

    /**
     * Get a setting value.
     *
     * @param string name    Name of setting
     * @param string default Value to return if the setting does not exist (optional, default is an empty string)
     *
     * @return string Setting value
     */
    public String getSetting(String name) {
        return getSetting(name, "");
    }

    public String getSetting(String name, String dflt) {
        String value = dflt;
        if (settings.containsKey(name)) {
            //return the first element of the list for now
            value = settings.get(name).get(0);
        }

        return value;

    }

    /**
     * Set a setting value.
     *
     * @param string name  Name of setting
     * @param string value Value to set, use an empty value to delete a setting (optional, default is null)
     */

    public void setSetting(String name) {
        settings.remove(name);
    }

    public void setSetting(String name, String value) {

        String old_value = this.getSetting(name);
        if (!value.equals(old_value)) {
            if (StringUtils.isNotEmpty(value)) {
                LTIUtil.setParameter(settings, name, value);
            } else {
                settings.remove(name);
            }
            this.settingsChanged = true;
        }

    }

    public void setSetting(String name, List<String> value) {
        settings.put(name, value);
    }

    /**
     * Get an array of all setting values.
     *
     * @return array Associative array of setting values
     */
    public Map<String, List<String>> getSettings() {

        return this.settings;

    }

    /**
     * Set an array of all setting values.
     *
     * @param array settings  Associative array of setting values
     */
    public void setSettings(Map<String, List<String>> settings) {

        this.settings = settings;

    }

    /**
     * Save setting values.
     *
     * @return boolean True if the settings were successfully saved
     */
    public boolean saveSettings() {
        boolean ok = true;
        if (this.settingsChanged) {
            ok = this.save();
        }
        return ok;

    }

    /**
     * Check if the Outcomes service is supported.
     *
     * @return boolean True if this resource link supports the Outcomes service (either the LTI 1.1 or extension service)
     */
    public boolean hasOutcomesService() {

        String url = this.getSetting("ext_ims_lis_basic_outcome_url") + this.getSetting("lis_outcome_service_url");

        return StringUtils.isNotEmpty(url);

    }

    /**
     * Check if the Memberships extension service is supported.
     *
     * @return boolean True if this resource link supports the Memberships extension service
     */
    public boolean hasMembershipsService() {

        String url = this.getSetting("ext_ims_lis_memberships_url");

        return StringUtils.isNotEmpty(url);

    }

    /**
     * Check if the Setting extension service is supported.
     *
     * @return boolean True if this resource link supports the Setting extension service
     */
    public boolean hasSettingService() {

        String url = this.getSetting("ext_ims_lti_tool_setting_url");

        return StringUtils.isNotEmpty(url);

    }

    /**
     * @deprecated use <code>{@link ResourceLink#doOutcomesService(int, Outcome, User)}</code> instead.
     * <p>
     * Perform an Outcomes service request.
     * <p>
     * The action type parameter should be one of the pre-defined constants.
     *
     * @see #EXT_READ
     * @see #EXT_WRITE
     * @see #EXT_DELETE
     *
     * @param action      action type
     * @param ltiOutcome  Outcome object
     *
     * @return <code>true</code> if the request was successfully processed
     */
    @Deprecated
    public boolean doOutcomesService(int action, Outcome ltiOutcome) {
        return doOutcomesService(action, ltiOutcome, null);
    }

    /**
     * Perform an Outcomes service request.
     *
     * @param int action The action type constant
     * @param Outcome ltiOutcome Outcome object
     * @param User user User object
     *
     * @return boolean True if the request was successfully processed
     */
    public boolean doOutcomesService(int action, Outcome ltiOutcome, User user) {

        boolean response = false;
        this.extResponse = null;
        String todo = "";
        String xml;
        ResourceLink sourceResourceLink = this;
        String sourcedId = ltiOutcome.getSourcedId();

        // Lookup service details from the source resource link appropriate to the user (in case the destination is being shared)
        if (user != null) {
            sourceResourceLink = user.getResourceLink();
            sourcedId = user.getLtiResultSourcedId();
        }

        // Use LTI 1.1 service in preference to extension service if it is available
        String urlLTI11 = sourceResourceLink.getSetting("lis_outcome_service_url");
        String urlExt = sourceResourceLink.getSetting("ext_ims_lis_basic_outcome_url");
        boolean ext = StringUtils.isNotEmpty(urlExt);
        boolean lti11 = StringUtils.isNotEmpty(urlLTI11);
        if (ext || lti11) {
            switch (action) {
            case EXT_READ:
                if (lti11 && (ltiOutcome.getType() == EXT_TYPE_DECIMAL)) {
                    todo = "readResult";
                } else if (ext) {
                    urlLTI11 = null;
                    todo = "basic-lis-readresult";
                }
                break;
            case EXT_WRITE:
                List<String> decimals = new ArrayList<String>();
                decimals.add(EXT_TYPE_DECIMAL);
                if (lti11 && this.checkValueType(ltiOutcome, decimals)) {
                    todo = "replaceResult";
                } else if (this.checkValueType(ltiOutcome)) {
                    urlLTI11 = null;
                    todo = "basic-lis-updateresult";
                }
                break;
            case EXT_DELETE:
                if (lti11 && (ltiOutcome.getType() == EXT_TYPE_DECIMAL)) {
                    todo = "deleteResult";
                } else if (ext) {
                    urlLTI11 = null;
                    todo = "basic-lis-deleteresult";
                }
                break;
            }
        }
        if (StringUtils.isNotEmpty(todo)) {
            String value = ltiOutcome.getValue();
            if (value == null) {
                value = "";
            }
            if (lti11) {
                xml = "";
                if (action == EXT_WRITE) {
                    xml = "\n" + "        <result>\n" + "          <resultScore>\n" + "            <language>"
                            + ltiOutcome.getLanguage() + "</language>\n" + "            <textString>"
                            + ltiOutcome.getValue() + "</textString>\n" + "          </resultScore>\n"
                            + "        </result>\n";

                }
                sourcedId = StringEscapeUtils.escapeHtml4(sourcedId);
                xml += "\n" + "      <resultRecord>\n" + "        <sourcedGUID>\n"
                        + "          <sourcedId>{sourcedId}</sourcedId>\n" + "        </sourcedGUID>\n"
                        + "      </resultRecord>\n";

                if (doLTI11Service(todo, urlLTI11, xml)) {
                    switch (action) {
                    case EXT_READ:
                        value = LTIUtil.getXmlChildValue(this.extDoc.getRootElement(), "textString");
                        if (value == null) {
                            break;
                        } else {
                            ltiOutcome.setValue(value);
                        }
                    case EXT_WRITE:
                    case EXT_DELETE:
                        response = true;
                        break;
                    }
                }
            } else {
                Map<String, List<String>> params = new HashMap<String, List<String>>();
                LTIUtil.setParameter(params, "sourcedid", sourcedId);
                LTIUtil.setParameter(params, "result_resultscore_textstring", value);
                if (StringUtils.isNotEmpty(ltiOutcome.getLanguage())) {
                    LTIUtil.setParameter(params, "result_resultscore_language", ltiOutcome.getLanguage());
                }
                if (StringUtils.isNotEmpty(ltiOutcome.getStatus())) {
                    LTIUtil.setParameter(params, "result_statusofresult", ltiOutcome.getStatus());
                }
                if (ltiOutcome.getDate() != null) {
                    LTIUtil.setParameter(params, "result_date", ltiOutcome.getDate().toString("YYYY-MM-DD"));
                }
                if (StringUtils.isNotEmpty(ltiOutcome.getType())) {
                    LTIUtil.setParameter(params, "result_resultvaluesourcedid", ltiOutcome.getType());
                }
                if (StringUtils.isNotEmpty(ltiOutcome.getDataSource())) {
                    LTIUtil.setParameter(params, "result_datasource", ltiOutcome.getDataSource());
                }
                try {
                    URL u = new URL(urlExt);
                    if (this.doService(todo, u, params)) {
                        switch (action) {
                        case EXT_READ:
                            value = LTIUtil.getXmlChildValue(this.extDoc.getRootElement(), "textstring");
                            if (value != null) {
                                ltiOutcome.setValue(value);
                            }
                            break;
                        case EXT_WRITE:
                        case EXT_DELETE:
                            response = true;
                            break;
                        }
                    }
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
            }

        }

        return response;

    }

    /**
     * Perform a Memberships service request.
     *
     * The user table is updated with the new list of user objects.
     *
     * @param boolean withGroups True is group information is to be requested as well
     *
     * @return mixed Array of User objects or False if the request was not successful
     */

    public List<User> doMembershipsService() {
        return doMembershipsService(false);
    }

    public List<User> doMembershipsService(boolean withGroups) {

        List<User> users = new ArrayList<User>();
        Map<String, User> oldUsers = this.getUserResultSourcedIDs(true, ToolProvider.ID_SCOPE_RESOURCE);
        this.extResponse = null;
        String urlString = this.getSetting("ext_ims_lis_memberships_url");
        boolean ok = false;
        Map<String, List<String>> params = new HashMap<String, List<String>>();
        try {
            URL url = new URL(urlString);

            LTIUtil.setParameter(params, "id", getSetting("ext_ims_lis_memberships_id"));
            if (withGroups) {
                ok = doService("basic-lis-readmembershipsforcontextwithgroups", url, params);
            }
            if (ok) {
                groupSets = new HashMap<String, GroupSet>();
                groups = new HashMap<String, Group>();
            } else {
                ok = doService("basic-lis-readmembershipsforcontext", url, params);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }

        if (ok) {
            Element el = LTIUtil.getXmlChild(this.extDoc.getRootElement(), "memberships");
            if (el != null) {
                List<Element> members = el.getChildren("member");
                for (Element el2 : members) {
                    String value = LTIUtil.getXmlChildValue(el2, "user_id");
                    User user = User.fromResourceLink(this, value);
                    //
                    /// Set the user name
                    //
                    String firstname = LTIUtil.getXmlChildValue(el, "person_name_given");
                    String lastname = LTIUtil.getXmlChildValue(el, "person_name_family");
                    String fullname = LTIUtil.getXmlChildValue(el, "person_name_full");
                    user.setNames(firstname, lastname, fullname);
                    //
                    /// Set the user email
                    //
                    value = LTIUtil.getXmlChildValue(el, "person_contact_email_primary");
                    if (value == null) {
                        value = "";
                    }
                    user.setEmail(value, this.consumer.getDefaultEmail());
                    //
                    /// Set the user roles
                    //
                    value = LTIUtil.getXmlChildValue(el, "roles");
                    if (value != null) {
                        user.setRoles(value);
                    }
                    //
                    /// Set the user groups
                    //
                    el = LTIUtil.getXmlChild(el, "groups");
                    if (el != null) {
                        List<Element> memberGroups = el.getChildren("group");
                        for (Element group : memberGroups) {
                            String groupId = LTIUtil.getXmlChildValue(group, "id");
                            Element set = LTIUtil.getXmlChild(group, "set");
                            String setId = null;
                            if (set != null) {
                                setId = LTIUtil.getXmlChildValue(set, "id");
                                GroupSet groupSet = this.groupSets.get(setId);
                                if (groupSet == null) {
                                    groupSet = new GroupSet(LTIUtil.getXmlChildValue(set, "title"));
                                    this.groupSets.put(setId, groupSet);
                                }
                                groupSet.incNumMembers();
                                if (user.isStaff()) {
                                    groupSet.incNumStaff();
                                }
                                if (user.isLearner()) {
                                    groupSet.incNumLearners();
                                }
                                groupSet.addGroup(groupId);
                            }
                            this.groups.put(groupId, new Group(LTIUtil.getXmlChildValue(group, "title"), setId));
                            user.addGroup(groupId);
                        }
                    }
                    //
                    /// If a result sourcedid is provided save the user
                    //
                    value = LTIUtil.getXmlChildValue(el, "lis_result_sourcedid");
                    if (value != null) {
                        user.setLtiResultSourcedId(value);
                        user.save();
                    }
                    users.add(user);
                    //
                    /// Remove old user (if it exists)
                    //
                    oldUsers.remove(user.getId(ToolProvider.ID_SCOPE_RESOURCE));
                }
                //
                /// Delete any old users which were not in the latest list from the tool consumer
                //
                for (User user : oldUsers.values()) {
                    user.delete();
                }
            }
        } else {
            users = null;
        }

        return users;

    }

    /**
     * Perform a Setting service request.
     *
     * @param int    action The action type constant
     * @param string value  The setting value (optional, default is null)
     *
     * @return mixed The setting value for a read action, true if a write or delete action was successful, otherwise false
     */

    public boolean doSettingService(int action) {
        return doSettingService(action, null);
    }

    public boolean doSettingService(int action, String value) {
        String todo = "";
        boolean response = false;
        this.extResponse = null;
        switch (action) {
        case EXT_READ:
            todo = "basic-lti-loadsetting";
            break;
        case EXT_WRITE:
            todo = "basic-lti-savesetting";
            break;
        case EXT_DELETE:
            todo = "basic-lti-deletesetting";
            break;
        }
        if (StringUtils.isNotEmpty(todo)) {

            String urlString = this.getSetting("ext_ims_lti_tool_setting_url");
            URL url = null;
            try {
                url = new URL(urlString);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            Map<String, List<String>> params = new HashMap<String, List<String>>();
            LTIUtil.setParameter(params, String.valueOf(id), getSetting("ext_ims_lti_tool_setting_id"));
            if (value == null) {
                value = "";
            }
            LTIUtil.setParameter(params, "setting", value);

            if (this.doService(todo, url, params)) {
                switch (action) {
                case EXT_READ:
                    Element el = LTIUtil.getXmlChild(this.extDoc.getRootElement(), "setting");
                    if (el != null) {
                        this.setSetting("ext_ims_lti_tool_setting", LTIUtil.getXmlChildValue(el, "value"));
                    }
                    response = true;
                    break;
                case EXT_WRITE:
                    this.setSetting("ext_ims_lti_tool_setting", value);
                    this.saveSettings();
                    response = true;
                    break;
                case EXT_DELETE:
                    response = true;
                    break;
                }
            }
        }

        return response;

    }

    /**
     * Check if the Tool Settings service is supported.
     *
     * @return boolean True if this resource link supports the Tool Settings service
     */
    public boolean hasToolSettingsService() {

        String url = this.getSetting("custom_link_setting_url");

        return StringUtils.isNotEmpty(url);

    }

    /**
     * Get Tool Settings.
     *
     * @param int      mode       Mode for request (optional, default is current level only)
     * @param boolean  simple     True if all the simple media type is to be used (optional, default is true)
     *
     * @return mixed The array of settings if successful, otherwise false
     */
    public Map<String, List<String>> getToolSettings() {
        return getToolSettings(ToolSettings.MODE_CURRENT_LEVEL, true);
    }

    public Map<String, List<String>> getToolSettings(int mode) {
        return getToolSettings(mode, true);
    }

    public Map<String, List<String>> getToolSettings(int mode, boolean simple) {

        String url = this.getSetting("custom_link_setting_url");
        ToolSettings service = new ToolSettings(this, url, simple);
        return service.get(mode);

    }

    /**
     * Perform a Tool Settings service request.
     *
     * @param array    settings   An associative array of settings (optional, default is none)
     *
     * @return boolean True if action was successful, otherwise false
     */
    public boolean setToolSettings() {
        return setToolSettings(null);
    }

    public boolean setToolSettings(Map<String, List<String>> settings) {

        String url = this.getSetting("custom_link_setting_url");
        ToolSettings service = new ToolSettings(this, url);
        LTIMessage http = service.set(settings);
        return (http != null);
    }

    /**
     * Check if the Membership service is supported.
     *
     * @return boolean True if this resource link supports the Membership service
     */
    public boolean hasMembershipService() {

        boolean has = (this.contextId != 0);
        if (has) {
            has = StringUtils.isNotEmpty(this.getContext().getSetting("custom_context_memberships_url"));
        }

        return has;

    }

    /**
     * Get Memberships.
     *
     * @return mixed The array of User objects if successful, otherwise false
     */
    public List<User> getMembership() {

        List<User> response = null;
        if (this.contextId != 0) {
            String url = this.getContext().getSetting("custom_context_memberships_url");
            if (StringUtils.isNotEmpty(url)) {
                Membership service = new Membership(this, url);
                response = service.get();
            }
        }

        return response;

    }

    /**
     * Obtain an array of User objects for users with a result sourcedId.
     *
     * The array may include users from other resource links which are sharing this resource link.
     * It may also be optionally indexed by the user ID of a specified scope.
     *
     * @param boolean localOnly True if only users from this resource link are to be returned, not users from shared resource links (optional, default is false)
     * @param int     idScope     Scope to use for ID values (optional, default is null for consumer default)
     *
     * @return array Array of User objects
     */
    public Map<String, User> getUserResultSourcedIDs() {
        return getUserResultSourcedIDs(false, 0);
    }

    public Map<String, User> getUserResultSourcedIDs(boolean localOnly) {
        return getUserResultSourcedIDs(localOnly, 0);
    }

    public Map<String, User> getUserResultSourcedIDs(boolean localOnly, int idScope) {

        return this.getDataConnector().getUserResultSourcedIDsResourceLink(this, localOnly, idScope);

    }

    /**
     * Get an array of ResourceLinkShare objects for each resource link which is sharing this context.
     *
     * @return array Array of ResourceLinkShare objects
     */
    public List<ResourceLinkShare> getShares() {

        return this.getDataConnector().getSharesResourceLink(this);

    }

    /**
     * Class constructor from consumer.
     *
     * @param ToolConsumer consumer Consumer object
     * @param string ltiResourceLinkId Resource link ID value
     * @param string tempId Temporary Resource link ID value (optional, default is null)
     * @return ResourceLink
     */

    public static ResourceLink fromConsumer(ToolConsumer consumer, String ltiResourceLinkId) {
        return fromConsumer(consumer, ltiResourceLinkId, null);
    }

    public static ResourceLink fromConsumer(ToolConsumer consumer, String ltiResourceLinkId, String tempId) {

        ResourceLink resourceLink = new ResourceLink();
        resourceLink.consumer = consumer;
        resourceLink.dataConnector = consumer.getDataConnector();
        resourceLink.ltiResourceLinkId = ltiResourceLinkId;
        if (ltiResourceLinkId != null) {
            resourceLink.load();
            if (resourceLink.id == 0 && tempId != null) {
                resourceLink.ltiResourceLinkId = tempId;
                resourceLink.load();
                resourceLink.ltiResourceLinkId = ltiResourceLinkId;
            }
        }

        return resourceLink;

    }

    public static ResourceLink fromConsumerWithPK(ToolConsumer consumer, int resourceLinkId) {
        ResourceLink resourceLink = new ResourceLink();
        resourceLink.consumer = consumer;
        resourceLink.dataConnector = consumer.getDataConnector();
        resourceLink.setRecordId(resourceLinkId);
        resourceLink.load();

        return resourceLink;
    }

    /**
     * Class constructor from context.
     *
     * @param Context context Context object
     * @param string ltiResourceLinkId Resource link ID value
     * @param string tempId Temporary Resource link ID value (optional, default is null)
     * @return ResourceLink
     */

    public static ResourceLink fromContext(Context context, String ltiResourceLinkId) {
        return fromContext(context, ltiResourceLinkId, null);
    }

    public static ResourceLink fromContext(Context context, String ltiResourceLinkId, String tempId) {

        ResourceLink resourceLink = new ResourceLink();
        resourceLink.setContextId(context.getRecordId());
        resourceLink.context = context;
        resourceLink.dataConnector = context.getDataConnector();
        resourceLink.ltiResourceLinkId = ltiResourceLinkId;
        if (StringUtils.isNotEmpty(ltiResourceLinkId)) {
            resourceLink.load();
            if (resourceLink.id == 0 && StringUtils.isNotEmpty(tempId)) {
                resourceLink.ltiResourceLinkId = tempId;
                resourceLink.load();
                resourceLink.ltiResourceLinkId = ltiResourceLinkId;
            }
        }

        return resourceLink;

    }

    /**
     * Load the resource link from the database.
     *
     * @param int id     Record ID of resource link
     * @param DataConnector   dataConnector    Database connection object
     *
     * @return ResourceLink  ResourceLink object
     */
    public static ResourceLink fromRecordId(int id, DataConnector dataConnector) {

        ResourceLink resourceLink = new ResourceLink();
        resourceLink.dataConnector = dataConnector;
        resourceLink.load(id);

        return resourceLink;

    }

    ///
    ///  PRIVATE METHODS
    ///

    /**
     * Load the resource link from the database.
     *
     * @param int id     Record ID of resource link (optional, default is null)
     *
     * @return boolean True if resource link was successfully loaded
     */
    private boolean load() {
        return load(0);
    }

    private boolean load(int id) {

        this.initialize();
        this.id = id;

        return this.getDataConnector().loadResourceLink(this);

    }

    /**
     * Convert data type of value to a supported type if possible.
     *
     * @param Outcome     ltiOutcome     Outcome object
     * @param string[]    supportedTypes Array of outcome types to be supported (optional, default is null to use supported types reported in the last launch for this resource link)
     *
     * @return boolean True if the type/value are valid and supported
     */

    private boolean checkValueType(Outcome ltiOutcome) {
        return checkValueType(ltiOutcome, null);
    }

    private boolean checkValueType(Outcome ltiOutcome, List<String> supportedTypes) {

        if (supportedTypes == null) {
            supportedTypes = new ArrayList<String>();
            String[] supportedTypesArray = StringUtils.split(",",
                    StringUtils.replace(
                            (this.getSetting("ext_ims_lis_resultvalue_sourcedids", EXT_TYPE_DECIMAL)).toLowerCase(),
                            " ", ""));
            for (String t : supportedTypesArray) {
                supportedTypes.add(t);
            }
        }
        String type = ltiOutcome.getType();
        String value = ltiOutcome.getValue();
        // Check whether the type is supported or there is no value
        boolean ok = supportedTypes.contains(type) || (value.length() <= 0);
        if (!ok) {
            // Convert numeric values to decimal
            if (type.equals(EXT_TYPE_PERCENTAGE)) {
                if (value.substring(value.length()).equals("%")) {
                    value = value.substring(0, value.length() - 1);
                }
                ok = StringUtils.isNumeric(value) && (Double.valueOf(value) >= 0) && (Double.valueOf(value) <= 100);
                if (ok) {
                    ltiOutcome.setValue(String.valueOf(Double.valueOf(value) / 100));
                    ltiOutcome.setType(EXT_TYPE_DECIMAL);
                }
            } else if (type.equals(EXT_TYPE_RATIO)) {
                String[] parts = StringUtils.split(value, '/');
                ok = (parts.length == 2) && StringUtils.isNumeric(parts[0]) && StringUtils.isNumeric(parts[1])
                        && (Double.valueOf(parts[0]) >= 0) && (Double.valueOf(parts[1]) > 0);
                if (ok) {
                    ltiOutcome.setValue(String.valueOf(Double.valueOf(parts[0]) / Double.valueOf(parts[1])));
                    ltiOutcome.setType(EXT_TYPE_DECIMAL);
                }
                // Convert letter_af to letter_af_plus or text
            } else if (type.equals(EXT_TYPE_LETTER_AF)) {
                if (supportedTypes.contains(EXT_TYPE_LETTER_AF_PLUS)) {
                    ok = true;
                    ltiOutcome.setType(EXT_TYPE_LETTER_AF_PLUS);
                } else if (supportedTypes.contains(EXT_TYPE_TEXT)) {
                    ok = true;
                    ltiOutcome.setType(EXT_TYPE_TEXT);
                }
                // Convert letter_af_plus to letter_af or text
            } else if (type.equals(EXT_TYPE_LETTER_AF_PLUS)) {
                if (supportedTypes.contains(EXT_TYPE_LETTER_AF) && (value.length() == 1)) {
                    ok = true;
                    ltiOutcome.setType(EXT_TYPE_LETTER_AF);
                } else if (supportedTypes.contains(EXT_TYPE_TEXT)) {
                    ok = true;
                    ltiOutcome.setType(EXT_TYPE_TEXT);
                }
                // Convert text to decimal
            } else if (type == EXT_TYPE_TEXT) {
                ok = StringUtils.isNumeric(value) && (Double.valueOf(value) >= 0) && (Double.valueOf(value) <= 1);
                if (ok) {
                    ltiOutcome.setType(EXT_TYPE_DECIMAL);
                } else if (value.substring(value.length() - 1).equals("%")) {
                    value = value.substring(0, -1);
                    ok = StringUtils.isNumeric(value) && (Double.valueOf(value) >= 0)
                            && (Double.valueOf(value) <= 100);
                    if (ok) {
                        if (supportedTypes.contains(EXT_TYPE_PERCENTAGE)) {
                            ltiOutcome.setType(EXT_TYPE_PERCENTAGE);
                        } else {
                            ltiOutcome.setValue(String.valueOf(Double.valueOf(value) / 100));
                            ltiOutcome.setType(EXT_TYPE_DECIMAL);
                        }
                    }
                }
            }
        }

        return ok;

    }

    /**
     * Send a service request to the tool consumer.
     *
     * @param string type   Message type value
     * @param string url    URL to send request to
     * @param array  params Associative array of parameter values to be passed
     *
     * @return boolean True if the request successfully obtained a response
     */
    private boolean doService(String type, URL url, Map<String, List<String>> params) {

        boolean ok = false;
        this.extRequest = null;
        this.extRequestHeaders = new HashMap<String, String>();
        this.extResponse = null;
        this.extResponseHeaders = new HashMap<String, String>();
        if (url != null) {
            params = this.getConsumer().signParameters(url.toExternalForm(), type,
                    this.getConsumer().getLtiVersion(), "POST", params);
            // Connect to tool consumer
            LTIMessage http = new LTIMessage(url.toExternalForm(), "POST", params);
            // Parse XML response
            if (http.send()) {
                this.extResponse = http.getResponse();
                this.extResponseHeaders = http.getResponseHeaders();
                try {
                    extDoc = LTIUtil.getXMLDoc(http.getResponse());
                    if (extDoc != null) {
                        Element el = LTIUtil.getXmlChild(extDoc.getRootElement(), "statusinfo");
                        ok = el != null;
                        if (ok) {
                            String responseCode = LTIUtil.getXmlChildValue(el, "codemajor");
                            ok = responseCode != null;
                            if (ok) {
                                ok = responseCode.equals("Success");
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            this.extRequest = http.getRequest();
            this.extRequestHeaders = http.getRequestHeaders();
        }

        return ok;

    }

    /**
     * Send a service request to the tool consumer.
     *
     * @param string type Message type value
     * @param string url  URL to send request to
     * @param string xml  XML of message request
     *
     * @return boolean True if the request successfully obtained a response
     */
    private boolean doLTI11Service(String type, String url, String xml) {

        boolean ok = false;
        this.extRequest = null;
        this.extRequestHeaders = null;
        this.extResponse = null;
        this.extResponseHeaders = null;
        if (StringUtils.isNotEmpty(url)) {
            String messageId = UUID.randomUUID().toString();
            String xmlRequest = "<?xml version = \"1.0\" encoding = \"UTF-8\"?>\n"
                    + "<imsx_POXEnvelopeRequest xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">\n"
                    + "  <imsx_POXHeader>\n" + "    <imsx_POXRequestHeaderInfo>\n"
                    + "      <imsx_version>V1.0</imsx_version>\n" + "      <imsx_messageIdentifier>" + id
                    + "</imsx_messageIdentifier>\n" + "    </imsx_POXRequestHeaderInfo>\n" + "  </imsx_POXHeader>\n"
                    + "  <imsx_POXBody>\n" + "    <" + type + "Request>\n" + xml + "    </" + type + "Request>\n"
                    + "  </imsx_POXBody>\n" + "</imsx_POXEnvelopeRequest>\n";
            // Calculate body hash
            String hash = Base64.encodeBase64String(DigestUtils.sha1(xmlRequest.toString()));
            Map<String, String> params = new HashMap<String, String>();
            params.put("oauth_body_hash", hash);
            HashSet<Map.Entry<String, String>> httpParams = new HashSet<Map.Entry<String, String>>();
            httpParams.addAll(params.entrySet());
            // Check for query parameters which need to be included in the signature
            Map<String, String> queryParams = new HashMap<String, String>();
            String urlNoQuery = url;
            try {
                URL uri = new URL(url);
                String query = uri.getQuery();
                if (query != null) {
                    urlNoQuery = urlNoQuery.substring(0, urlNoQuery.length() - query.length() - 1);
                    String[] queryItems = query.split("&");
                    for (int i = 0; i < queryItems.length; i++) {
                        String[] queryItem = queryItems[i].split("=", 2);
                        if (queryItem.length > 1) {
                            queryParams.put(queryItem[0], queryItem[1]);
                        } else {
                            queryParams.put(queryItem[0], "");
                        }
                    }
                    httpParams.addAll(queryParams.entrySet());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            // Add OAuth signature
            Map<String, String> header = new HashMap<String, String>();
            OAuthMessage oAuthMessage = new OAuthMessage("POST", urlNoQuery, httpParams);
            OAuthConsumer oAuthConsumer = new OAuthConsumer("about:blank", this.consumer.getKey(),
                    this.consumer.getSecret(), null);
            OAuthAccessor oAuthAccessor = new OAuthAccessor(oAuthConsumer);
            try {
                oAuthMessage.addRequiredParameters(oAuthAccessor);
                header.put("Authorization", oAuthMessage.getAuthorizationHeader(null));
                header.put("Content-Type", "application/xml");
            } catch (OAuthException e) {
            } catch (URISyntaxException e) {
            } catch (IOException e) {
            }
            try {
                StringEntity entity = new StringEntity(xmlRequest);

                // Connect to tool consumer
                this.extResponse = doPostRequest(url, LTIUtil.getHTTPParams(params), header, entity);
                // Parse XML response
                if (this.extResponse != null) {
                    this.extDoc = LTIUtil.getXMLDoc(extResponse);
                    ok = this.extDoc != null;
                    if (ok) {
                        Element el = LTIUtil.getXmlChild(this.extDoc.getRootElement(), "imsx_statusInfo");
                        ok = el != null;
                        if (ok) {
                            String responseCode = LTIUtil.getXmlChildValue(el, "imsx_codeMajor");
                            ok = responseCode != null;
                            if (ok) {
                                ok = responseCode.equals("success");
                            }
                        }
                    }
                    if (!ok) {
                        this.extResponse = null;
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        return (this.extResponse != null);

    }

    public int getPrimaryResourceLinkId() {
        return primaryResourceLinkId;
    }

    public void setPrimaryResourceLinkId(int primaryResourceLinkId) {
        this.primaryResourceLinkId = primaryResourceLinkId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getLtiResourceLinkId() {
        return ltiResourceLinkId;
    }

    public void setLtiResourceLinkId(String ltiResourceLinkId) {
        this.ltiResourceLinkId = ltiResourceLinkId;
    }

    public Map<String, GroupSet> getGroupSets() {
        return groupSets;
    }

    public void setGroupSets(Map<String, GroupSet> groupSets) {
        this.groupSets = groupSets;
    }

    public Map<String, Group> getGroups() {
        return groups;
    }

    public void setGroups(Map<String, Group> groups) {
        this.groups = groups;
    }

    public String getExtRequest() {
        return extRequest;
    }

    public void setExtRequest(String extRequest) {
        this.extRequest = extRequest;
    }

    public Map<String, String> getExtRequestHeaders() {
        return extRequestHeaders;
    }

    public void setExtRequestHeaders(Map<String, String> extRequestHeaders) {
        this.extRequestHeaders = extRequestHeaders;
    }

    public String getExtResponse() {
        return extResponse;
    }

    public void setExtResponse(String extResponse) {
        this.extResponse = extResponse;
    }

    public Map<String, String> getExtResponseHeaders() {
        return extResponseHeaders;
    }

    public void setExtResponseHeaders(Map<String, String> extResponseHeaders) {
        this.extResponseHeaders = extResponseHeaders;
    }

    public Boolean isShareApproved() {
        return shareApproved;
    }

    public void setShareApproved(Boolean shareApproved) {
        this.shareApproved = shareApproved;
    }

    public DateTime getCreated() {
        return created;
    }

    public void setCreated(DateTime created) {
        this.created = created;
    }

    public DateTime getUpdated() {
        return updated;
    }

    public void setUpdated(DateTime updated) {
        this.updated = updated;
    }

    public boolean isSettingsChanged() {
        return settingsChanged;
    }

    public void setSettingsChanged(boolean settingsChanged) {
        this.settingsChanged = settingsChanged;
    }

    public Document getExtDoc() {
        return extDoc;
    }

    public void setExtDoc(Document extDoc) {
        this.extDoc = extDoc;
    }

    public Map<String, List<String>> getExtNodes() {
        return extNodes;
    }

    public void setExtNodes(Map<String, List<String>> extNodes) {
        this.extNodes = extNodes;
    }

    public int getConsumerId() {
        return consumerId;
    }

    public void setConsumer(ToolConsumer consumer) {
        this.consumer = consumer;
    }

    public void setContext(Context context) {
        this.context = context;
    }

    public void setDataConnector(DataConnector dataConnector) {
        this.dataConnector = dataConnector;
    }

    /**
     * Returns the consumer key for the resource link with which this resource link is shared.
     *
     * @return consumer key
     */
    public String getPrimaryConsumerKey() {
        return this.primaryConsumerKey;
    }

    /**
     * Set the consumer key for the resource link with which this resource link is shared.
     *
     * @param primaryConsumerKey  consumer key
     */
    public void setPrimaryConsumerKey(String primaryConsumerKey) {
        this.primaryConsumerKey = primaryConsumerKey;
    }

    /**
     * Performs an HTTP POST request.
     *
     * @param url     URL to send request to
     * @param params  map of parameter values to be passed
     * @param header  values to include in the request header
     *
     * @return response returned from request, null if an error occurred
     */
    private String doPostRequest(String url, List<NameValuePair> params, Map<String, String> header,
            StringEntity entity) {

        String fileContent = null;
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(TIMEOUT)
                .setConnectionRequestTimeout(TIMEOUT).setSocketTimeout(TIMEOUT).build();
        CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig)
                .setRedirectStrategy(new DefaultRedirectStrategy()).build();
        HttpPost httpPost = new HttpPost(url);
        try {
            if (header != null) {
                for (String name : header.keySet()) {
                    params.add(new BasicNameValuePair(name, header.get(name)));
                }
            }
            httpPost.setEntity(new UrlEncodedFormEntity(params));
            HttpResponse hr = httpClient.execute(httpPost);
            if (hr.getStatusLine().getStatusCode() < 400) {
                BufferedReader rd = new BufferedReader(new InputStreamReader(hr.getEntity().getContent()));

                StringBuffer result = new StringBuffer();
                String line = "";
                while ((line = rd.readLine()) != null) {
                    result.append(line);
                }
                fileContent = result.toString();
            }
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
            fileContent = null;
        }

        return fileContent;

    }

    public void setSettingsMap(Map<String, String> settings2) {
        for (String key : settings2.keySet()) {
            LTIUtil.setParameter(settings, key, settings2.get(key));
        }

    }

}