/*
* $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/basiclti/BasicLTIUtil.java $
* $Id: BasicLTIUtil.java 131989 2013-11-27 01:36:00Z csev@umich.edu $
*
* Copyright (c) 2008 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.basiclti;
import static org.imsglobal.basiclti.BasicLTIConstants.CUSTOM_PREFIX;
import static org.imsglobal.basiclti.BasicLTIConstants.EXTENSION_PREFIX;
import static org.imsglobal.basiclti.BasicLTIConstants.LTI_MESSAGE_TYPE;
import static org.imsglobal.basiclti.BasicLTIConstants.LTI_VERSION;
import static org.imsglobal.basiclti.BasicLTIConstants.OAUTH_PREFIX;
import static org.imsglobal.basiclti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL;
import static org.imsglobal.basiclti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_DESCRIPTION;
import static org.imsglobal.basiclti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_GUID;
import static org.imsglobal.basiclti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_NAME;
import static org.imsglobal.basiclti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.OAuthValidator;
import net.oauth.SimpleOAuthValidator;
import net.oauth.server.OAuthServlet;
import net.oauth.signature.OAuthSignatureMethod;
/* Leave out until we have JTidy 0.8 in the repository
import org.w3c.tidy.Tidy;
import java.io.ByteArrayOutputStream;
*/
/**
* Some Utility code for IMS LTI
* http://www.anyexample.com/programming/java
* /java_simple_class_to_compute_sha_1_hash.xml
* <p>
* Sample Descriptor
*
* <pre>
* <?xml version="1.0" encoding="UTF-8"?>
* <basic_lti_link xmlns="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
* <title>generated by tp+user</title>
* <description>generated by tp+user</description>
* <custom>
* <parameter key="keyname">value</parameter>
* </custom>
* <extensions platform="www.lms.com">
* <parameter key="keyname">value</parameter>
* </extensions>
* <launch_url>url to the basiclti launch URL</launch_url>
* <secure_launch_url>url to the basiclti launch URL</secure_launch_url>
* <icon>url to an icon for this tool (optional)</icon>
* <secure_icon>url to an icon for this tool (optional)</secure_icon>
* <cartridge_icon identifierref="BLTI001_Icon"/>
* <vendor>
* <code>vendor.com</code>
* <name>Vendor Name</name>
* <description>
* This is a Grade Book that supports many column types.
* </description>
* <contact>
* <email>support@vendor.com</email>
* </contact>
* <url>http://www.vendor.com/product</url>
* </vendor>
* </basic_lti_link>
* </pre>
*/
public class BasicLTIUtil {
// We use the built-in Java logger because this code needs to be very generic
private static Logger M_log = Logger.getLogger(BasicLTIUtil.class.toString());
/** To turn on really verbose debugging */
private static boolean verbosePrint = false;
public static final String BASICLTI_SUBMIT = "ext_basiclti_submit";
private static final Pattern CUSTOM_REGEX = Pattern.compile("[^A-Za-z0-9]");
private static final String UNDERSCORE = "_";
// Simple Debug Print Mechanism
public static void dPrint(String str) {
if (verbosePrint)
System.out.println(str);
M_log.fine(str);
}
// expected_oauth_key can be null - if it is non-null it must match the key in the request
public static Object validateMessage(HttpServletRequest request, String URL,
String oauth_secret, String expected_oauth_key)
{
OAuthMessage oam = OAuthServlet.getMessage(request, URL);
String oauth_consumer_key = null;
try {
oauth_consumer_key = oam.getConsumerKey();
} catch (Exception e) {
return "Unable to find consumer key";
}
if ( expected_oauth_key != null && ! expected_oauth_key.equals(oauth_consumer_key) ) {
return "Incorrect consumer key";
}
OAuthValidator oav = new SimpleOAuthValidator();
OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key,oauth_secret, null);
OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (Exception e) {
return "Unable to find base string";
}
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
if (base_string != null) {
return "Failed to validate: "+e.getLocalizedMessage()+"\nBase String\n"+base_string;
}
return "Failed to validate: "+e.getLocalizedMessage();
}
return Boolean.TRUE;
}
public static String validateDescriptor(String descriptor) {
if (descriptor == null)
return null;
if (descriptor.indexOf("<basic_lti_link") < 0)
return null;
Map<String, Object> tm = XMLMap.getFullMap(descriptor.trim());
if (tm == null)
return null;
// We demand at least an endpoint
String ltiSecureLaunch = XMLMap.getString(tm,
"/basic_lti_link/secure_launch_url");
// We demand at least an endpoint
if (ltiSecureLaunch != null && ltiSecureLaunch.trim().length() > 0)
return ltiSecureLaunch;
String ltiLaunch = XMLMap.getString(tm, "/basic_lti_link/launch_url");
if (ltiLaunch != null && ltiLaunch.trim().length() > 0)
return ltiLaunch;
return null;
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics. NOTE: no blacklisting of keys is
* performed.
*
* @param rawProperties
* A set of properties that will be cleaned.
* @return A cleansed version of rawProperties.
*/
public static Map<String, String> cleanupProperties(
final Map<String, String> rawProperties) {
return cleanupProperties(rawProperties, null);
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics.
*
* @param rawProperties
* A set of properties that will be cleaned.
* @param blackList
* An array of {@link String}s which are considered unsafe to be
* included in launch data. Any matches will be removed from the
* return.
* @return A cleansed version of rawProperties.
*/
public static Map<String, String> cleanupProperties(
final Map<String, String> rawProperties, final String[] blackList) {
final Map<String, String> newProp = new HashMap<String, String>(
rawProperties.size()); // roughly the same size
for (String okey : rawProperties.keySet()) {
final String key = okey.trim();
if (blackList != null) {
boolean blackListed = false;
for (String blackKey : blackList) {
if (blackKey.equals(key)) {
blackListed = true;
break;
}
}
if (blackListed) {
continue;
}
}
final String value = rawProperties.get(key);
if (value == null || "".equals(value)) {
// remove null or empty values
continue;
}
if (isSpecifiedPropertyName(key)) {
// a well known property name
newProp.put(key, value);
} else {
// convert to a custom property name
newProp.put(adaptToCustomPropertyName(key), value);
}
}
return newProp;
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics.
*
* @deprecated See {@link #cleanupProperties(Map)}
* @param rawProperties
* A set of {@link Properties} that will be cleaned. Keys must be of
* type {@link String}.
* @return A cleansed version of {@link Properties}.
*/
public static Properties cleanupProperties(final Properties rawProperties) {
final Map<String, String> map = cleanupProperties(
convertToMap(rawProperties), null);
return convertToProperties(map);
}
/**
* Checks to see if the passed propertyName is equal to one of the Strings
* contained in {@link BasicLTIConstants#validPropertyNames}. String matching
* is case sensitive.
*
* @param propertyName
* @return true if propertyName is equal to one of the Strings contained in
* {@link BasicLTIConstants#validPropertyNames}
* or is a custom parameter oe extension parameter ;
* else return false.
*/
public static boolean isSpecifiedPropertyName(final String propertyName) {
boolean found = false;
if ( propertyName.startsWith(CUSTOM_PREFIX) ) return true;
if ( propertyName.startsWith(EXTENSION_PREFIX) ) return true;
if ( propertyName.startsWith(OAUTH_PREFIX) ) return true;
for (String key : BasicLTIConstants.validPropertyNames) {
if (key.equals(propertyName)) {
found = true;
break;
}
}
return found;
}
/**
* A simple utility method which implements the specified semantics of custom
* properties.
* <p>
* i.e. The parameter names are mapped to lower case and any character that is
* neither a number nor letter in a parameter name is replaced with an
* "underscore".
* <p>
* e.g. Review:Chapter=1.2.56 would map to custom_review_chapter=1.2.56.
*
* @param propertyName
* @return
*/
public static String adaptToCustomPropertyName(final String propertyName) {
if (propertyName == null || "".equals(propertyName)) {
throw new IllegalArgumentException("propertyName cannot be null");
}
String customName = propertyName.toLowerCase();
customName = CUSTOM_REGEX.matcher(customName).replaceAll(UNDERSCORE);
if (!customName.startsWith(CUSTOM_PREFIX)) {
customName = CUSTOM_PREFIX + customName;
}
return customName;
}
/**
* Add the necessary fields and sign.
*
* @deprecated See:
* {@link BasicLTIUtil#signProperties(Map, String, String, String, String, String, String, String, String, String)}
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @param org_id
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_GUID}
* @param org_desc
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_DESCRIPTION}
* @param org_url
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_URL}
* @return
*/
public static Properties signProperties(Properties postProp, String url,
String method, String oauth_consumer_key, String oauth_consumer_secret,
String org_id, String org_desc, String org_url) {
final Map<String, String> signedMap = signProperties(
convertToMap(postProp), url, method, oauth_consumer_key,
oauth_consumer_secret, org_id, org_desc, org_url, null, null);
return convertToProperties(signedMap);
}
/**
* Add the necessary fields and sign.
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @param tool_consumer_instance_guid
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_GUID}
* @param tool_consumer_instance_description
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_DESCRIPTION}
* @param tool_consumer_instance_url
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_URL}
* @param tool_consumer_instance_name
* See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_NAME}
* @param tool_consumer_instance_contact_email
* See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL}
* @return
*/
public static Map<String, String> signProperties(
Map<String, String> postProp, String url, String method,
String oauth_consumer_key, String oauth_consumer_secret,
String tool_consumer_instance_guid,
String tool_consumer_instance_description,
String tool_consumer_instance_url, String tool_consumer_instance_name,
String tool_consumer_instance_contact_email) {
postProp = BasicLTIUtil.cleanupProperties(postProp);
if ( postProp.get(LTI_VERSION) == null ) postProp.put(LTI_VERSION, "LTI-1p0");
if ( postProp.get(LTI_MESSAGE_TYPE) == null ) postProp.put(LTI_MESSAGE_TYPE, "basic-lti-launch-request");
// Allow caller to internationalize this for us...
if (postProp.get(BASICLTI_SUBMIT) == null) {
postProp.put(BASICLTI_SUBMIT, "Launch Endpoint with BasicLTI Data");
}
if (tool_consumer_instance_guid != null)
postProp.put(TOOL_CONSUMER_INSTANCE_GUID, tool_consumer_instance_guid);
if (tool_consumer_instance_description != null)
postProp.put(TOOL_CONSUMER_INSTANCE_DESCRIPTION,
tool_consumer_instance_description);
if (tool_consumer_instance_url != null)
postProp.put(TOOL_CONSUMER_INSTANCE_URL, tool_consumer_instance_url);
if (tool_consumer_instance_name != null)
postProp.put(TOOL_CONSUMER_INSTANCE_NAME, tool_consumer_instance_name);
if (tool_consumer_instance_contact_email != null)
postProp.put(TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL,
tool_consumer_instance_contact_email);
if (postProp.get("oauth_callback") == null)
postProp.put("oauth_callback", "about:blank");
if (oauth_consumer_key == null || oauth_consumer_secret == null) {
dPrint("No signature generated in signProperties");
return postProp;
}
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet());
OAuthConsumer cons = new OAuthConsumer("about:blank", oauth_consumer_key,
oauth_consumer_secret, null);
OAuthAccessor acc = new OAuthAccessor(cons);
try {
oam.addRequiredParameters(acc);
// System.out.println("Base Message String\n"+OAuthSignatureMethod.getBaseString(oam)+"\n");
List<Map.Entry<String, String>> params = oam.getParameters();
Map<String, String> nextProp = new HashMap<String, String>();
// Convert to Map<String, String>
for (final Map.Entry<String, String> entry : params) {
nextProp.put(entry.getKey(), entry.getValue());
}
return nextProp;
} catch (net.oauth.OAuthException e) {
M_log.warning("BasicLTIUtil.signProperties OAuth Exception "
+ e.getMessage());
throw new Error(e);
} catch (java.io.IOException e) {
M_log.warning("BasicLTIUtil.signProperties IO Exception "
+ e.getMessage());
throw new Error(e);
} catch (java.net.URISyntaxException e) {
M_log.warning("BasicLTIUtil.signProperties URI Syntax Exception "
+ e.getMessage());
throw new Error(e);
}
}
/**
* Check if the properties are properly signed
*
* @deprecated See:
* {@link BasicLTIUtil#checkProperties(Map, String, String, String, String, String, String, String, String, String)}
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @return
*/
public static boolean checkProperties(Properties postProp, String url,
String method, String oauth_consumer_key, String oauth_consumer_secret)
{
return checkProperties( convertToMap(postProp), url, method,
oauth_consumer_key, oauth_consumer_secret);
}
/**
* Check if the fields are properly signed
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @return
*/
public static boolean checkProperties(
Map<String, String> postProp, String url, String method,
String oauth_consumer_key, String oauth_consumer_secret) {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet());
OAuthConsumer cons = new OAuthConsumer("about:blank", oauth_consumer_key,
oauth_consumer_secret, null);
OAuthValidator oav = new SimpleOAuthValidator();
OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (Exception e) {
M_log.warning(e.getLocalizedMessage());
base_string = null;
return false;
}
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
M_log.warning("Provider failed to validate message");
M_log.warning(e.getLocalizedMessage());
if (base_string != null) {
M_log.warning(base_string);
}
return false;
}
return true;
}
/**
* Create the HTML to render a POST form and then automatically submit it.
* Make sure to call {@link #cleanupProperties(Properties)} before signing.
*
* @deprecated Moved to {@link #postLaunchHTML(Map, String, boolean)}
* @param cleanProperties
* Assumes you have called {@link #cleanupProperties(Properties)}
* beforehand.
* @param endpoint
* The LTI launch url.
* @param debug
* Useful for viewing the HTML before posting to end point.
* @return the HTML ready for IFRAME src = inclusion.
*/
public static String postLaunchHTML(final Properties cleanProperties,
String endpoint, boolean debug) {
Map<String, String> map = convertToMap(cleanProperties);
return postLaunchHTML(map, endpoint, debug);
}
/**
* Create the HTML to render a POST form and then automatically submit it.
* Make sure to call {@link #cleanupProperties(Properties)} before signing.
*
* @param cleanProperties
* Assumes you have called {@link #cleanupProperties(Properties)}
* beforehand.
* @param endpoint
* The LTI launch url.
* @param debug
* Useful for viewing the HTML before posting to end point.
* @return the HTML ready for IFRAME src = inclusion.
*/
public static String postLaunchHTML(
final Map<String, String> cleanProperties, String endpoint, boolean debug) {
if (cleanProperties == null || cleanProperties.isEmpty()) {
throw new IllegalArgumentException(
"cleanProperties == null || cleanProperties.isEmpty()");
}
if (endpoint == null) {
throw new IllegalArgumentException("endpoint == null");
}
Map<String, String> newMap = null;
if (debug) {
// sort the properties for readability
newMap = new TreeMap<String, String>(cleanProperties);
} else {
newMap = cleanProperties;
}
StringBuilder text = new StringBuilder();
// paint form
text.append("<div id=\"ltiLaunchFormSubmitArea\">\n");
text.append("<form action=\"");
text.append(endpoint);
text.append("\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" ");
text.append(" encType=\"application/x-www-form-urlencoded\" accept-charset=\"utf-8\">\n");
for (Entry<String, String> entry : newMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null)
continue;
// This will escape the contents pretty much - at least
// we will be safe and not generate dangerous HTML
key = htmlspecialchars(key);
value = htmlspecialchars(value);
if (key.equals(BASICLTI_SUBMIT)) {
text.append("<input type=\"submit\" name=\"");
} else {
text.append("<input type=\"hidden\" name=\"");
}
text.append(key);
text.append("\" value=\"");
text.append(value);
text.append("\"/>\n");
}
text.append("</form>\n");
text.append("</div>\n");
// Paint the auto-pop up if we are transitioning from https: to http:
// and are not already the top frame...
text.append("<script type=\"text/javascript\">\n");
text.append("if (window.top!=window.self) {\n");
text.append(" theform = document.getElementById('ltiLaunchForm');\n");
text.append(" if ( theform && theform.action ) {\n");
text.append(" formAction = theform.action;\n");
text.append(" ourUrl = window.location.href;\n");
text.append(" if ( formAction.indexOf('http://') == 0 && ourUrl.indexOf('https://') == 0 ) {\n");
text.append(" theform.target = '_blank';\n");
text.append(" window.console && console.log('Launching http from https in new window!');\n");
text.append(" }\n");
text.append(" }\n");
text.append("}\n");
text.append("</script>\n");
// paint debug output
if (debug) {
text.append("<pre>\n");
text.append("<b>BasicLTI Endpoint</b>\n");
text.append(endpoint);
text.append("\n\n");
text.append("<b>BasicLTI Parameters:</b>\n");
for (Entry<String, String> entry : newMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null)
continue;
text.append(htmlspecialchars(key));
text.append("=");
text.append(htmlspecialchars(value));
text.append("\n");
}
text.append("</pre>\n");
} else {
// paint auto submit script
text
.append(" <script language=\"javascript\"> \n"
+ " document.getElementById(\"ltiLaunchFormSubmitArea\").style.display = \"none\";\n"
+ " nei = document.createElement('input');\n"
+ " nei.setAttribute('type', 'hidden');\n"
+ " nei.setAttribute('name', '"
+ BASICLTI_SUBMIT
+ "');\n"
+ " nei.setAttribute('value', '"
+ newMap.get(BASICLTI_SUBMIT)
+ "');\n"
+ " document.getElementById(\"ltiLaunchForm\").appendChild(nei);\n"
+ " document.ltiLaunchForm.submit(); \n" + " </script> \n");
}
String htmltext = text.toString();
return htmltext;
}
/**
* @deprecated See: {@link #parseDescriptor(Map, Map, String)}
* @param launch_info
* Variable is mutated by this method.
* @param postProp
* Variable is mutated by this method.
* @param descriptor
* @return
*/
public static boolean parseDescriptor(Properties launch_info,
Properties postProp, String descriptor) {
// this is an ugly copy/paste of the non-@deprecated method
// could not convert data types as they variables get mutated (ugh)
Map<String, Object> tm = null;
try {
tm = XMLMap.getFullMap(descriptor.trim());
} catch (Exception e) {
M_log.warning("BasicLTIUtil exception parsing BasicLTI descriptor: "
+ e.getMessage());
return false;
}
if (tm == null) {
M_log.warning("Unable to parse XML in parseDescriptor");
return false;
}
String launch_url = toNull(XMLMap.getString(tm,
"/basic_lti_link/launch_url"));
String secure_launch_url = toNull(XMLMap.getString(tm,
"/basic_lti_link/secure_launch_url"));
if (launch_url == null && secure_launch_url == null)
return false;
setProperty(launch_info, "launch_url", launch_url);
setProperty(launch_info, "secure_launch_url", secure_launch_url);
// Extensions for hand-authored placements - The export process should scrub
// these
setProperty(launch_info, "key", toNull(XMLMap.getString(tm,
"/basic_lti_link/x-secure/launch_key")));
setProperty(launch_info, "secret", toNull(XMLMap.getString(tm,
"/basic_lti_link/x-secure/launch_secret")));
List<Map<String, Object>> theList = XMLMap.getList(tm,
"/basic_lti_link/custom/parameter");
for (Map<String, Object> setting : theList) {
dPrint("Setting=" + setting);
String key = XMLMap.getString(setting, "/!key"); // Get the key attribute
String value = XMLMap.getString(setting, "/"); // Get the value
if (key == null || value == null)
continue;
key = "custom_" + mapKeyName(key);
dPrint("key=" + key + " val=" + value);
postProp.setProperty(key, value);
}
return true;
}
/**
*
* @param launch_info
* Variable is mutated by this method.
* @param postProp
* Variable is mutated by this method.
* @param descriptor
* @return
*/
public static boolean parseDescriptor(Map<String, String> launch_info,
Map<String, String> postProp, String descriptor) {
Map<String, Object> tm = null;
try {
tm = XMLMap.getFullMap(descriptor.trim());
} catch (Exception e) {
M_log.warning("BasicLTIUtil exception parsing BasicLTI descriptor: "
+ e.getMessage());
return false;
}
if (tm == null) {
M_log.warning("Unable to parse XML in parseDescriptor");
return false;
}
String launch_url = toNull(XMLMap.getString(tm,
"/basic_lti_link/launch_url"));
String secure_launch_url = toNull(XMLMap.getString(tm,
"/basic_lti_link/secure_launch_url"));
if (launch_url == null && secure_launch_url == null)
return false;
setProperty(launch_info, "launch_url", launch_url);
setProperty(launch_info, "secure_launch_url", secure_launch_url);
// Extensions for hand-authored placements - The export process should scrub
// these
setProperty(launch_info, "key", toNull(XMLMap.getString(tm,
"/basic_lti_link/x-secure/launch_key")));
setProperty(launch_info, "secret", toNull(XMLMap.getString(tm,
"/basic_lti_link/x-secure/launch_secret")));
List<Map<String, Object>> theList = XMLMap.getList(tm,
"/basic_lti_link/custom/parameter");
for (Map<String, Object> setting : theList) {
dPrint("Setting=" + setting);
String key = XMLMap.getString(setting, "/!key"); // Get the key attribute
String value = XMLMap.getString(setting, "/"); // Get the value
if (key == null || value == null)
continue;
key = "custom_" + mapKeyName(key);
dPrint("key=" + key + " val=" + value);
postProp.put(key, value);
}
return true;
}
// Remove fields that should not be exported
public static String prepareForExport(String descriptor) {
Map<String, Object> tm = null;
try {
tm = XMLMap.getFullMap(descriptor.trim());
} catch (Exception e) {
M_log.warning("BasicLTIUtil exception parsing BasicLTI descriptor"
+ e.getMessage());
return null;
}
if (tm == null) {
M_log.warning("Unable to parse XML in prepareForExport");
return null;
}
XMLMap.removeSubMap(tm, "/basic_lti_link/x-secure");
String retval = XMLMap.getXML(tm, true);
return retval;
}
/**
* The parameter name is mapped to lower case and any character that is
* neither a number or letter is replaced with an "underscore". So if a custom
* entry was as follows:
*
* <parameter name="Vendor:Chapter">1.2.56</parameter>
*
* Would map to: custom_vendor_chapter=1.2.56
*/
public static String mapKeyName(String keyname) {
StringBuffer sb = new StringBuffer();
if (keyname == null)
return null;
keyname = keyname.trim();
if (keyname.length() < 1)
return null;
for (int i = 0; i < keyname.length(); i++) {
Character ch = Character.toLowerCase(keyname.charAt(i));
if (Character.isLetter(ch) || Character.isDigit(ch)) {
sb.append(ch);
} else {
sb.append('_');
}
}
return sb.toString();
}
public static String toNull(String str) {
if (str == null)
return null;
if (str.trim().length() < 1)
return null;
return str;
}
/**
* Mutates the passed Map<String, String> map variable. Puts the key,value
* into the Map if the value is not null and is not empty.
*
* @param map
* Variable is mutated by this method.
* @param key
* @param value
*/
public static void setProperty(final Map<String, String> map,
final String key, final String value) {
if (value != null && !"".equals(value)) {
map.put(key, value);
}
}
/**
* Mutates the passed Properties props variable. Puts the key,value into the
* Map if the value is not null and is not empty.
*
* @deprecated See: {@link #setProperty(Map, String, String)}
* @param props
* Variable is mutated by this method.
* @param key
* @param value
*/
public static void setProperty(Properties props, String key, String value) {
if (value == null) return;
if (value.trim().length() < 1) return;
props.setProperty(key, value);
}
// Basic utility to encode form text - handle the "safe cases"
public static String htmlspecialchars(String input) {
if (input == null)
return null;
String retval = input.replace("&", "&");
retval = retval.replace("\"", """);
retval = retval.replace("<", "<");
retval = retval.replace(">", ">");
retval = retval.replace(">", ">");
retval = retval.replace("=", "=");
return retval;
}
/**
* Simple utility method deal with a request that has the wrong URL when behind
* a proxy.
*
* @param request
* @param extUrl
* The url that the external world sees us as responding to. This needs to be
* up to but not including the last slash like and not include any path information
* http://www.sakaiproject.org - although we do compensate for extra stuff at the end.
* @return
* The full path of the request with extUrl in place of whatever the request
* thinks is the current URL.
*/
static public String getRealPath(String servletUrl, String extUrl)
{
Pattern pat = Pattern.compile("^https??://[^/]*");
// Deal with potential bad extUrl formats
Matcher m = pat.matcher(extUrl);
if (m.find()) {
extUrl = m.group(0);
}
String retval = pat.matcher(servletUrl).replaceFirst(extUrl);
return retval;
}
static public String getRealPath(HttpServletRequest request, String extUrl)
{
String URLstr = request.getRequestURL().toString();
String retval = getRealPath(URLstr, extUrl);
return retval;
}
/**
* Simple utility method to help with the migration from Properties to
* Map<String, String>.
*
* @param properties
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Map<String, String> convertToMap(final Properties properties) {
final Map<String, String> map = new HashMap(properties);
return map;
}
/**
* Simple utility method to help with the migration from Map<String, String>
* to Properties.
*
* @deprecated Should migrate to Map<String, String> signatures.
* @param map
* @return
*/
public static Properties convertToProperties(final Map<String, String> map) {
final Properties properties = new Properties();
if (map != null) {
for (Entry<String, String> entry : map.entrySet()) {
properties.setProperty(entry.getKey(), entry.getValue());
}
}
return properties;
}
/**
* <p>
* Checks if a String is whitespace, empty ("") or null.
* </p>
*
* <pre>
* StringUtils.isBlank(null) = true
* StringUtils.isBlank("") = true
* StringUtils.isBlank(" ") = true
* StringUtils.isBlank("bob") = false
* StringUtils.isBlank(" bob ") = false
* </pre>
*
* @param str
* the String to check, may be null
* @return <code>true</code> if the String is null, empty or whitespace
* @since 2.0
*/
public static boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
/**
* <p>
* Checks if a String is not empty (""), not null and not whitespace only.
* </p>
*
* <pre>
* StringUtils.isNotBlank(null) = false
* StringUtils.isNotBlank("") = false
* StringUtils.isNotBlank(" ") = false
* StringUtils.isNotBlank("bob") = true
* StringUtils.isNotBlank(" bob ") = true
* </pre>
*
* @param str
* the String to check, may be null
* @return <code>true</code> if the String is not empty and not null and not
* whitespace
* @since 2.0
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* <p>
* Compares two Strings, returning <code>true</code> if they are equal.
* </p>
*
* <p>
* <code>null</code>s are handled without exceptions. Two <code>null</code>
* references are considered to be equal. The comparison is case sensitive.
* </p>
*
* <pre>
* StringUtils.equals(null, null) = true
* StringUtils.equals(null, "abc") = false
* StringUtils.equals("abc", null) = false
* StringUtils.equals("abc", "abc") = true
* StringUtils.equals("abc", "ABC") = false
* </pre>
*
* @see java.lang.String#equals(Object)
* @param str1
* the first String, may be null
* @param str2
* the second String, may be null
* @return <code>true</code> if the Strings are equal, case sensitive, or both
* <code>null</code>
*/
public static boolean equals(String str1, String str2) {
return str1 == null ? str2 == null : str1.equals(str2);
}
/**
* <p>
* Compares two Strings, returning <code>true</code> if they are equal
* ignoring the case.
* </p>
*
* <p>
* <code>null</code>s are handled without exceptions. Two <code>null</code>
* references are considered equal. Comparison is case insensitive.
* </p>
*
* <pre>
* StringUtils.equalsIgnoreCase(null, null) = true
* StringUtils.equalsIgnoreCase(null, "abc") = false
* StringUtils.equalsIgnoreCase("abc", null) = false
* StringUtils.equalsIgnoreCase("abc", "abc") = true
* StringUtils.equalsIgnoreCase("abc", "ABC") = true
* </pre>
*
* @see java.lang.String#equalsIgnoreCase(String)
* @param str1
* the first String, may be null
* @param str2
* the second String, may be null
* @return <code>true</code> if the Strings are equal, case insensitive, or
* both <code>null</code>
*/
public static boolean equalsIgnoreCase(String str1, String str2) {
return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2);
}
}