org.imsglobal.lti2.LTI2Util.java Source code

Java tutorial

Introduction

Here is the source code for org.imsglobal.lti2.LTI2Util.java

Source

/*
 * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/lti2/LTI2Util.java $
 * $Id: LTI2Util.java 134448 2014-02-12 18:32:12Z csev@umich.edu $
 *
 * Copyright (c) 2013 IMS GLobal Learning Consortium
 *
 * Licensed 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.
 */

package org.imsglobal.lti2;

import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;

import org.imsglobal.lti.BasicLTIUtil;
import org.imsglobal.lti2.objects.consumer.ServiceOffered;
import org.imsglobal.lti2.objects.consumer.StandardServices;
import org.imsglobal.lti2.objects.consumer.ToolConsumer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

public class LTI2Util {

    // We use the built-in Java logger because this code needs to be very generic
    private static Logger M_log = Logger.getLogger(LTI2Util.class.toString());

    public static final String SCOPE_LtiLink = "LtiLink";
    public static final String SCOPE_ToolProxyBinding = "ToolProxyBinding";
    public static final String SCOPE_ToolProxy = "ToolProxy";

    private static final String EMPTY_JSON_OBJECT = "{\n}\n";

    // Validate the incoming tool_services against a tool consumer
    public static String validateServices(ToolConsumer consumer, JSONObject providerProfile) {
        // Mostly to catch casting errors from bad JSON
        try {
            JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT);
            if (security_contract == null) {
                return "JSON missing security_contract";
            }
            JSONArray tool_services = (JSONArray) security_contract.get(LTI2Constants.TOOL_SERVICE);

            List<ServiceOffered> services_offered = consumer.getService_offered();

            if (tool_services != null)
                for (Object o : tool_services) {
                    JSONObject tool_service = (JSONObject) o;
                    String json_service = (String) tool_service.get(LTI2Constants.SERVICE);

                    boolean found = false;
                    for (ServiceOffered service : services_offered) {
                        String service_endpoint = service.getEndpoint();
                        if (service_endpoint.equals(json_service)) {
                            found = true;
                            break;
                        }
                    }
                    if (!found)
                        return "Service not allowed: " + json_service;
                }
            return null;
        } catch (Exception e) {
            return "Exception:" + e.getLocalizedMessage();
        }
    }

    // Validate incoming capabilities requested against out ToolConsumer
    public static String validateCapabilities(ToolConsumer consumer, JSONObject providerProfile) {
        List<Properties> theTools = new ArrayList<Properties>();
        Properties info = new Properties();

        // Mostly to catch casting errors from bad JSON
        try {
            String retval = parseToolProfile(theTools, info, providerProfile);
            if (retval != null)
                return retval;

            if (theTools.size() < 1)
                return "No tools found in profile";

            // Check all the capabilities requested by all the tools comparing against consumer
            List<String> capabilities = consumer.getCapability_offered();
            for (Properties theTool : theTools) {
                String ec = (String) theTool.get("enabled_capability");
                JSONArray enabled_capability = (JSONArray) JSONValue.parse(ec);
                if (enabled_capability != null)
                    for (Object o : enabled_capability) {
                        ec = (String) o;
                        if (capabilities.contains(ec))
                            continue;
                        return "Capability not permitted=" + ec;
                    }
            }
            return null;
        } catch (Exception e) {
            return "Exception:" + e.getLocalizedMessage();
        }
    }

    public static void allowEmail(List<String> capabilities) {
        capabilities.add("Person.email.primary");
    }

    public static void allowName(List<String> capabilities) {
        capabilities.add("User.username");
        capabilities.add("Person.name.fullname");
        capabilities.add("Person.name.given");
        capabilities.add("Person.name.family");
        capabilities.add("Person.name.full");
    }

    public static void allowResult(List<String> capabilities) {
        capabilities.add("Result.sourcedId");
        capabilities.add("Result.autocreate");
        capabilities.add("Result.url");
    }

    public static void allowSettings(List<String> capabilities) {
        capabilities.add("LtiLink.custom.url");
        capabilities.add("ToolProxy.custom.url");
        capabilities.add("ToolProxyBinding.custom.url");
    }

    // If this code looks like a hack - it is because the spec is a hack.
    // There are five possible scenarios for GET and two possible scenarios
    // for PUT.  I begged to simplify the business logic but was overrulled.
    // So we write obtuse code.
    @SuppressWarnings({ "unchecked", "unused" })
    public static Object getSettings(HttpServletRequest request, String scope, JSONObject link_settings,
            JSONObject binding_settings, JSONObject proxy_settings, String link_url, String binding_url,
            String proxy_url) {
        // Check to see if we are doing the bubble
        String bubbleStr = request.getParameter("bubble");
        String acceptHdr = request.getHeader("Accept");
        String contentHdr = request.getContentType();

        if (bubbleStr != null && bubbleStr.equals("all")
                && acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) < 0) {
            return "Simple format does not allow bubble=all";
        }

        if (SCOPE_LtiLink.equals(scope) || SCOPE_ToolProxyBinding.equals(scope) || SCOPE_ToolProxy.equals(scope)) {
            // All good
        } else {
            return "Bad Setttings Scope=" + scope;
        }

        boolean bubble = bubbleStr != null && "GET".equals(request.getMethod());
        boolean distinct = bubbleStr != null && "distinct".equals(bubbleStr);
        boolean bubbleAll = bubbleStr != null && "all".equals(bubbleStr);

        // Check our output format
        boolean acceptComplex = acceptHdr == null || acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) >= 0;

        if (distinct && link_settings != null && scope.equals(SCOPE_LtiLink)) {
            Iterator<String> i = link_settings.keySet().iterator();
            while (i.hasNext()) {
                String key = (String) i.next();
                if (binding_settings != null)
                    binding_settings.remove(key);
                if (proxy_settings != null)
                    proxy_settings.remove(key);
            }
        }

        if (distinct && binding_settings != null && scope.equals(SCOPE_ToolProxyBinding)) {
            Iterator<String> i = binding_settings.keySet().iterator();
            while (i.hasNext()) {
                String key = (String) i.next();
                if (proxy_settings != null)
                    proxy_settings.remove(key);
            }
        }

        // Lets get this party started...
        JSONObject jsonResponse = null;
        if ((distinct || bubbleAll) && acceptComplex) {
            jsonResponse = new JSONObject();
            jsonResponse.put(LTI2Constants.CONTEXT, StandardServices.TOOLSETTINGS_CONTEXT);
            JSONArray graph = new JSONArray();
            boolean started = false;
            if (link_settings != null && SCOPE_LtiLink.equals(scope)) {
                JSONObject cjson = new JSONObject();
                cjson.put(LTI2Constants.JSONLD_ID, link_url);
                cjson.put(LTI2Constants.TYPE, SCOPE_LtiLink);
                cjson.put(LTI2Constants.CUSTOM, link_settings);
                graph.add(cjson);
                started = true;
            }
            if (binding_settings != null && (started || SCOPE_ToolProxyBinding.equals(scope))) {
                JSONObject cjson = new JSONObject();
                cjson.put(LTI2Constants.JSONLD_ID, binding_url);
                cjson.put(LTI2Constants.TYPE, SCOPE_ToolProxyBinding);
                cjson.put(LTI2Constants.CUSTOM, binding_settings);
                graph.add(cjson);
                started = true;
            }
            if (proxy_settings != null && (started || SCOPE_ToolProxy.equals(scope))) {
                JSONObject cjson = new JSONObject();
                cjson.put(LTI2Constants.JSONLD_ID, proxy_url);
                cjson.put(LTI2Constants.TYPE, SCOPE_ToolProxy);
                cjson.put(LTI2Constants.CUSTOM, proxy_settings);
                graph.add(cjson);
            }
            jsonResponse.put(LTI2Constants.GRAPH, graph);

        } else if (distinct) { // Simple format output
            jsonResponse = proxy_settings;
            if (SCOPE_LtiLink.equals(scope)) {
                jsonResponse.putAll(binding_settings);
                jsonResponse.putAll(link_settings);
            } else if (SCOPE_ToolProxyBinding.equals(scope)) {
                jsonResponse.putAll(binding_settings);
            }
        } else { // bubble not specified
            jsonResponse = new JSONObject();
            jsonResponse.put(LTI2Constants.CONTEXT, StandardServices.TOOLSETTINGS_CONTEXT);
            JSONObject theSettings = null;
            String endpoint = null;
            if (SCOPE_LtiLink.equals(scope)) {
                endpoint = link_url;
                theSettings = link_settings;
            } else if (SCOPE_ToolProxyBinding.equals(scope)) {
                endpoint = binding_url;
                theSettings = binding_settings;
            }
            if (SCOPE_ToolProxy.equals(scope)) {
                endpoint = proxy_url;
                theSettings = proxy_settings;
            }
            if (acceptComplex) {
                JSONArray graph = new JSONArray();
                JSONObject cjson = new JSONObject();
                cjson.put(LTI2Constants.JSONLD_ID, endpoint);
                cjson.put(LTI2Constants.TYPE, scope);
                cjson.put(LTI2Constants.CUSTOM, theSettings);
                graph.add(cjson);
                jsonResponse.put(LTI2Constants.GRAPH, graph);
            } else {
                jsonResponse = theSettings;
            }
        }
        return jsonResponse;
    }

    // Parse a provider profile with lots of error checking...
    public static String parseToolProfile(List<Properties> theTools, Properties info, JSONObject jsonObject) {
        try {
            return parseToolProfileInternal(theTools, info, jsonObject);
        } catch (Exception e) {
            M_log.warning("Internal error parsing tool proxy\n" + jsonObject.toString());
            e.printStackTrace();
            return "Internal error parsing tool proxy:" + e.getLocalizedMessage();
        }
    }

    // Parse a provider profile with lots of error checking...
    @SuppressWarnings("unused")
    private static String parseToolProfileInternal(List<Properties> theTools, Properties info,
            JSONObject jsonObject) {
        Object o = null;
        JSONObject tool_profile = (JSONObject) jsonObject.get("tool_profile");
        if (tool_profile == null) {
            return "JSON missing tool_profile";
        }
        JSONObject product_instance = (JSONObject) tool_profile.get("product_instance");
        if (product_instance == null) {
            return "JSON missing product_instance";
        }

        String instance_guid = (String) product_instance.get("guid");
        if (instance_guid == null) {
            return "JSON missing product_info / guid";
        }
        info.put("instance_guid", instance_guid);

        JSONObject product_info = (JSONObject) product_instance.get("product_info");
        if (product_info == null) {
            return "JSON missing product_info";
        }

        // Look for required fields
        JSONObject product_name = product_info == null ? null : (JSONObject) product_info.get("product_name");
        String productTitle = product_name == null ? null : (String) product_name.get("default_value");
        JSONObject description = product_info == null ? null : (JSONObject) product_info.get("description");
        String productDescription = description == null ? null : (String) description.get("default_value");

        JSONObject product_family = product_info == null ? null : (JSONObject) product_info.get("product_family");
        String productCode = product_family == null ? null : (String) product_family.get("code");
        JSONObject product_vendor = product_family == null ? null : (JSONObject) product_family.get("vendor");
        description = product_vendor == null ? null : (JSONObject) product_vendor.get("description");
        String vendorDescription = description == null ? null : (String) description.get("default_value");
        String vendorCode = product_vendor == null ? null : (String) product_vendor.get("code");

        if (productTitle == null || productDescription == null) {
            return "JSON missing product_name or description ";
        }
        if (productCode == null || vendorCode == null || vendorDescription == null) {
            return "JSON missing product code, vendor code or description";
        }

        info.put("product_name", productTitle);
        info.put("description", productDescription); // Backwards compatibility
        info.put("product_description", productDescription);
        info.put("product_code", productCode);
        info.put("vendor_code", vendorCode);
        info.put("vendor_description", vendorDescription);

        o = tool_profile.get("base_url_choice");
        if (!(o instanceof JSONArray) || o == null) {
            return "JSON missing base_url_choices";
        }
        JSONArray base_url_choices = (JSONArray) o;

        String secure_base_url = null;
        String default_base_url = null;
        for (Object i : base_url_choices) {
            JSONObject url_choice = (JSONObject) i;
            secure_base_url = (String) url_choice.get("secure_base_url");
            default_base_url = (String) url_choice.get("default_base_url");
        }

        String launch_url = secure_base_url;
        if (launch_url == null)
            launch_url = default_base_url;
        if (launch_url == null) {
            return "Unable to determine launch URL";
        }

        o = (JSONArray) tool_profile.get("resource_handler");
        if (!(o instanceof JSONArray) || o == null) {
            return "JSON missing resource_handlers";
        }
        JSONArray resource_handlers = (JSONArray) o;

        // Loop through resource handlers, read, and check for errors
        for (Object i : resource_handlers) {
            JSONObject resource_handler = (JSONObject) i;
            JSONObject resource_type_json = (JSONObject) resource_handler.get("resource_type");
            String resource_type_code = (String) resource_type_json.get("code");
            if (resource_type_code == null) {
                return "JSON missing resource_type code";
            }
            o = (JSONArray) resource_handler.get("message");
            if (!(o instanceof JSONArray) || o == null) {
                return "JSON missing resource_handler / message";
            }
            JSONArray messages = (JSONArray) o;

            JSONObject titleObject = (JSONObject) resource_handler.get("name");
            String title = titleObject == null ? null : (String) titleObject.get("default_value");
            if (title == null || titleObject == null) {
                return "JSON missing resource_handler / name / default_value";
            }

            JSONObject buttonObject = (JSONObject) resource_handler.get("short_name");
            String button = buttonObject == null ? null : (String) buttonObject.get("default_value");

            JSONObject descObject = (JSONObject) resource_handler.get("description");
            String resourceDescription = descObject == null ? null : (String) descObject.get("default_value");

            String path = null;
            JSONArray parameter = null;
            JSONArray enabled_capability = null;
            for (Object m : messages) {
                JSONObject message = (JSONObject) m;
                String message_type = (String) message.get("message_type");
                if (!"basic-lti-launch-request".equals(message_type))
                    continue;
                if (path != null) {
                    return "A resource_handler cannot have more than one basic-lti-launch-request message RT="
                            + resource_type_code;
                }
                path = (String) message.get("path");
                if (path == null) {
                    return "A basic-lti-launch-request message must have a path RT=" + resource_type_code;
                }
                o = (JSONArray) message.get("parameter");
                if (!(o instanceof JSONArray)) {
                    return "Must be an array: parameter RT=" + resource_type_code;
                }
                parameter = (JSONArray) o;

                o = (JSONArray) message.get("enabled_capability");
                if (!(o instanceof JSONArray)) {
                    return "Must be an array: enabled_capability RT=" + resource_type_code;
                }
                enabled_capability = (JSONArray) o;
            }

            // Ignore everything except launch handlers
            if (path == null)
                continue;

            // Check the URI
            String thisLaunch = launch_url;
            if (!thisLaunch.endsWith("/") && !path.startsWith("/"))
                thisLaunch = thisLaunch + "/";
            thisLaunch = thisLaunch + path;
            try {
                URL url = new URL(thisLaunch);
            } catch (Exception e) {
                return "Bad launch URL=" + thisLaunch;
            }

            // Passed all the tests...  Lets keep it...
            Properties theTool = new Properties();

            theTool.put("resource_type", resource_type_code); // Backwards compatibility
            theTool.put("resource_type_code", resource_type_code);
            if (title == null)
                title = productTitle;
            if (title != null)
                theTool.put("title", title);
            if (button != null)
                theTool.put("button", button);
            if (resourceDescription == null)
                resourceDescription = productDescription;
            if (resourceDescription != null)
                theTool.put("description", resourceDescription);
            if (parameter != null)
                theTool.put("parameter", parameter.toString());
            if (enabled_capability != null)
                theTool.put("enabled_capability", enabled_capability.toString());
            theTool.put("launch", thisLaunch);
            theTools.add(theTool);
        }
        return null; // All good
    }

    public static JSONObject parseSettings(String settings) {
        if (settings == null || settings.length() < 1) {
            settings = EMPTY_JSON_OBJECT;
        }
        return (JSONObject) JSONValue.parse(settings);
    }

    /* Two possible formats:
        
       key=val;key2=val2;
           
       key=val
       key2=val2
    */
    public static boolean mergeLTI1Custom(Properties custom, String customstr) {
        if (customstr == null || customstr.length() < 1)
            return true;

        String[] params = customstr.split("[\n;]");
        for (int i = 0; i < params.length; i++) {
            String param = params[i];
            if (param == null)
                continue;
            if (param.length() < 1)
                continue;

            int pos = param.indexOf("=");
            if (pos < 1)
                continue;
            if (pos + 1 > param.length())
                continue;
            String key = mapKeyName(param.substring(0, pos));
            if (key == null)
                continue;

            if (custom.containsKey(key))
                continue;

            String value = param.substring(pos + 1);
            if (value == null)
                continue;
            value = value.trim();
            if (value.length() < 1)
                continue;
            setProperty(custom, key, value);
        }
        return true;
    }

    /*
      "custom" : 
      {
       "isbn" : "978-0321558145",
       "style" : "jazzy"
      }
    */
    public static boolean mergeLTI2Custom(Properties custom, String customstr) {
        if (customstr == null || customstr.length() < 1)
            return true;
        JSONObject json = null;
        try {
            json = (JSONObject) JSONValue.parse(customstr.trim());
        } catch (Exception e) {
            M_log.warning("mergeLTI2Custom could not parse\n" + customstr);
            M_log.warning(e.getLocalizedMessage());
            return false;
        }

        // This could happen if the old settings service was used
        // on an LTI 2.x placement to put in settings that are not
        // JSON - we just ignore it.
        if (json == null)
            return false;
        Iterator<?> keys = json.keySet().iterator();
        while (keys.hasNext()) {
            String key = (String) keys.next();
            if (custom.containsKey(key))
                continue;
            Object value = json.get(key);
            if (value instanceof String) {
                setProperty(custom, key, (String) value);
            }
        }
        return true;
    }

    /*
      "parameter" : 
      [
       { "name" : "result_url",
    "variable" : "Result.url"
       },
       { "name" : "discipline",
    "fixed" : "chemistry"
       }
      ]
    */
    public static boolean mergeLTI2Parameters(Properties custom, String customstr) {
        if (customstr == null || customstr.length() < 1)
            return true;
        JSONArray json = null;
        try {
            json = (JSONArray) JSONValue.parse(customstr.trim());
        } catch (Exception e) {
            M_log.warning("mergeLTI2Parameters could not parse\n" + customstr);
            M_log.warning(e.getLocalizedMessage());
            return false;
        }
        Iterator<?> parameters = json.iterator();
        while (parameters.hasNext()) {
            Object o = parameters.next();
            JSONObject parameter = null;
            try {
                parameter = (JSONObject) o;
            } catch (Exception e) {
                M_log.warning("mergeLTI2Parameters did not find list of objects\n" + customstr);
                M_log.warning(e.getLocalizedMessage());
                return false;
            }

            String name = (String) parameter.get("name");

            if (name == null)
                continue;
            if (custom.containsKey(name))
                continue;
            String fixed = (String) parameter.get("fixed");
            String variable = (String) parameter.get("variable");
            if (variable != null) {
                setProperty(custom, name, variable);
                continue;
            }
            if (fixed != null) {
                setProperty(custom, name, fixed);
            }
        }
        return true;
    }

    public static void substituteCustom(Properties custom, Properties lti2subst) {
        if (custom == null || lti2subst == null)
            return;
        Enumeration<?> e = custom.propertyNames();
        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            String value = custom.getProperty(key);
            if (value == null || value.length() < 1)
                continue;
            String newValue = lti2subst.getProperty(value);
            if (newValue == null || newValue.length() < 1)
                continue;
            setProperty(custom, key, (String) newValue);
        }
    }

    // Place the custom values into the launch
    public static void addCustomToLaunch(Properties ltiProps, Properties custom) {
        Enumeration<?> e = custom.propertyNames();
        while (e.hasMoreElements()) {
            String keyStr = (String) e.nextElement();
            String value = custom.getProperty(keyStr);
            setProperty(ltiProps, "custom_" + keyStr, value);
        }
    }

    @SuppressWarnings("deprecation")
    public static void setProperty(Properties props, String key, String value) {
        BasicLTIUtil.setProperty(props, key, value);
    }

    public static String mapKeyName(String keyname) {
        return BasicLTIUtil.mapKeyName(keyname);
    }

}