org.tsugi.lti2.LTI2Servlet.java Source code

Java tutorial

Introduction

Here is the source code for org.tsugi.lti2.LTI2Servlet.java

Source

/**
 * Copyright (c) 2013- Charles R. Severance
 *
 * 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.tsugi.lti2;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tsugi.basiclti.BasicLTIConstants;
import org.tsugi.basiclti.BasicLTIUtil;
import org.tsugi.json.IMSJSONRequest;
import org.tsugi.lti2.objects.Service_offered;
import org.tsugi.lti2.objects.StandardServices;
import org.tsugi.lti2.objects.ToolConsumer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

/**
 * 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:
 *
 *  <servlet>
 *    <servlet-name>SampleServlet</servlet-name>
 *    <servlet-class>org.tsugi.lti2.LTI2Servlet</servlet-class>
 *  </servlet>
 *  <servlet-mapping>
 *    <servlet-name>SampleServlet</servlet-name>
 *    <url-pattern>/sample/*</url-pattern>
 *  </servlet-mapping>
 *
 *  The 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 Log M_log = LogFactory.getLog(LTI2Servlet.class);

    protected Service_offered LTI2ResultItem = null;
    protected Service_offered LTI2LtiLinkSettings = null;
    protected Service_offered LTI2ToolProxyBindingSettings = null;
    protected Service_offered 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.warn("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.warn("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(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 = M_log.isDebugEnabled();
            // TODO: Internationalize this
            // String launchtext = getRB(rb, "launch.button", "Press to Launch External Tool");
            String launchtext = "Press to Launch External Tool";
            output = BasicLTIUtil.postLaunchHTML(ltiProps, launch_url, launchtext, dodebug, null);
        } else {
            output = "<form>Register URL:<br/><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 {
            ToolProxy toolProxy = new ToolProxy(profile);

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

            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, false);

            Map<String, String> extra = new HashMap<String, String>();
            ltiProps = BasicLTIUtil.signProperties(ltiProps, launch, "POST", TEST_KEY, shared_secret, null, null,
                    null, extra);

            boolean dodebug = M_log.isDebugEnabled();
            // TODO: Internationalize this
            // String launchtext = getRB(rb, "launch.button", "Press to Launch External Tool");
            String launchtext = "Press to Launch External Tool";
            output = BasicLTIUtil.postLaunchHTML(ltiProps, launch, launchtext, dodebug, extra);
        }

        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.writerWithDefaultPrettyPrinter();
            // ***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.tsugi.lti2.LTI2ConfigSample();

        ToolConsumer consumer = new ToolConsumer(profile_id + "", getServiceURL(request) + "#", cnf);

        // Normally we would check permissions before we offer capabilities
        consumer.allowEmail();
        consumer.allowName();
        consumer.allowSettings();
        consumer.allowResult();
        // Keep our life simple
        // consumer.allowSplitSecret();
        // consumer.allowHmac256();

        // Normally we would check permissions before we offer services
        List<Service_offered> 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;
        }

        ToolProxy toolProxy = null;
        try {
            toolProxy = new ToolProxy(jsonRequest.getPostBody());
            // System.out.println("OBJ:"+toolProxy);
        } catch (Throwable t) {
            t.printStackTrace();
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            doErrorJSON(request, response, jsonRequest, "JSON parse failed", null);
            return;
        }

        JSONObject default_custom = toolProxy.getCustom();

        JSONObject security_contract = toolProxy.getSecurityContract();
        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 = toolProxy.validateServices(consumer);
        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 = toolProxy.validateCapabilities(consumer);
        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", toolProxy.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.debug(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.debug(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");
        ToolProxy toolProxy = new ToolProxy(profile);
        JSONObject security_contract = toolProxy.getSecurityContract();
        String oauth_secret = (String) security_contract.get(LTI2Constants.SHARED_SECRET);

        // Validate the incoming message
        Object retval = BasicLTIUtil.validateMessage(request, URL, oauth_secret, consumer_key);
        if (retval instanceof String) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            doErrorJSON(request, response, jsonRequest, (String) retval, 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.error(e.getLocalizedMessage(), e);
        }
        M_log.info(message);
        String output = IMSJSONRequest.doErrorJSON(request, response, json, message, e);
        System.out.println(output);
    }

    public void destroy() {
    }

}