/*
 * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/basiclti/BasicLTIUtil.java $
 * $Id: BasicLTIUtil.java 133995 2014-02-02 22:06:40Z 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.lti;

import static org.imsglobal.lti.BasicLTIConstants.CUSTOM_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.EXTENSION_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.LTI_MESSAGE_TYPE;
import static org.imsglobal.lti.BasicLTIConstants.LTI_VERSION;
import static org.imsglobal.lti.BasicLTIConstants.OAUTH_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_DESCRIPTION;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_GUID;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_NAME;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_URL;

import java.io.IOException;
import java.net.URISyntaxException;
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;
import org.apache.commons.lang3.StringUtils;
import org.imsglobal.lti.launch.LtiError;
import org.imsglobal.lti.launch.LtiLaunch;
import org.imsglobal.lti.launch.LtiVerificationResult;

/* 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
 *

 * Sample Descriptor
 *
 *

 * <?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 lti launch URL</launch_url>
 *   <secure_launch_url>url to the lti 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>
 *

 */
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);
    }

    public static LtiVerificationResult validateMessage(HttpServletRequest request, String URL, String oauth_secret) {
        OAuthMessage oam = OAuthServlet.getMessage(request, URL);
        String oauth_consumer_key = null;
        try {
            oauth_consumer_key = oam.getConsumerKey();
        } catch (Exception e) {
            return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Unable to find consumer key in message");
        }

        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 (IOException|URISyntaxException e) {
            return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Unable to find base string");
        }

        try {
            oav.validateMessage(oam, acc);
        } catch (Exception e) {
            if (base_string != null) {
                return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage() + "\nBase String\n" + base_string);
            } else {
                return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage());
            }
        }
        return new LtiVerificationResult(true, new LtiLaunch(request));
    }

    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.
     *

     * 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".
     *

     * 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)}
     *
     * @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("
\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("
\n");

        // Paint the auto-pop up if we are transitioning from https: to http:
        // and are not already the top frame...
        text.append("\n");

        // paint debug output
        if (debug) {
            text.append("
\n");
            text.append("BasicLTI Endpoint\n");
            text.append(endpoint);
            text.append("\n\n");
            text.append("BasicLTI Parameters:\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("
\n");
        } else {
            // paint auto submit script
            text
                    .append("  \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 = StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/launch_url"));
        String secure_launch_url = StringUtils.stripToNull(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", StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/x-secure/launch_key")));
        setProperty(launch_info, "secret", StringUtils.stripToNull(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 = StringUtils.stripToNull(XMLMap.getString(tm,
                "/basic_lti_link/launch_url"));
        String secure_launch_url = StringUtils.stripToNull(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", StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/x-secure/launch_key")));
        setProperty(launch_info, "secret", StringUtils.stripToNull(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:
     *
     * {@code <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();
    }

    /**
     * Mutates the passed {@code 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 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 {@code Properties} to {@code 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 {@code Map<String, String>}
     * to {@code Properties}.
     *
     * @deprecated Should migrate to {@code 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;
    }
}


package org.imsglobal.lti.launch;

import net.oauth.*;
import net.oauth.server.OAuthServlet;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Map;
import java.util.logging.Logger;

/**
 * This class <b>verifies</b> LTI launches according to the Oauth 1.0 spec
 * @author  Paul Gray
 * @since   1.1
 */
public class LtiOauthVerifier implements LtiVerifier {

    public static final String OAUTH_KEY_PARAMETER= "oauth_consumer_key";

    private final static Logger logger = Logger.getLogger(LtiOauthVerifier.class.getName());

    /**
     * This method verifies the signed HttpServletRequest
     * @param request the HttpServletRequest that will be verified
     * @param secret the secret to verify the properties with
     * @return the result of the verification, along with contextual
     * information
     * @throws LtiVerificationException
     */
    @Override
    public LtiVerificationResult verify(HttpServletRequest request, String secret) throws LtiVerificationException {
        OAuthMessage oam = OAuthServlet.getMessage(request, OAuthServlet.getRequestURL(request));
        String oauth_consumer_key = null;
        try {
            oauth_consumer_key = oam.getConsumerKey();
        } catch (Exception e) {
            return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Unable to find consumer key in message");
        }

        OAuthValidator oav = new SimpleOAuthValidator();
        OAuthConsumer cons = new OAuthConsumer(null, oauth_consumer_key, secret, null);
        OAuthAccessor acc = new OAuthAccessor(cons);

        try {
            oav.validateMessage(oam, acc);
        } catch (Exception e) {
            return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage());
        }
        return new LtiVerificationResult(true, new LtiLaunch(request));
    }

    /**
     * This method will verify a collection of parameters
     * @param parameters the parameters that will be verified. mapped by key & value
     * @param url the url this request was made at
     * @param method the method this url was requested with
     * @param secret the secret to verify the propertihes with
     * @return
     * @throws LtiVerificationException
     */
    @Override
    public LtiVerificationResult verifyParameters(Map<String, String> parameters, String url, String method, String secret) throws LtiVerificationException {
        OAuthMessage oam = new OAuthMessage(method, url, parameters.entrySet());
        OAuthConsumer cons = new OAuthConsumer(null, parameters.get(OAUTH_KEY_PARAMETER), secret, null);
        OAuthValidator oav = new SimpleOAuthValidator();
        OAuthAccessor acc = new OAuthAccessor(cons);

        try {
            oav.validateMessage(oam, acc);
        } catch (Exception e) {
            return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage() + ", Parameters: " + Arrays.toString(parameters.entrySet().toArray()));
        }
        return new LtiVerificationResult(true, new LtiLaunch(parameters));
    }
}


package org.imsglobal.lti2.objects.consumer;

import java.util.Arrays;

public class StandardServices {

    public static final String TOOLPROXY_FORMAT = "application/vnd.ims.lti.v2.toolproxy+json";
    public static final String TOOLSETTINGS_CONTEXT = "http://purl.imsglobal.org/ctx/lti/v2/ToolSettings";
    public static final String TOOLSETTINGS_FORMAT = "application/vnd.ims.lti.v2.toolsettings+json";
    public static final String TOOLSETTINGS_SIMPLE_FORMAT = "application/vnd.ims.lti.v2.toolsettings.simple+json";

    public static final String TOOLPROXY_ID_CONTEXT = "http://purl.imsglobal.org/ctx/lti/v2/ToolProxyId";
    public static final String TOOLPROXY_ID_FORMAT = "application/vnd.ims.lti.v2.toolproxy.id+json";
    public static final String TOOLPROXY_ID_TYPE = "ToolProxy";

	public static final String RESULT_FORMAT = "application/vnd.ims.lis.v2.result+json";
	public static final String RESULT_CONTEXT = "http://purl.imsglobal.org/ctx/lis/v2/Result";
	public static final String RESULT_TYPE = "Result";

    public static ServiceOffered LTI2Registration(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:ToolProxy.collection");    // TODO: Is this right see 5.6 in the docs
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList(TOOLPROXY_FORMAT));
		ret.setAction(Arrays.asList("POST"));
		return ret;
    }

    // "endpoint" : "http://lms.example.com/resources/ToolProxy/{tool_proxy_guid}",
    public static ServiceOffered LTI2ProxyItem(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:ToolProxy.item");
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList(TOOLPROXY_FORMAT));
		ret.setAction(Arrays.asList("GET", "PUT"));
		return ret;
    }

    // "endpoint" : "http://lms.example.com/resources/Result/{sourcedId}",
    public static ServiceOffered LTI2ResultItem(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:Result.item");    // TODO: Is this right see 5.6 in the docs
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList(RESULT_FORMAT));
		ret.setAction(Arrays.asList("GET", "PUT"));
		return ret;
    }

    // "endpoint" : "http://lms.example.com/resources/ToolProxy/{tool_proxy_guid}/custom",
    public static ServiceOffered LTI2ToolProxySettings(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:ToolProxySettings");
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList(TOOLSETTINGS_FORMAT, TOOLSETTINGS_SIMPLE_FORMAT));
		ret.setAction(Arrays.asList("GET", "PUT"));
		return ret;
    }

    // "endpoint" : "http://lms.example.com/resources/links/{link_id}/custom",
    public static ServiceOffered LTI2LtiLinkSettings(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:LtiLinkSettings");
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList(TOOLSETTINGS_FORMAT, TOOLSETTINGS_SIMPLE_FORMAT));
		ret.setAction(Arrays.asList("GET", "PUT"));
		return ret;
    }

    public static ServiceOffered LTI1Outcomes(String endpoint) {
		ServiceOffered ret = new ServiceOffered();
		ret.setEndpoint(endpoint);
		ret.set_id("tcp:LTI_1_1_ResultService");
		ret.set_type("RestService");
		ret.setFormat(Arrays.asList("application/vnd.ims.lti.v1.outcome+xml"));
		ret.setAction(Arrays.asList("POST"));
		return ret;
    }

}


package org.imsglobal.pox;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

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;
import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
import oauth.signpost.exception.OAuthException;
import oauth.signpost.http.HttpParameters;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.imsglobal.lti.XMLMap;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class IMSPOXRequest {

	private final static Logger Log = Logger.getLogger(IMSPOXRequest.class .getName());

	public final static String MAJOR_SUCCESS = "success";
	public final static String MAJOR_FAILURE = "failure";
	public final static String MAJOR_UNSUPPORTED = "unsupported";
	public final static String MAJOR_PROCESSING = "processing";

	public final static String [] validMajor = {
		MAJOR_SUCCESS, MAJOR_FAILURE, MAJOR_UNSUPPORTED, MAJOR_PROCESSING };

	public final static String SEVERITY_ERROR = "error";
	public final static String SEVERITY_WARNING = "warning";
	public final static String SEVERITY_STATUS = "status";

	public final static String [] validSeverity = {
		SEVERITY_ERROR, SEVERITY_WARNING, SEVERITY_STATUS };

	public final static String MINOR_FULLSUCCESS ="fullsuccess";
	public final static String MINOR_NOSOURCEDIDS = "nosourcedids";
	public final static String MINOR_IDALLOC = "idalloc";
	public final static String MINOR_OVERFLOWFAIL = "overflowfail";
	public final static String MINOR_IDALLOCINUSEFAIL = "idallocinusefail";
	public final static String MINOR_INVALIDDATAFAIL = "invaliddata";
	public final static String MINOR_INCOMPLETEDATA = "incompletedata";
	public final static String MINOR_PARTIALSTORAGE = "partialdatastorage";
	public final static String MINOR_UNKNOWNOBJECT = "unknownobject";
	public final static String MINOR_DELETEFAILURE = "deletefailure";
	public final static String MINOR_TARGETREADFAILURE = "targetreadfailure";
	public final static String MINOR_SAVEPOINTERROR = "savepointerror";
	public final static String MINOR_SAVEPOINTSYNCERROR = "savepointsyncerror";
	public final static String MINOR_UNKNOWNQUERY = "unknownquery";
	public final static String MINOR_UNKNOWNVOCAB = "unknownvocab";
	public final static String MINOR_TARGETISBUSY = "targetisbusy";
	public final static String MINOR_UNKNOWNEXTENSION = "unknownextension";
	public final static String MINOR_UNAUTHORIZEDREQUEST = "unauthorizedrequest";
	public final static String MINOR_LINKFAILURE = "linkfailure";
	public final static String MINOR_UNSUPPORTED = "unsupported";

	public final static String [] validMinor = {
		MINOR_FULLSUCCESS, MINOR_NOSOURCEDIDS, MINOR_IDALLOC, MINOR_OVERFLOWFAIL,
		MINOR_IDALLOCINUSEFAIL, MINOR_INVALIDDATAFAIL, MINOR_INCOMPLETEDATA,
		MINOR_PARTIALSTORAGE, MINOR_UNKNOWNOBJECT, MINOR_DELETEFAILURE,
		MINOR_TARGETREADFAILURE, MINOR_SAVEPOINTERROR, MINOR_SAVEPOINTSYNCERROR,
		MINOR_UNKNOWNQUERY, MINOR_UNKNOWNVOCAB, MINOR_TARGETISBUSY,
		MINOR_UNKNOWNEXTENSION, MINOR_UNAUTHORIZEDREQUEST, MINOR_LINKFAILURE,
		MINOR_UNSUPPORTED
	} ;

	public Document postDom = null;
	public Element bodyElement = null;
	public Element headerElement = null;
	public String postBody = null;
	private String header = null;
	private String oauth_body_hash = null;
	private String oauth_consumer_key = null;

	public boolean valid = false;
	private String operation = null;
	public String errorMessage = null;
	public String base_string = null;
	private Map<String,String> bodyMap = null;
	private Map<String,String> headerMap = null;

	public String getOperation()
	{
		return operation;
	}

	public String getOAuthConsumerKey()
	{
		return oauth_consumer_key;
	}

	public String getHeaderVersion()
	{
		return getHeaderItem("/imsx_version");
	}

	public String getHeaderMessageIdentifier()
	{
		return getHeaderItem("/imsx_messageIdentifier");
	}

	public String getHeaderItem(String path)
	{
		if ( getHeaderMap() == null ) return null;
		return headerMap.get(path);
	}

	public Map<String,String> getHeaderMap()
	{
		if ( headerMap != null ) return headerMap;
		if ( headerElement == null ) return null;
		headerMap = XMLMap.getMap(headerElement);
		return headerMap;
	}

	public Map<String,String> getBodyMap()
	{
		if ( bodyMap != null ) return bodyMap;
		if ( bodyElement == null ) return null;
		bodyMap = XMLMap.getMap(bodyElement);
		return bodyMap;
	}

	public String getPostBody()
	{
		return postBody;
	}

	// Normal Constructor
	public IMSPOXRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request)
	{
		loadFromRequest(request);
		if ( ! valid ) return;
		validateRequest(oauth_consumer_key, oauth_secret, request);
	}

	// Constructor for delayed validation
	public IMSPOXRequest(HttpServletRequest request)
	{
		loadFromRequest(request);
	}

	// Constructor for testing...
	public IMSPOXRequest(String bodyString)
	{
		postBody = bodyString;
		parsePostBody();
	}

	// Load but do not check the authentication
	public void loadFromRequest(HttpServletRequest request)
	{
		String contentType = request.getContentType();
		if ( ! "application/xml".equals(contentType) ) {
			errorMessage = "Content Type must be application/xml";
			Log.info(errorMessage+"\n"+contentType);
			return;
		}

		setAuthHeader(request.getHeader("Authorization"));
		if ( oauth_body_hash == null ) {
			errorMessage = "Did not find oauth_body_hash";
			Log.info(errorMessage+"\n"+header);
			return;
		}

		try {
			Reader in = request.getReader();
			postBody = readPostBody(in);
		} catch(Exception e) {
			errorMessage = "Could not read message body:"+e.getMessage();
			return;
		}

		validatePostBody();
		if (errorMessage != null) return;

		parsePostBody();
	}

	@SuppressWarnings("deprecation")
	public void setAuthHeader(String header) {
		this.header = header;
		oauth_body_hash = null;
		if ( header != null ) {
			if (header.startsWith("OAuth ")) header = header.substring(5);
			String [] parms = header.split(",");
			for ( String parm : parms ) {
				parm = parm.trim();
				if ( parm.startsWith("oauth_body_hash=") ) {
					String [] pieces = parm.split("\"");
					oauth_body_hash = URLDecoder.decode(pieces[1]);
				}
				if ( parm.startsWith("oauth_consumer_key=") ) {
					String [] pieces = parm.split("\"");
					oauth_consumer_key = URLDecoder.decode(pieces[1]);
				}
			}
		}
	}

	public static String readPostBody(Reader in) throws IOException {
		// System.out.println("OBH="+oauth_body_hash);
		final char[] buffer = new char[0x10000];
		StringBuilder out = new StringBuilder();
		int read;
		do {
			read = in.read(buffer, 0, buffer.length);
			if (read > 0) {
				out.append(buffer, 0, read);
			}
		} while (read >= 0);
		return out.toString();
	}

	public static String getBodyHash(String postBody) throws GeneralSecurityException {
		MessageDigest md = MessageDigest.getInstance("SHA1");
		md.update(postBody.getBytes());
		byte[] output = Base64.encodeBase64(md.digest());
		return new String(output);
	}

	public void validatePostBody() {
		try {
			String hash = getBodyHash(postBody);
			// System.out.println("HASH="+hash);
			if ( ! hash.equals(oauth_body_hash) ) {
				errorMessage = "Body hash does not match header";
				return;
			}
		} catch (Exception e) {
			errorMessage = "Could not compute body hash";
			return;
		}
	}

	public void parsePostBody()
	{
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
			dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
			DocumentBuilder db = dbf.newDocumentBuilder();
			postDom = db.parse(new ByteArrayInputStream(postBody.getBytes()));
		}catch(Exception e) {
			errorMessage = "Could not parse XML: "+e.getMessage();
			return;
		}

		try {
			XPath xpath = XPathFactory.newInstance().newXPath();
			XPathExpression expr = xpath.compile("/imsx_POXEnvelopeRequest/imsx_POXBody/*");
			Object result = expr.evaluate(postDom, XPathConstants.NODESET);
			NodeList nodes = (NodeList) result;
			bodyElement = (Element) nodes.item(0);
			operation = bodyElement.getNodeName();

			expr = xpath.compile("/imsx_POXEnvelopeRequest/imsx_POXHeader/*");
			result = expr.evaluate(postDom, XPathConstants.NODESET);
			nodes = (NodeList) result;
			headerElement = (Element) nodes.item(0);
		}catch(javax.xml.xpath.XPathExpressionException e) {
			errorMessage = "Could not parse XPATH: "+e.getMessage();
			return;
		}catch(Exception e) {
			errorMessage = "Could not parse input XML: "+e.getMessage();
			return;
		}

		if ( operation == null || bodyElement == null ) {
			errorMessage = "Could not find operation";
			return;
		}
		valid = true;
	}

	// Assumes data is all loaded
	public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request)
	{
		validateRequest(oauth_consumer_key, oauth_secret, request, null) ;
	}

	public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request, String URL)
	{
		valid = false;
		OAuthMessage oam = OAuthServlet.getMessage(request, URL);
		OAuthValidator oav = new SimpleOAuthValidator();
		OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed",
				oauth_consumer_key, oauth_secret, null);

		OAuthAccessor acc = new OAuthAccessor(cons);

		try {
			base_string = OAuthSignatureMethod.getBaseString(oam);
		} catch (Exception e) {
			base_string = null;
		}

		try {
			oav.validateMessage(oam,acc);
		} catch(Exception e) {
			errorMessage = "Launch fails OAuth validation: "+e.getMessage();
			return;
		}
		valid = true;
	}

	public static String fetchTag(org.w3c.dom.Element element, String tag)
	{
		try {
			org.w3c.dom.NodeList elements = element.getElementsByTagName(tag);
			int numElements = elements.getLength();
			if (numElements > 0) {
				org.w3c.dom.Element e = (org.w3c.dom.Element)elements.item(0);
				if (e.hasChildNodes()) {
					return e.getFirstChild().getNodeValue();
				}
			}
		} catch (Throwable t) {
			Log.warning(t.getMessage());
			// t.printStackTrace();
		}
		return null;
	}

	public boolean inArray(final String [] theArray, final String theString)
	{
		if ( theString == null ) return false;
		for ( String str : theArray ) {
			if ( theString.equals(str) ) return true;
		}
		return false;
	}

	static final String fatalMessage =
		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
		"<imsx_POXEnvelopeResponse xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">\n" +
		"    <imsx_POXHeader>\n" +
		"        <imsx_POXResponseHeaderInfo>\n" +
		"            <imsx_version>V1.0</imsx_version>\n" +
		"            <imsx_messageIdentifier>%s</imsx_messageIdentifier>\n" +
		"            <imsx_statusInfo>\n" +
		"                <imsx_codeMajor>failure</imsx_codeMajor>\n" +
		"                <imsx_severity>error</imsx_severity>\n" +
		"                <imsx_description>%s</imsx_description>\n" +
		"                <imsx_operationRefIdentifier>%s</imsx_operationRefIdentifier>" +
		"            </imsx_statusInfo>\n" +
		"        </imsx_POXResponseHeaderInfo>\n" +
		"    </imsx_POXHeader>\n" +
		"    <imsx_POXBody/>\n" +
		"</imsx_POXEnvelopeResponse>";

	public static String getFatalResponse(String description)
	{
		return getFatalResponse(description, "unknown");
	}

	public static String getFatalResponse(String description, String message_id)
	{
		Date dt = new Date();
		String messageId = ""+dt.getTime();

		return String.format(fatalMessage,
				StringEscapeUtils.escapeXml(messageId),
				StringEscapeUtils.escapeXml(description),
				StringEscapeUtils.escapeXml(message_id));
	}

	static final String responseMessage =
		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
		"<imsx_POXEnvelopeResponse xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">\n" +
		"  <imsx_POXHeader>\n" +
		"    <imsx_POXResponseHeaderInfo>\n" +
		"      <imsx_version>V1.0</imsx_version>\n" +
		"      <imsx_messageIdentifier>%s</imsx_messageIdentifier>\n" +
		"      <imsx_statusInfo>\n" +
		"        <imsx_codeMajor>%s</imsx_codeMajor>\n" +
		"        <imsx_severity>%s</imsx_severity>\n" +
		"        <imsx_description>%s</imsx_description>\n" +
		"        <imsx_messageRefIdentifier>%s</imsx_messageRefIdentifier>\n" +
		"        <imsx_operationRefIdentifier>%s</imsx_operationRefIdentifier>" +
		"%s\n"+
		"      </imsx_statusInfo>\n" +
		"    </imsx_POXResponseHeaderInfo>\n" +
		"  </imsx_POXHeader>\n" +
		"  <imsx_POXBody>\n" +
		"%s%s"+
		"  </imsx_POXBody>\n" +
		"</imsx_POXEnvelopeResponse>";

	public String getResponseUnsupported(String desc)
	{
		return getResponse(desc, MAJOR_UNSUPPORTED, null, null, null, null);
	}

	public String getResponseFailure(String desc, Properties minor)
	{
		return getResponse(desc, null, null, null, minor, null);
	}

	public String getResponseFailure(String desc, Properties minor, String bodyString)
	{
		return getResponse(desc, null, null, null, minor, bodyString);
	}

	public String getResponseSuccess(String desc, String bodyString)
	{
		return getResponse(desc, MAJOR_SUCCESS, null, null, null, bodyString);
	}

	public String getResponse(String description, String major, String severity,
			String messageId, Properties minor, String bodyString)
	{
		StringBuffer internalError = new StringBuffer();
		if ( major == null ) major = MAJOR_FAILURE;
		if ( severity == null && MAJOR_PROCESSING.equals(major) ) severity = SEVERITY_STATUS;
		if ( severity == null && MAJOR_SUCCESS.equals(major) ) severity = SEVERITY_STATUS;
		if ( severity == null ) severity = SEVERITY_ERROR;
		if ( messageId == null ) {
			Date dt = new Date();
			messageId = ""+dt.getTime();
		}

		StringBuffer sb = new StringBuffer();
		if ( minor != null && minor.size() > 0 ) {
			for(Object okey : minor.keySet() ) {
				String key = (String) okey;
				String value = minor.getProperty(key);
				if ( key == null || value == null ) continue;
				if ( !inArray(validMinor, value) ) {
					if ( internalError.length() > 0 ) sb.append(", ");
					internalError.append("Invalid imsx_codeMinorFieldValue="+major);
					continue;
				}
				if ( sb.length() == 0 ) sb.append("\n        <imsx_codeMinor>\n");
				sb.append("          <imsx_codeMinorField>\n            <imsx_codeMinorFieldName>");
				sb.append(key);
				sb.append("</imsx_codeMinorFieldName>\n            <imsx_codeMinorFieldValue>");
				sb.append(StringEscapeUtils.escapeXml(value));
				sb.append("</imsx_codeMinorFieldValue>\n          </imsx_codeMinorField>\n");
			}
			if ( sb.length() > 0 ) sb.append("        </imsx_codeMinor>");
		}
		String minorString = sb.toString();

		if ( ! inArray(validMajor, major) ) {
			if ( internalError.length() > 0 ) sb.append(", ");
			internalError.append("Invalid imsx_codeMajor="+major);
		}
		if ( ! inArray(validSeverity, severity) ) {
			if ( internalError.length() > 0 ) sb.append(", ");
			internalError.append("Invalid imsx_severity="+major);
		}

		if ( internalError.length() > 0 ) {
			description = description + " (Internal error: " + internalError.toString() + ")";
			Log.warning(internalError.toString());
		}

		if ( bodyString == null ) bodyString = "";
		// Trim off XML header
		if ( bodyString.startsWith("<?xml") ) {
			int pos = bodyString.indexOf("<",1);
			if ( pos > 0 ) bodyString = bodyString.substring(pos);
		}
		bodyString = bodyString.trim();
		String newLine = "";
		if ( bodyString.length() > 0 ) newLine = "\n";
		return String.format(responseMessage,
				StringEscapeUtils.escapeXml(messageId),
				StringEscapeUtils.escapeXml(major),
				StringEscapeUtils.escapeXml(severity),
				StringEscapeUtils.escapeXml(description),
				StringEscapeUtils.escapeXml(getHeaderMessageIdentifier()),
				StringEscapeUtils.escapeXml(operation),
				StringEscapeUtils.escapeXml(minorString),
				bodyString, newLine);

	}

	/**
	 * A template string for creating ReplaceResult messages.
	 *
	 * Similar to {@link #replaceResultMessage}, except has support for messageIdentifier
	 *
	 * Use like:
	 * <pre>
	 *     String.format(
	 *       ReplaceResultMessageTemplate,
	 *       messageId,
	 *       sourcedId,
	 *       resultScore,
	 *       resultDataXml
	 *     )
	 * </pre>
	 *
	 *
	 */
	public static final String ReplaceResultMessageTemplate =
			"<?xml version = \"1.0\" encoding = \"UTF-8\"?>"+
			"<imsx_POXEnvelopeRequest xmlns=\"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">"+
			"	<imsx_POXHeader>"+
			"		<imsx_POXRequestHeaderInfo>"+
			"			<imsx_version>V1.0</imsx_version>"+
			"			<imsx_messageIdentifier>%s</imsx_messageIdentifier>"+
			"		</imsx_POXRequestHeaderInfo>"+
			"	</imsx_POXHeader>"+
			"	<imsx_POXBody>"+
			"		<replaceResultRequest>"+
			"			<resultRecord>"+
			"				<sourcedGUID>"+
			"					<sourcedId>%s</sourcedId>"+
			"				</sourcedGUID>"+
			"				<result>"+
			"					<resultScore>"+
			"						<language>en</language>"+
			"						<textString>%s</textString>"+
			"					</resultScore>"+
			"					%s"+
			"				</result>"+
			"			</resultRecord>"+
			"		</replaceResultRequest>"+
			"	</imsx_POXBody>"+
			"</imsx_POXEnvelopeRequest>";

	/**
	 * @deprecated use {@link #ReplaceResultMessageTemplate} instead.
	 */
	@Deprecated
	public static final String replaceResultMessage =
			"<?xml version = \"1.0\" encoding = \"UTF-8\"?>"+
			"<imsx_POXEnvelopeRequest xmlns=\"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">"+
			"	<imsx_POXHeader>"+
			"		<imsx_POXRequestHeaderInfo>"+
			"			<imsx_version>V1.0</imsx_version>"+
			"		</imsx_POXRequestHeaderInfo>"+
			"	</imsx_POXHeader>"+
			"	<imsx_POXBody>"+
			"		<replaceResultRequest>"+
			"			<resultRecord>"+
			"				<sourcedGUID>"+
			"					<sourcedId>%s</sourcedId>"+
			"				</sourcedGUID>"+
			"				<result>"+
			"					<resultScore>"+
			"						<language>en</language>"+
			"						<textString>%s</textString>"+
			"					</resultScore>"+
			"					%s"+
			"				</result>"+
			"			</resultRecord>"+
			"		</replaceResultRequest>"+
			"	</imsx_POXBody>"+
			"</imsx_POXEnvelopeRequest>";

	static final String resultDataText = "<resultData><text>%s</text></resultData>";

	static final String resultDataUrl = "<resultData><url>%s</url></resultData>";

	public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score) throws IOException, OAuthException, GeneralSecurityException {
		sendReplaceResult(url, key, secret, sourcedid, score, null);
	}

	public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData) throws IOException, OAuthException, GeneralSecurityException {
		sendReplaceResult(url, key, secret, sourcedid, score, resultData, false);
	}

	public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData, Boolean isUrl) throws IOException, OAuthException, GeneralSecurityException {
		HttpPost request = buildReplaceResult(url, key, secret, sourcedid, score, resultData, isUrl);
		DefaultHttpClient client = new DefaultHttpClient();
		HttpResponse response = client.execute(request);
		if (response.getStatusLine().getStatusCode() >= 400) {
			throw new HttpResponseException(response.getStatusLine().getStatusCode(),
					response.getStatusLine().getReasonPhrase());
		}
	}

	public static HttpPost buildReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData, Boolean isUrl) throws IOException, OAuthException, GeneralSecurityException {
		return buildReplaceResult(url, key, secret, sourcedid, score, resultData, isUrl, null);
	}

	public static HttpPost buildReplaceResult(String url, String key, String secret, String sourcedid,
		String score, String resultData, Boolean isUrl, String messageId) throws IOException, OAuthException, GeneralSecurityException
	{
		String dataXml = "";
		if (resultData != null) {
			String format = isUrl ? resultDataUrl : resultDataText;
			dataXml = String.format(format, StringEscapeUtils.escapeXml(resultData));
		}

		String messageIdentifier = StringUtils.isBlank(messageId) ? String.valueOf(new Date().getTime()) : messageId;
		String xml = String.format(ReplaceResultMessageTemplate,
			StringEscapeUtils.escapeXml(messageIdentifier),
			StringEscapeUtils.escapeXml(sourcedid),
			StringEscapeUtils.escapeXml(score),
			dataXml);

		HttpParameters parameters = new HttpParameters();
		String hash = getBodyHash(xml);
		parameters.put("oauth_body_hash", URLEncoder.encode(hash, "UTF-8"));

		CommonsHttpOAuthConsumer signer = new CommonsHttpOAuthConsumer(key, secret);
		HttpPost request = new HttpPost(url);
		request.setHeader("Content-Type", "application/xml");
		request.setEntity(new StringEntity(xml, "UTF-8"));
		signer.setAdditionalParameters(parameters);
		signer.sign(request);
		return request;
	}

	/*

roleType:
Learner
Instructor
ContentDeveloper
Member
Manager
Mentor
Administrator
TeachingAssistant

fieldType:
Boolean
Integer
Real
String

<readMembershipResponse
xmlns="http://www.imsglobal.org/services/lis/mms2p0/wsdl11/sync/imsmms_v2p0">
<membershipRecord>
<sourcedId>GUID.TYPE</sourcedId>
<membership>
<collectionSourcedId>GUID.TYPE</collectionSourcedId>
<membershipIdType>MEMBERSHIPIDTYPE.TYPE</membershipIdType>
<member>
<personSourcedId>GUID.TYPE</personSourcedId>
<role>
<roleType>STRING</roleType>
<subRole>STRING</subRole>
<timeFrame>
<begin>DATETIME</begin>
<end>DATETIME</end>
<restrict>BOOLEAN</restrict>
<adminPeriod>
<language>LANGUAGESET.TYPE</language>
<textString>STRING</textString>
</adminPeriod>
</timeFrame>
<status>STATUS.TYPE</status>
<dateTime>DATETIME</dateTime>
<dataSource>GUID.TYPE</dataSource>
<recordInfo>
<extensionField>
<fieldName>STRING</fieldName>
<fieldType>FIELDTYPE.TYPE</fieldType>
<fieldValue>STRING</fieldValue>
</extensionField>
</recordInfo>
<extension>
<extensionField>
<fieldName>STRING</fieldName>
<fieldType>FIELDTYPE.TYPE</fieldType>
<fieldValue>STRING</fieldValue>
</extensionField>
</extension>
</role>
</member>
<creditHours>INTEGER</creditHours>
<dataSource>GUID.TYPE</dataSource>
</membership>
</membershipRecord>
</readMembershipResponse>
	 */
}


/**
 * 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.
 *
 * Author: Charles Severance <csev@umich.edu>
 */

package org.imsglobal.lti2;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.HttpStatus;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.imsglobal.lti.BasicLTIConstants;
import org.imsglobal.lti.BasicLTIUtil;
import org.imsglobal.lti.launch.LtiVerificationResult;
import org.imsglobal.json.IMSJSONRequest;
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;

/**
 * Notes:
 *
 * This is a sample "Hello World" servlet for LTI2.  It is a simple UI - mostly
 * intended to exercise the APIs and show the way for servlet-based LTI2 code.
 *
 * Here are the web.xml entries:
 *

 * {@code
 *  <servlet>
 *    <servlet-name>SampleServlet</servlet-name>
 *    <servlet-class>org.imsglobal.lti2.LTI2Servlet</servlet-class>
 *  </servlet>
 *  <servlet-mapping>
 *    <servlet-name>SampleServlet</servlet-name>
 *    <url-pattern>/sample/*</url-pattern>
 *  </servlet-mapping>
 * }
 *

 *
 *  Then navigate to:
 *  http://localhost/testservlet/sample/register
 *
 *  A PHP endpoint is available at:
 *
 *  https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-docs/resources/docs/sakai-api-test
 *
 *  The tp.php script is the Tool Provider registration endpoint in the PHP code
 *
 */

@SuppressWarnings("deprecation")
public class LTI2Servlet extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private static Logger M_log = Logger.getLogger(LTI2Servlet.class.getName());

	protected ServiceOffered LTI2ResultItem = null;
	protected ServiceOffered LTI2LtiLinkSettings = null;
	protected ServiceOffered LTI2ToolProxyBindingSettings = null;
	protected ServiceOffered LTI2ToolProxySettings = null;

	private static final String SVC_tc_profile = "tc_profile";
	private static final String SVC_tc_registration = "tc_registration";
	private static final String SVC_Settings = "Settings";
	private static final String SVC_Result = "Result";

	@SuppressWarnings("unused")
	private static final String EMPTY_JSON_OBJECT = "{\n}\n";

	private static final String APPLICATION_JSON = "application/json";

	// Normally these would be in a database
	private static String TEST_KEY = "42";
	private static String TEST_SECRET = "zaphod";

	// Pretending to be a database row :)
	private static Map<String, String> PERSIST = new TreeMap<String, String> ();

	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
	}

	protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doPost(request,response);
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doPost(request,response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		try {
			doRequest(request, response);
		} catch (Exception e) {
			String ipAddress = request.getRemoteAddr();
			String uri = request.getRequestURI();
			M_log.log(Level.WARNING, "General LTI2 Failure URI="+uri+" IP=" + ipAddress);
			e.printStackTrace();
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			doErrorJSON(request, response, null, "General failure", e);
		}
	}

	@SuppressWarnings("unused")
	protected void doRequest(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		System.out.println("getServiceURL="+getServiceURL(request));

		String ipAddress = request.getRemoteAddr();
		System.out.println("LTI Service request from IP=" + ipAddress);

		String rpi = request.getPathInfo();
		String uri = request.getRequestURI();
		String [] parts = uri.split("/");
		if ( parts.length < 4 ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, null, "Incorrect url format", null);
			return;
		}
		String controller = parts[3];
		if ( "register".equals(controller) ) {
			doRegister(request,response);
			return;
		} else if ( "launch".equals(controller) ) {
			doLaunch(request,response);
			return;
		} else if ( SVC_tc_profile.equals(controller) && parts.length == 5 ) {
			String profile_id = parts[4];
			getToolConsumerProfile(request,response,profile_id);
			return;
		} else if ( SVC_tc_registration.equals(controller) && parts.length == 5 ) {
			String profile_id = parts[4];
			registerToolProviderProfile(request, response, profile_id);
			return;
		} else if ( SVC_Result.equals(controller) && parts.length == 5 ) {
			String sourcedid = parts[4];
			handleResultRequest(request, response, sourcedid);
			return;
		} else if ( SVC_Settings.equals(controller) && parts.length >= 6 ) {
			handleSettingsRequest(request, response, parts);
			return;
		}

		IMSJSONRequest jsonRequest = new IMSJSONRequest(request);
		if ( jsonRequest.valid ) {
			System.out.println(jsonRequest.getPostBody());
		}

		response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
		M_log.log(Level.WARNING, "Unknown request="+uri);
		doErrorJSON(request, response, null, "Unknown request="+uri, null);
	}

	protected void doRegister(HttpServletRequest request, HttpServletResponse response)
	{
		// Reset our database
		PERSIST.clear();
		String launch_url = request.getParameter("launch_url");
		response.setContentType("text/html");

		String output = null;
		if ( launch_url != null ) {
			Properties ltiProps = new Properties();

			ltiProps.setProperty(BasicLTIConstants.LTI_VERSION, LTI2Constants.LTI2_VERSION_STRING);
			ltiProps.setProperty(LTI2Constants.REG_KEY,TEST_KEY);
			ltiProps.setProperty(LTI2Constants.REG_PASSWORD,TEST_SECRET);
			ltiProps.setProperty(BasicLTIUtil.BASICLTI_SUBMIT, "Press to Launch External Tool");
			ltiProps.setProperty(BasicLTIConstants.LTI_MESSAGE_TYPE, BasicLTIConstants.LTI_MESSAGE_TYPE_TOOLPROXYREGISTRATIONREQUEST);

			String serverUrl = getServiceURL(request);
			ltiProps.setProperty(LTI2Constants.TC_PROFILE_URL,serverUrl + SVC_tc_profile + "/" + TEST_KEY);
			ltiProps.setProperty(BasicLTIConstants.LAUNCH_PRESENTATION_RETURN_URL, serverUrl + "launch");
			System.out.println("ltiProps="+ltiProps);

			boolean dodebug = true;
			output = BasicLTIUtil.postLaunchHTML(ltiProps, launch_url, dodebug);
		} else {
			output = "<form>Register URL:
<input type=\"text\" name=\"launch_url\" size=\"80\"\n" +
				"value=\"http://localhost:8888/sakai-api-test/tp.php\"><input type=\"submit\">\n";
		}

		try {
			PrintWriter out = response.getWriter();
			out.println(output);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	// We are actually bypassing the activation step.  Usually activation will parse
	// the profile, and install a tool if the admin is happy.  For us we just parse
	// the profile and do a launch.
	@SuppressWarnings("unused")
	protected void doLaunch(HttpServletRequest request, HttpServletResponse response)
	{

		String profile = PERSIST.get("profile");
		response.setContentType("text/html");

		String output = null;
		if ( profile == null ) {
			output = "Missing profile";
		} else {
	        JSONObject providerProfile = (JSONObject) JSONValue.parse(profile);

			List<Properties> profileTools = new ArrayList<Properties> ();
	        Properties info = new Properties();
			String retval = LTI2Util.parseToolProfile(profileTools, info, providerProfile);
			String launch = null;
			String parameter = null;
			for ( Properties profileTool : profileTools ) {
				launch = (String) profileTool.get("launch");
				parameter = (String) profileTool.get("parameter");
			}
			JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT);

			String shared_secret = (String) security_contract.get(LTI2Constants.SHARED_SECRET);
			System.out.println("launch="+launch);
			System.out.println("shared_secret="+shared_secret);

			Properties ltiProps = LTI2SampleData.getLaunch();
			ltiProps.setProperty(BasicLTIConstants.LTI_VERSION,BasicLTIConstants.LTI_VERSION_2);

			Properties lti2subst = LTI2SampleData.getSubstitution();
			String settings_url = getServiceURL(request) + SVC_Settings + "/";
			lti2subst.setProperty("LtiLink.custom.url", settings_url + LTI2Util.SCOPE_LtiLink + "/"
					+ ltiProps.getProperty(BasicLTIConstants.RESOURCE_LINK_ID));
			lti2subst.setProperty("ToolProxyBinding.custom.url", settings_url + LTI2Util.SCOPE_ToolProxyBinding + "/"
					+ ltiProps.getProperty(BasicLTIConstants.CONTEXT_ID));
			lti2subst.setProperty("ToolProxy.custom.url", settings_url + LTI2Util.SCOPE_ToolProxy + "/"
					+ TEST_KEY);
			lti2subst.setProperty("Result.url", getServiceURL(request) + SVC_Result + "/"
					+ ltiProps.getProperty(BasicLTIConstants.RESOURCE_LINK_ID));

			// Do the substitutions
			Properties custom = new Properties();
			LTI2Util.mergeLTI2Parameters(custom, parameter);
			LTI2Util.substituteCustom(custom, lti2subst);

			// Place the custom values into the launch
			LTI2Util.addCustomToLaunch(ltiProps, custom);

			ltiProps = BasicLTIUtil.signProperties(ltiProps, launch, "POST",
                TEST_KEY, shared_secret, null, null, null);

			boolean dodebug = true;
			output = BasicLTIUtil.postLaunchHTML(ltiProps, launch, dodebug);
		}

		try {
			PrintWriter out = response.getWriter();
			out.println(output);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	protected void getToolConsumerProfile(HttpServletRequest request,
			HttpServletResponse response,String profile_id)
	{
		// Map<String,Object> deploy = ltiService.getDeployForConsumerKeyDao(profile_id);
		Map<String,Object> deploy = null;

		ToolConsumer consumer = buildToolConsumerProfile(request, deploy, profile_id);

		ObjectMapper mapper = new ObjectMapper();
		try {
			// http://stackoverflow.com/questions/6176881/how-do-i-make-jackson-pretty-print-the-json-content-it-generates
			ObjectWriter writer = mapper.defaultPrettyPrintingWriter();
			// ***IMPORTANT!!!*** for Jackson 2.x use the line below instead of the one above:
			// ObjectWriter writer = mapper.writer().withDefaultPrettyPrinter();
			// System.out.println(mapper.writeValueAsString(consumer));
			response.setContentType(APPLICATION_JSON);
			PrintWriter out = response.getWriter();
			out.println(writer.writeValueAsString(consumer));
			// System.out.println(writer.writeValueAsString(consumer));
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	// Normally deploy would have the data about the deployment - for this test
	// it is always null and we allow everything
	protected ToolConsumer buildToolConsumerProfile(HttpServletRequest request, Map<String, Object> deploy, String profile_id)
	{
		// Load the configuration data
		LTI2Config cnf = new org.imsglobal.lti2.LTI2ConfigSample();

		ToolConsumer consumer = new ToolConsumer(profile_id, "LTI-2p0", getServiceURL(request), cnf);

		// Normally we would check permissions before we offer capabilities
		List<String> capabilities = consumer.getCapability_offered();
		LTI2Util.allowEmail(capabilities);
		LTI2Util.allowName(capabilities);
		LTI2Util.allowSettings(capabilities);
		LTI2Util.allowResult(capabilities);

		// Normally we would check permissions before we offer services
		List<ServiceOffered> services = consumer.getService_offered();
		services.add(StandardServices.LTI2Registration(getServiceURL(request) +
			SVC_tc_registration + "/" + profile_id));
		services.add(StandardServices.LTI2ResultItem(getServiceURL(request) +
			SVC_Result + "/{" + BasicLTIConstants.LIS_RESULT_SOURCEDID + "}"));
		services.add(StandardServices.LTI2LtiLinkSettings(getServiceURL(request) +
			SVC_Settings + "/" + LTI2Util.SCOPE_LtiLink + "/{" + BasicLTIConstants.RESOURCE_LINK_ID + "}"));
		services.add(StandardServices.LTI2ToolProxySettings(getServiceURL(request) +
			SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxyBinding + "/{" + BasicLTIConstants.CONTEXT_ID + "}"));
		services.add(StandardServices.LTI2ToolProxySettings(getServiceURL(request) +
			SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxy + "/{" + LTI2Constants.TOOL_PROXY_GUID + "}"));
		return consumer;
	}

	@SuppressWarnings({ "unchecked", "unused", "rawtypes" })
	public void registerToolProviderProfile(HttpServletRequest request,HttpServletResponse response,
			String profile_id) throws java.io.IOException
	{
		// Normally we would look up the deployment descriptor
		if ( ! TEST_KEY.equals(profile_id) ) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		String key = TEST_KEY;
		String secret = TEST_SECRET;

		IMSJSONRequest jsonRequest = new IMSJSONRequest(request);

		if ( ! jsonRequest.valid ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, "Request is not in a valid format", null);
			return;
		}

		System.out.println(jsonRequest.getPostBody());

		// Lets check the signature
		if ( key == null || secret == null ) {
			response.setStatus(HttpServletResponse.SC_FORBIDDEN);
			doErrorJSON(request, response, jsonRequest, "Deployment is missing credentials", null);
			return;
		}

		jsonRequest.validateRequest(key, secret, request);
		if ( !jsonRequest.valid ) {
			response.setStatus(HttpServletResponse.SC_FORBIDDEN);
			doErrorJSON(request, response, jsonRequest, "OAuth signature failure", null);
			return;
		}

		JSONObject providerProfile = (JSONObject) JSONValue.parse(jsonRequest.getPostBody());
		// System.out.println("OBJ:"+providerProfile);
		if ( providerProfile == null  ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, "JSON parse failed", null);
			return;
		}

		JSONObject default_custom = (JSONObject) providerProfile.get(LTI2Constants.CUSTOM);

		JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT);
		if ( security_contract == null  ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, "JSON missing security_contract", null);
			return;
		}

		String shared_secret = (String) security_contract.get(LTI2Constants.SHARED_SECRET);
		System.out.println("shared_secret="+shared_secret);
		if ( shared_secret == null  ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, "JSON missing shared_secret", null);
			return;
		}

		// Make sure that the requested services are a subset of the offered services
		ToolConsumer consumer = buildToolConsumerProfile(request, null, profile_id);

		JSONArray tool_services = (JSONArray) security_contract.get(LTI2Constants.TOOL_SERVICE);
		String retval = LTI2Util.validateServices(consumer, providerProfile);
		if ( retval != null ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, retval, null);
			return;
		}

		// Parse the tool profile bit and extract the tools with error checking
		retval = LTI2Util.validateCapabilities(consumer, providerProfile);
		if ( retval != null ) {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request, response, jsonRequest, retval, null);
			return;
		}

		// Pass the profile to the launch process
		PERSIST.put("profile", providerProfile.toString());

		// Share our happiness with the Tool Provider
		Map jsonResponse = new TreeMap();
		jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.TOOLPROXY_ID_CONTEXT);
		jsonResponse.put(LTI2Constants.TYPE, StandardServices.TOOLPROXY_ID_TYPE);
		jsonResponse.put(LTI2Constants.JSONLD_ID, getServiceURL(request) + SVC_tc_registration + "/" +profile_id);
		jsonResponse.put(LTI2Constants.TOOL_PROXY_GUID, profile_id);
		jsonResponse.put(LTI2Constants.CUSTOM_URL, getServiceURL(request) + SVC_Settings + "/" + LTI2Util.SCOPE_ToolProxy + "/" +profile_id);
		response.setContentType(StandardServices.TOOLPROXY_ID_FORMAT);
		response.setStatus(HttpServletResponse.SC_CREATED);
		String jsonText = JSONValue.toJSONString(jsonResponse);
		M_log.log(Level.FINE, jsonText);
		PrintWriter out = response.getWriter();
		out.println(jsonText);
	}

	public String getServiceURL(HttpServletRequest request) {
		String scheme = request.getScheme();             // http
		String serverName = request.getServerName();     // localhost
		int serverPort = request.getServerPort();        // 80
		String contextPath = request.getContextPath();   // /imsblis
		String servletPath = request.getServletPath();   // /ltitest
		String url = scheme+"://"+serverName+":"+serverPort+contextPath+servletPath+"/";
		return url;
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void handleResultRequest(HttpServletRequest request,HttpServletResponse response,
			String sourcedid) throws java.io.IOException
	{
		IMSJSONRequest jsonRequest = null;
		String retval = null;
		if ( "GET".equals(request.getMethod()) ) {
			String grade = PERSIST.get("grade");
			String comment = PERSIST.get("comment");

			Map jsonResponse = new TreeMap();
			Map resultScore = new TreeMap();

			jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.RESULT_CONTEXT);
			jsonResponse.put(LTI2Constants.TYPE, StandardServices.RESULT_TYPE);
			resultScore.put(LTI2Constants.TYPE, LTI2Constants.GRADE_TYPE_DECIMAL);
			jsonResponse.put(LTI2Constants.COMMENT, grade);
			resultScore.put(LTI2Constants.VALUE, comment);
			jsonResponse.put(LTI2Constants.RESULTSCORE,resultScore);
			response.setContentType(StandardServices.RESULT_FORMAT);
			response.setStatus(HttpServletResponse.SC_OK);
			String jsonText = JSONValue.toJSONString(jsonResponse);
			M_log.log(Level.FINE, jsonText);
			PrintWriter out = response.getWriter();
			out.println(jsonText);
			return;
		} else if ( "PUT".equals(request.getMethod()) ) {
			retval = "Error parsing input data";
			try {
				jsonRequest = new IMSJSONRequest(request);
				// System.out.println(jsonRequest.getPostBody());
				JSONObject requestData = (JSONObject) JSONValue.parse(jsonRequest.getPostBody());
				String comment = (String) requestData.get(LTI2Constants.COMMENT);
				JSONObject resultScore = (JSONObject) requestData.get(LTI2Constants.RESULTSCORE);
				String sGrade = (String) resultScore.get(LTI2Constants.VALUE);
				Double dGrade = new Double(sGrade);

				PERSIST.put("comment", comment);
				PERSIST.put("grade", dGrade+"");
				response.setStatus(HttpServletResponse.SC_OK);
				return;
			} catch (Exception e) {
				retval = "Error: "+ e.getMessage();
			}
		} else {
			retval = "Unsupported operation:" + request.getMethod();
		}

		response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
		doErrorJSON(request,response, jsonRequest, (String) retval, null);
	}

	// 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("unused")
	public void handleSettingsRequest(HttpServletRequest request,HttpServletResponse response,
			String[] parts) throws java.io.IOException
	{

		String URL = request.getRequestURL().toString();
System.out.println("URL="+URL);
		String scope = parts[4];
System.out.println("scope="+scope);

		String acceptHdr = request.getHeader("Accept");
		String contentHdr = request.getContentType();
		boolean acceptComplex = acceptHdr == null || acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) >= 0 ;

System.out.println("accept="+acceptHdr+" ac="+acceptComplex);

		// Check the JSON on PUT and check the oauth_body_hash
		IMSJSONRequest jsonRequest = null;
		JSONObject requestData = null;
		if ( "PUT".equals(request.getMethod()) ) {
			try {
				jsonRequest = new IMSJSONRequest(request);
				requestData = (JSONObject) JSONValue.parse(jsonRequest.getPostBody());
			} catch (Exception e) {
				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				doErrorJSON(request,response, jsonRequest, "Could not parse JSON", e);
				return;
			}
		}

		String consumer_key = TEST_KEY;
		String profile = PERSIST.get("profile");
		JSONObject providerProfile = (JSONObject) JSONValue.parse(profile);
		JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT);
		String oauth_secret = (String) security_contract.get(LTI2Constants.SHARED_SECRET);

		// Validate the incoming message
		LtiVerificationResult result = BasicLTIUtil.validateMessage(request, URL, oauth_secret);
		if (!result.getSuccess()) {
			response.setStatus(HttpStatus.SC_FORBIDDEN);
			doErrorJSON(request,response, jsonRequest, result.getMessage(), null);
			return;
		}

		// The URLs for the various settings resources
		String settingsUrl = getServiceURL(request) + SVC_Settings;
		String proxy_url = settingsUrl + "/" + LTI2Util.SCOPE_ToolProxy + "/" + consumer_key;
		String binding_url = settingsUrl + "/" + LTI2Util.SCOPE_ToolProxyBinding + "/" + "TBD";
		String link_url = settingsUrl + "/" + LTI2Util.SCOPE_LtiLink + "/" + "TBD";

		// Load and parse the old settings...
		JSONObject link_settings = LTI2Util.parseSettings(PERSIST.get(LTI2Util.SCOPE_LtiLink));
		JSONObject binding_settings = LTI2Util.parseSettings(PERSIST.get(LTI2Util.SCOPE_ToolProxyBinding));
		JSONObject proxy_settings = LTI2Util.parseSettings(PERSIST.get(LTI2Util.SCOPE_ToolProxy));

		// For a GET request we depend on LTI2Util to do the GET logic
		if ( "GET".equals(request.getMethod()) ) {
			Object obj = LTI2Util.getSettings(request, scope,
				link_settings, binding_settings, proxy_settings,
				link_url, binding_url, proxy_url);

			if ( obj instanceof String ) {
				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				doErrorJSON(request,response, jsonRequest, (String) obj, null);
				return;
			}

			if ( acceptComplex ) {
				response.setContentType(StandardServices.TOOLSETTINGS_FORMAT);
			} else {
				response.setContentType(StandardServices.TOOLSETTINGS_SIMPLE_FORMAT);
			}

			JSONObject jsonResponse = (JSONObject) obj;
			response.setStatus(HttpServletResponse.SC_OK);
			PrintWriter out = response.getWriter();
System.out.println("jsonResponse="+jsonResponse);
			out.println(jsonResponse.toString());
			return;
		} else if ( "PUT".equals(request.getMethod()) ) {
			// This is assuming the rule that a PUT of the complex settings
			// format that there is only one entry in the graph and it is
			// the same as our current URL.  We parse without much checking.
			String settings = null;
			try {
				JSONArray graph = (JSONArray) requestData.get(LTI2Constants.GRAPH);
				if ( graph.size() != 1 ) {
					response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
					doErrorJSON(request,response, jsonRequest, "Only one graph entry allowed", null);
					return;
				}
				JSONObject firstChild = (JSONObject) graph.get(0);
				JSONObject custom = (JSONObject) firstChild.get(LTI2Constants.CUSTOM);
				settings = custom.toString();
			} catch (Exception e) {
				settings = jsonRequest.getPostBody();
			}
			PERSIST.put(scope,settings);
System.out.println("Stored settings scope="+scope);
System.out.println("settings="+settings);
			response.setStatus(HttpServletResponse.SC_OK);
		} else {
			response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			doErrorJSON(request,response, jsonRequest, "Method not handled="+request.getMethod(), null);
		}
	}

	/* IMS JSON version of Errors */
	public void doErrorJSON(HttpServletRequest request,HttpServletResponse response,
			IMSJSONRequest json, String message, Exception e)
		throws java.io.IOException
	{
		if (e != null) {
			M_log.log(Level.WARNING, e.getLocalizedMessage(), e);
		}
        M_log.info(message);
		String output = IMSJSONRequest.doErrorJSON(request, response, json, message, e);
System.out.println(output);
    }

	public void destroy() {
	}

}


package org.imsglobal.json;

import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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;
import org.apache.commons.codec.binary.Base64;

import org.json.simple.JSONValue;

public class IMSJSONRequest {

	private final static Logger Log = Logger.getLogger(IMSJSONRequest.class .getName());

    public final static String STATUS = "status";
    public final static String STATUS_CODE = "code";
    public final static String STATUS_DESCRIPTION = "description";

	public final static String CODE_MAJOR_SUCCESS = "success";
	public final static String CODE_MAJOR_FAILURE = "failure";
	public final static String CODE_MAJOR_UNSUPPORTED = "unsupported";

	public String postBody = null;
	private String header = null;
	private String oauth_body_hash = null;
	private String oauth_consumer_key = null;

	public boolean valid = false;
	public String errorMessage = null;
	public String base_string = null;

    private static final String APPLICATION_JSON = "application/json";

	public String getOAuthConsumerKey()
	{
		return oauth_consumer_key;
	}

	public String getPostBody()
	{
		return postBody;
	}

	// Normal Constructor
	public IMSJSONRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request)
	{
		loadFromRequest(request);
		if ( ! valid ) return;
		validateRequest(oauth_consumer_key, oauth_secret, request);
	}

	// Constructor for delayed validation
	public IMSJSONRequest(HttpServletRequest request)
	{
		loadFromRequest(request);
	}

	// Constructor for testing...
	public IMSJSONRequest(String bodyString)
	{
		postBody = bodyString;
	}

	// Load but do not check the authentication
	@SuppressWarnings("deprecation")
	public void loadFromRequest(HttpServletRequest request)
	{
		header = request.getHeader("Authorization");
		System.out.println("Header: "+header);
		oauth_body_hash = null;
		if ( header != null ) {
			if (header.startsWith("OAuth ")) header = header.substring(5);
			String [] parms = header.split(",");
			for ( String parm : parms ) {
				parm = parm.trim();
				if ( parm.startsWith("oauth_body_hash=") ) {
					String [] pieces = parm.split("\"");
					if ( pieces.length == 2 ) oauth_body_hash = URLDecoder.decode(pieces[1]);
				}
				if ( parm.startsWith("oauth_consumer_key=") ) {
					String [] pieces = parm.split("\"");
					if ( pieces.length == 2 ) oauth_consumer_key = URLDecoder.decode(pieces[1]);
				}
			}
		}

		if ( oauth_body_hash == null ) {
			errorMessage = "Did not find oauth_body_hash";
			Log.info(errorMessage+"\n"+header);
			return;
		}

		System.out.println("OBH="+oauth_body_hash);
        byte[] buf = new byte[1024];
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		int chars = 0;
		try {
			ServletInputStream is = request.getInputStream();
			int readNum;
			do {
				readNum = is.read(buf);
				if (readNum>0) {
					bos.write(buf, 0, readNum);
					chars = chars + readNum;
					// We dont' want a DOS
					if ( chars > 10000000 ) {
						errorMessage = "Message body size exceeded";
						return;
					}
				}
			} while (readNum>=0);
		} catch(Exception e) {
			errorMessage = "Could not read message body:"+e.getMessage();
			return;
		}

        byte[] bytes = bos.toByteArray();

		try {
			postBody = new String(bytes, "UTF-8");
			MessageDigest md = MessageDigest.getInstance("SHA1");
			md.update(bytes);
			byte[] output = Base64.encodeBase64(md.digest());
			String hash = new String(output);
			System.out.println("HASH="+hash+" bytes="+bytes.length);
			if ( ! hash.equals(oauth_body_hash) ) {
				errorMessage = "Body hash does not match. bytes="+bytes.length;
				System.out.println(postBody);
				return;
			}
		} catch (Exception e) {
			errorMessage = "Could not compute body hash.  bytes="+bytes.length;
			return;
		}
		valid = true;  // So far we are valid
	}

	// Assumes data is all loaded
	public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request)
	{
		validateRequest(oauth_consumer_key, oauth_secret, request, null) ;
	}

	public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request, String URL)
	{
		valid = false;
		OAuthMessage oam = OAuthServlet.getMessage(request, URL);
		OAuthValidator oav = new SimpleOAuthValidator();
		OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed",
				oauth_consumer_key, oauth_secret, null);

		OAuthAccessor acc = new OAuthAccessor(cons);

		try {
			base_string = OAuthSignatureMethod.getBaseString(oam);
		} catch (Exception e) {
			base_string = null;
		}

		try {
			oav.validateMessage(oam,acc);
		} catch(Exception e) {
			errorMessage = "Launch fails OAuth validation: "+e.getMessage();
			return;
		}
		valid = true;
	}

	public boolean inArray(final String [] theArray, final String theString)
	{
		if ( theString == null ) return false;
		for ( String str : theArray ) {
			if ( theString.equals(str) ) return true;
		}
		return false;
	}

	public static Map<String, String> getStatusUnsupported(String desc)
	{
		return getStatus(desc, CODE_MAJOR_UNSUPPORTED);
	}

	public static Map<String, String> getStatusFailure(String desc)
	{
		return getStatus(desc, CODE_MAJOR_FAILURE);
	}

	public static Map<String, String> getStatusSuccess(String desc)
	{
		return getStatus(desc, CODE_MAJOR_SUCCESS);
	}

	public static Map<String, String> getStatus(String description, String major)
	{
		Map<String, String> retval = new LinkedHashMap<String, String>();
		retval.put(STATUS_CODE,major);
		retval.put(STATUS_DESCRIPTION,description);
		return retval;
	}

	/* IMS JSON version of Errors - does the complet request - returns the JSON in case
	   the code above us wants to log it. */
	@SuppressWarnings("static-access")
	public static String doErrorJSON(HttpServletRequest request,HttpServletResponse response,
			IMSJSONRequest json, String message, Exception e)
		throws java.io.IOException
	{
		response.setContentType(APPLICATION_JSON);
		Map<String, Object> jsonResponse = new TreeMap<String, Object>();

		Map<String, String> status = null;
		if ( json == null ) {
			status = IMSJSONRequest.getStatusFailure(message);
		} else {
			status = json.getStatusFailure(message);
			if ( json.base_string != null ) {
				jsonResponse.put("base_string", json.base_string);
			}
		}
		jsonResponse.put(IMSJSONRequest.STATUS, status);
		if ( e != null ) {
			jsonResponse.put("exception", e.getLocalizedMessage());
			try {
				StringWriter sw = new StringWriter();
				PrintWriter pw = new PrintWriter(sw, true);
				e.printStackTrace(pw);
				pw.flush();
				sw.flush();
				jsonResponse.put("traceback", sw.toString() );
			} catch ( Exception f ) {
				jsonResponse.put("traceback", f.getLocalizedMessage());
			}
		}
		String jsonText = JSONValue.toJSONString(jsonResponse);
		PrintWriter out = response.getWriter();
		out.println(jsonText);
		return jsonText;
	}

	/** Unit Tests */
	static final String inputTestData = "<?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>999999123</imsx_messageIdentifier>\n" +
		"</imsx_POXRequestHeaderInfo>\n" +
		"</imsx_POXHeader>\n" +
		"<imsx_POXBody>\n" +
		"<replaceResultRequest>\n" +
		"<resultRecord>\n" +
		"<sourcedGUID>\n" +
		"<sourcedId>3124567</sourcedId>\n" +
		"</sourcedGUID>\n" +
		"<result>\n" +
		"<resultScore>\n" +
		"<language>en-us</language>\n" +
		"<textString>A</textString>\n" +
		"</resultScore>\n" +
		"</result>\n" +
		"</resultRecord>\n" +
		"</replaceResultRequest>\n" +
		"</imsx_POXBody>\n" +
		"</imsx_POXEnvelopeRequest>";

	public static void runTest() {
/*
		System.out.println("Runnig test.");
		IMSJSONRequest pox = new IMSJSONRequest(inputTestData);
		System.out.println("Version = "+pox.getHeaderVersion());
		System.out.println("Operation = "+pox.getOperation());
		Map<String,String> bodyMap = pox.getBodyMap();
		String guid = bodyMap.get("/resultRecord/sourcedGUID/sourcedId");
		System.out.println("guid="+guid);
		String grade = bodyMap.get("/resultRecord/result/resultScore/textString");
		System.out.println("grade="+grade);

		String desc = "Message received and validated operation="+pox.getOperation()+
			" guid="+guid+" grade="+grade;

		String output = pox.getResponseUnsupported(desc);
		System.out.println("---- Unsupported ----");
		System.out.println(output);

		Properties props = new Properties();
		props.setProperty("fred","zap");
		props.setProperty("sam",IMSPOXRequest.MINOR_IDALLOC);
		System.out.println("---- Generate Log Error ----");
		output = pox.getResponseFailure(desc,props);
		System.out.println("---- Failure ----");
		System.out.println(output);

		Map<String, Object> theMap = new TreeMap<String, Object> ();
		theMap.put("/readMembershipResponse/membershipRecord/sourcedId", "123course456");

		List<Map<String,String>> lm = new ArrayList<Map<String,String>>();
		Map<String,String> mm = new TreeMap<String,String>();
		mm.put("/personSourcedId","123user456");
		mm.put("/role/roleType","Learner");
		lm.add(mm);

		mm = new TreeMap<String,String>();
		mm.put("/personSourcedId","789user123");
		mm.put("/role/roleType","Instructor");
		lm.add(mm);
		theMap.put("/readMembershipResponse/membershipRecord/membership/member", lm);

		String theXml = XMLMap.getXMLFragment(theMap, true);
		// System.out.println("th="+theXml);
		output = pox.getResponseSuccess(desc,theXml);
		System.out.println("---- Success String ----");
		System.out.println(output);
*/
	}

	/*

roleType:
Learner
Instructor
ContentDeveloper
Member
Manager
Mentor
Administrator
TeachingAssistant

fieldType:
Boolean
Integer
Real
String

<readMembershipResponse
xmlns="http://www.imsglobal.org/services/lis/mms2p0/wsdl11/sync/imsmms_v2p0">
<membershipRecord>
<sourcedId>GUID.TYPE</sourcedId>
<membership>
<collectionSourcedId>GUID.TYPE</collectionSourcedId>
<membershipIdType>MEMBERSHIPIDTYPE.TYPE</membershipIdType>
<member>
<personSourcedId>GUID.TYPE</personSourcedId>
<role>
<roleType>STRING</roleType>
<subRole>STRING</subRole>
<timeFrame>
<begin>DATETIME</begin>
<end>DATETIME</end>
<restrict>BOOLEAN</restrict>
<adminPeriod>
<language>LANGUAGESET.TYPE</language>
<textString>STRING</textString>
</adminPeriod>
</timeFrame>
<status>STATUS.TYPE</status>
<dateTime>DATETIME</dateTime>
<dataSource>GUID.TYPE</dataSource>
<recordInfo>
<extensionField>
<fieldName>STRING</fieldName>
<fieldType>FIELDTYPE.TYPE</fieldType>
<fieldValue>STRING</fieldValue>
</extensionField>
</recordInfo>
<extension>
<extensionField>
<fieldName>STRING</fieldName>
<fieldType>FIELDTYPE.TYPE</fieldType>
<fieldValue>STRING</fieldValue>
</extensionField>
</extension>
</role>
</member>
<creditHours>INTEGER</creditHours>
<dataSource>GUID.TYPE</dataSource>
</membership>
</membershipRecord>
</readMembershipResponse>
	 */
}