All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.imsglobal.lti2.LTI2Util Maven / Gradle / Ivy

Go to download

BasicLTI Utilities are a set of utility classes to aid in the development of BasicLTI consumers and providers. They deal with much of the heavy lifting and make the process more opaque to the developer.

The newest version!
/*
 * $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 [email protected] $
 *
 * 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 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 theTools = new ArrayList ();
		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 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 capabilities) {
		capabilities.add("Person.email.primary");
	}

	public static void allowName(List 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 capabilities) {
		capabilities.add("Result.sourcedId");
		capabilities.add("Result.autocreate");
		capabilities.add("Result.url");
	}

	public static void allowSettings(List 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 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 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 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 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);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy