Index: 3rdParty_sources/basiclti-util/LICENSE =================================================================== diff -u --- 3rdParty_sources/basiclti-util/LICENSE (revision 0) +++ 3rdParty_sources/basiclti-util/LICENSE (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,248 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +=============================================================================== + +The BasicLTI Utilities (basiclti-util) distribution includes a number of subcomponents +with separate copyright notices and license terms. Your use of the +code for the these subcomponents is subject to the terms and +conditions of the following licenses. + +=============================================================================== +OAuth: + +Copyright (c) 1998-2009 AOL LLC. +Copyright 2007, 2008 Google, Inc. +Copyright 2007, 2008 Netflix, Inc. + +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. + +=============================================================================== +Base64: + +Copyright 1999-2008 The Apache Software Foundation. + +The ASF licenses this file to You 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. + +=============================================================================== Index: 3rdParty_sources/basiclti-util/NOTICE =================================================================== diff -u --- 3rdParty_sources/basiclti-util/NOTICE (revision 0) +++ 3rdParty_sources/basiclti-util/NOTICE (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,10 @@ +BasicLTI Utilities (basiclti-util) +Copyright 2010 IMS Global Learning Consortium www.imsglobal.org + +----------------------------------------------------------- + +This product includes software (OAuth) developed by +AOL LLC., Google, Inc., and Netflix, Inc. (http://code.google.com/p/oauth/). + +This product includes software (Base64) developed at +The Apache Software Foundation (http://www.apache.org/). Index: 3rdParty_sources/basiclti-util/README.md =================================================================== diff -u --- 3rdParty_sources/basiclti-util/README.md (revision 0) +++ 3rdParty_sources/basiclti-util/README.md (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,116 @@ +#IMS Global - LTI™ Utilities + +[![Build Status](https://travis-ci.org/pfgray/basiclti-util-java.svg?branch=master)](https://travis-ci.org/pfgray/basiclti-util-java) + +What is it? +----------- + +LTI™ Utilities are a set of utility classes to aid in the development +of LTI v1.0 consumers and providers. They deal with much of the heavy lifting +and make the process more opaque to the developer. + + +How to use: +----------- + +With Maven: +Include in your project's `pom.xml`: + +```xml + + org.imsglobal + basiclti-util + 1.1.2 + +``` + +This library provides support for: + +**Tool Providers**: + + 1. Verifying an LTI launch request + 2. Sending LTI 1.1 Outcomes request (xml-based) + 3. AspectJ launch verifiers for easy integration with Spring-web. + +**Tool Consumers**s: + + 1. Creating a valid LTI launch request + +Some exploratory support for LTIv2: + + 1. Parsing Tool Profiles + 2. Validating Tool Capabilities & Services + + +LTI Providers: +---- + +**Verifying an LTI launch request.** + +```java +HttpServletRequest request; // java servlet request +LtiVerifier ltiVerifier = new LtiOauthVerifier(); +String key = request.getParameter("oauth_consumer_key"); +String secret = // retrieve corresponding secret for key from db +LtiVerificationResult ltiResult = ltiVerifier.verify(request, secret); +``` + +**Sending LTI 1.1 Outcomes request (xml-based).** + +```java +//send Request directly +IMSPOXRequest.sendReplaceResult(url, key, secret, sourcedid, score); + +//or build the request to send later: +HttpPost request = IMSPOXRequest.buildReplaceResult(url, key, secret, sourcedid, score, true); +``` + +**AspectJ launch verifiers for easy integration with Spring-web.** + +Spring Controller (LTI Producer): +```java +@Lti +@RequestMapping(value = "/lti", method = RequestMethod.POST) +public String ltiEntry(HttpServletRequest request, LtiVerificationResult result) { + if(!result.getSuccess()){ + return "error"; + } else { + return "success"; + } +} +``` + +KeyService Implementation: +```java +public class MockKeyService implements LtiKeySecretService { + public String getSecretForKey(String key) { + return "secret"; + } +} +``` + +Spring Context xml: +```xml + + + + + + + +``` + +LTI Consumers: +---- + +**Building an LTI launch request.** + +```java +LtiSigner ltiSigner = new LtiOauthSigner(); +Map signedParameters = signParameters(parameters, key, secret, url, "POST"); +``` + + +© 2014 IMS Global Learning Consortium, Inc. All Rights Reserved. + + Index: 3rdParty_sources/basiclti-util/pom.xml =================================================================== diff -u --- 3rdParty_sources/basiclti-util/pom.xml (revision 0) +++ 3rdParty_sources/basiclti-util/pom.xml (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,180 @@ + + + 4.0.0 + + IMS BasicLTI Utilities + BasicLTI Utilities are a set of utility classes to aid in the development of BasicLTI consumers and + providers. They deal with much of the heavy lifting and make the process more opaque to the developer. + + https://github.com/IMSGlobal/basiclti-util-java + org.imsglobal + basiclti-util + 1.2.0-SNAPSHOT + + IMS Global Learning Consortium + www.imsglobal.org/ + + 2009 + jar + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + Charles Severance + csev@umich.edu + University of Michigan + https://www.si.umich.edu/ + + + Paul Gray + pfbgray@gmail.com + Learning Objects + http://www.learningobjects.com/ + + + Braden Anderson + braden@instructure.com + Instructure + http://www.instructure.com/ + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.1 + + + + + + + + scm:git:git@github.com:IMSGlobal/basiclti-util-java.git + scm:git:git@github.com:IMSGlobal/basiclti-util-java.git + git@github.com:IMSGlobal/basiclti-util-java.git + + + + 1.5.5 + UTF-8 + + + + + javax.servlet + servlet-api + 2.5 + provided + + + commons-lang + commons-lang + 2.6 + + + net.oauth.core + oauth-provider + 20100527 + + + org.apache.httpcomponents + httpclient + 4.2.2 + + + oauth.signpost + signpost-core + 1.2.1.2 + + + oauth.signpost + signpost-commonshttp4 + 1.2.1.2 + + + commons-io + commons-io + 2.4 + jar + + + com.google.code.gson + gson + 2.2.4 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + Index: 3rdParty_sources/basiclti-util/readme by LAMS.txt =================================================================== diff -u --- 3rdParty_sources/basiclti-util/readme by LAMS.txt (revision 0) +++ 3rdParty_sources/basiclti-util/readme by LAMS.txt (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,6 @@ +LAMS notice: +---- +It has been modified by LAMS. + +* LDEV-4221: Some of the integrations complain about outcome call request format (when LAMS is used as LTI Tool Provider) +Which required changes to be done for org.imsglobal.pox.IMSPOXRequest.java \ No newline at end of file Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/Lti.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/Lti.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/Lti.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package org.imsglobal.aspect; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author pgray + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +public @interface Lti { + + boolean rejectIfBad() default true; + + String keyService() default ""; + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/LtiKeySecretService.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/LtiKeySecretService.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/aspect/LtiKeySecretService.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,10 @@ +package org.imsglobal.aspect; + +/** + * Created by paul on 5/28/14. + */ +public interface LtiKeySecretService { + + public String getSecretForKey(String key); + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/json/IMSJSONRequest.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/json/IMSJSONRequest.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/json/IMSJSONRequest.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,401 @@ +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 com.google.gson.Gson; +import com.google.gson.JsonArray; + +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 getStatusUnsupported(String desc) + { + return getStatus(desc, CODE_MAJOR_UNSUPPORTED); + } + + public static Map getStatusFailure(String desc) + { + return getStatus(desc, CODE_MAJOR_FAILURE); + } + + public static Map getStatusSuccess(String desc) + { + return getStatus(desc, CODE_MAJOR_SUCCESS); + } + + public static Map getStatus(String description, String major) + { + Map retval = new LinkedHashMap(); + 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 jsonResponse = new TreeMap(); + + Map 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()); + } + } + Gson gson = new Gson(); + String jsonText = gson.toJson(jsonResponse); + PrintWriter out = response.getWriter(); + out.println(jsonText); + return jsonText; + } + + /** Unit Tests */ + static final String inputTestData = "\n" + + "\n" + + "\n" + + "\n" + + "V1.0\n" + + "999999123\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "3124567\n" + + "\n" + + "\n" + + "\n" + + "en-us\n" + + "A\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + 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 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 theMap = new TreeMap (); + theMap.put("/readMembershipResponse/membershipRecord/sourcedId", "123course456"); + + List> lm = new ArrayList>(); + Map mm = new TreeMap(); + mm.put("/personSourcedId","123user456"); + mm.put("/role/roleType","Learner"); + lm.add(mm); + + mm = new TreeMap(); + 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 + + + +GUID.TYPE + +GUID.TYPE +MEMBERSHIPIDTYPE.TYPE + +GUID.TYPE + +STRING +STRING + +DATETIME +DATETIME +BOOLEAN + +LANGUAGESET.TYPE +STRING + + +STATUS.TYPE +DATETIME +GUID.TYPE + + +STRING +FIELDTYPE.TYPE +STRING + + + + +STRING +FIELDTYPE.TYPE +STRING + + + + +INTEGER +GUID.TYPE + + + + */ +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIConstants.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIConstants.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIConstants.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,444 @@ +/* + * 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; + +public class BasicLTIConstants { + /** + * context_id=8213060-006f-27b2066ac545 + *

+ * This is an opaque identifier that uniquely identifies the context that + * contains the link being launched. + */ + public static final String CONTEXT_ID = "context_id"; + /** + * context_label=SI182 + *

+ * A label for the context - intended to fit in a column. + */ + public static final String CONTEXT_LABEL = "context_label"; + /** + * context_title=Design of Personal Environments + *

+ * A title of the context - it should be about the length of a line. + */ + public static final String CONTEXT_TITLE = "context_title"; + + /** + * context_type=CourseSection + *

+ * This string is a comma-separated list of URN values that identify the type + * of context. At a minimum, the list MUST include a URN value drawn from the + * LIS vocabulary (see Appendix A). The assumed namespace of these URNs is the + * LIS vocabulary so TCs can use the handles when the intent is to refer to an + * LIS context type. If the TC wants to include a context type from another + * namespace, a fully-qualified URN should be used. + */ + public static final String CONTEXT_TYPE = "context_type"; + public static final String CONTEXT_TYPE_COURSE_OFFERING = "CourseOffering"; + public static final String CONTEXT_TYPE_COURSE_SECTION = "CourseSection"; + public static final String CONTEXT_TYPE_COURSE_TEMPLATE = "CourseTemplate"; + public static final String CONTEXT_TYPE_GROUP = "Group"; + + /** + * ext_param=value + *

+ * Systems can add their own values to the launch but should prefix + * any extensions with "ext_". + */ + public static final String EXTENSION_PREFIX = "ext_"; + /** + * custom_keyname=value + *

+ * The creator of a Basic LTI link can add custom key/value parameters to a + * launch which are to be included with the launch of the Basic LTI link. The + * Common Cartridge section below describes how these parameters are + * represented when storing custom parameters in a Common Cartridge. + *

+ * When there are custom name / value parameters in the launch, a POST + * parameter is included for each custom parameter. 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". So if a custom entry + * was as follows: + *

+ * Review:Chapter=1.2.56 + *

+ * Would map to: custom_review_chapter=1.2.56 + *

+ * Creators of Basic LTI links would be well served to limit their parameter + * names to lower case and to use no punctuation other than underscores. If + * these custom parameters are included in the Basic LTI link, the TC must + * include them in the launch data or the TP may fail to function. + */ + public static final String CUSTOM_PREFIX = "custom_"; + /** + * Parameters with the OAuth prefix are also acceptible. + */ + public static final String OAUTH_PREFIX = "oauth_"; + /** + * launch_presentation_document_target=iframe + *

+ * The value should be either 'frame', 'iframe' or 'window'. This field + * communicates the kind of browser window/frame where the TC has launched the + * tool. + */ + public static final String LAUNCH_PRESENTATION_DOCUMENT_TARGET = "launch_presentation_document_target"; + /** + * launch_presentation_height=240 + *

+ * The height of the window or frame where the content from the tool will be + * displayed. + */ + public static final String LAUNCH_PRESENTATION_HEIGHT = "launch_presentation_height"; + /** + * launch_presentation_locale=en_US_variant + *

+ * Language, country and variant separated by underscores. Language is the + * lower-case, two-letter code as defined by ISO-639 (list of codes available + * at http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt). Country is the + * upper-case, two-letter code as defined by ISO-3166 (list of codes available + * at http://www.chemie.fu- berlin.de/diverse/doc/ISO_3166.html). Country and + * variant codes are optional. + */ + public static final String LAUNCH_PRESENTATION_LOCALE = "launch_presentation_locale"; + /** + * launch_presentation_return_url=http://lmsng.school.edu/portal/123/page/988/ + *

+ * Fully qualified URL where the TP can redirect the user back to the TC interface. This + * URL can be used once the TP is finished or if the TP cannot start or has some + * technical difficulty. In the case of an error, the TP may add a parameter called + * lti_errormsg that includes some detail as to the nature of the error. The + * lti_errormsg value should make sense if displayed to the user. If the tool has + * displayed a message to the end user and only wants to give the TC a message to log, + * use the parameter lti_errorlog instead of lti_errormsg. If the tool is terminating + * normally, and wants a message displayed to the user it can include a text message as + * the lti_msg parameter to the return URL. If the tool is terminating normally and + * wants to give the TC a message to log, use the parameter lti_log. This data should be + * sent on the URL as a GET - so the TP should take care to keep the overall length of + * the parameters small enough to fit within the limitations of a GET request. + */ + public static final String LAUNCH_PRESENTATION_RETURN_URL = "launch_presentation_return_url"; + /** + * launch_presentation_width=320 + *

+ * The width of the window or frame where the content from the tool will be + * displayed. + */ + public static final String LAUNCH_PRESENTATION_WIDTH = "launch_presentation_width"; + /** + * launch_presentation_css_url=http://www.toolconsumer.url/path/to/lti.css + *

+ * This points to a fully qualified URL for a CSS which can be used to style the tool. + * There are no officially defined CSS classes for this file, but the Consumer can + * apply styles to paragraphs, body, and the various HTML elements. It is up to the + * tool as to whether this CSS is used or not, and in what order this is included relative + * to the tool-specific CSS. + */ + public static final String LAUNCH_PRESENTATION_CSS_URL = "launch_presentation_css_url"; + /** + * lis_person_contact_email_primary=user@school.edu + *

+ * These fields contain information about the user account that is performing + * this launch. The names of these data items are taken from LIS. The precise + * meaning of the content in these fields is defined by LIS. + */ + public static final String LIS_PERSON_CONTACT_EMAIL_PRIMARY = "lis_person_contact_email_primary"; + /** + * lis_person_name_family=Public + *

+ * These fields contain information about the user account that is performing + * this launch. The names of these data items are taken from LIS. The precise + * meaning of the content in these fields is defined by LIS. + */ + public static final String LIS_PERSON_NAME_FAMILY = "lis_person_name_family"; + /** + * lis_person_name_full=Jane Q. Public + *

+ * These fields contain information about the user account that is performing + * this launch. The names of these data items are taken from LIS. The precise + * meaning of the content in these fields is defined by LIS. + */ + public static final String LIS_PERSON_NAME_FULL = "lis_person_name_full"; + /** + * lis_person_name_given=Jane + *

+ * These fields contain information about the user account that is performing + * this launch. The names of these data items are taken from LIS. The precise + * meaning of the content in these fields is defined by LIS. + */ + public static final String LIS_PERSON_NAME_GIVEN = "lis_person_name_given"; + + /** + * lis_person_sourcedid=school.edu:user + *

+ * This field contains the LIS identifier for the user account that is + * performing this launch. The example syntax of "school:user" + * is not the required format – lis_person_sourcedid is simply a + * unique identifier (i.e., a normalized string). This field + * is optional and its content and meaning are defined by LIS. + */ + public static final String LIS_PERSON_SOURCEDID = "lis_person_sourcedid"; + + /** + * lis_course_offering_sourcedid=school.edu:SI182-F08 + * lis_course_section_sourcedid=school.edu:SI182-001-F08 + *

+ * These fields contain LIS course identifiers associated with the + * context of this launch. These fields are optional and their + * content and meaning are defined by LIS. + */ + public static final String LIS_COURSE_OFFERING_SOURCEDID = "lis_course_offering_sourcedid"; + public static final String LIS_COURSE_SECTION_SOURCEDID = "lis_course_section_sourcedid"; + + /** + * lis_outcome_service_url=http://lmsng.school.edu/service/ltiout/ + *

+ * This field should be no more than 1023 characters long and the TP + * should assume that the URL might change from launch to launch and + * allow for the fact that a TC might change their service URL from + * launch to launch and only use the most recent value for this + * parameter. The service URL may support various operations / services. + * The TC will respond with a response of 'unimplemented' for services + * it does not support. + */ + public static final String LIS_OUTCOME_SERVICE_URL = "lis_outcome_service_url"; + + /** + * lis_result_sourcedid=83873872987329873264783687634 + *

+ * This field contains an identifier that indicates the LIS Result + * Identifier (if any) associated with this launch. This field is + * optional and its content and meaning is defined by LIS. + */ + public static final String LIS_RESULT_SOURCEDID = "lis_result_sourcedid"; + + /** + * lti_message_type=basic-lti-launch-request + *

+ * This indicates that this is a Basic LTI Launch Message. This allows a TP to + * accept a number of different LTI message types at the same launch URL. This + * parameter is required. + */ + public static final String LTI_MESSAGE_TYPE = "lti_message_type"; + public static final String LTI_MESSAGE_TYPE_TOOLPROXYREGISTRATIONREQUEST = "ToolProxyRegistrationRequest"; + public static final String LTI_MESSAGE_TYPE_TOOLPROXY_RE_REGISTRATIONREQUEST = "ToolProxyReregistrationRequest"; + public static final String LTI_MESSAGE_TYPE_BASICLTILAUNCHREQUEST = "basic-lti-launch-request"; + /** + * lti_version=LTI-1p0 + *

+ * This indicates which version of the specification is being used for this + * particular message. This parameter is required. + */ + public static final String LTI_VERSION = "lti_version"; + public static final String LTI_VERSION_1 = "LTI-1p0"; + public static final String LTI_VERSION_2 = "LTI-2p0"; + + /** + * tool_consumer_info_product_family_code=desire2learn + *

+ * In order to better assist tools in using extensions and also making their user + * interface fit into the TC's user interface that they are being called from, + * each TC is encouraged to include the this parameter. Possible example values + * for this field might be: + * + * learn + * desire2learn + * sakai + * eracer + * olat + * webct + * This parameter is optional but recommended. + */ + public static final String TOOL_CONSUMER_INFO_PRODUCT_FAMILY_CODE = "tool_consumer_info_product_family_code"; + + /** + * tool_consumer_info_version=9.2.4 + *

+ * This field should have a major release number followed by a period. The format of the minor release is flexible. Possible vaues for this field might be: + * + * 9.1.7081 + * 2.8-01 + * 7.1 + * 8 + * The Tool Provider should be flexible when parsing this field. This parameter is optional but recommended. + + */ + public static final String TOOL_CONSUMER_INFO_VERSION = "tool_consumer_info_version"; + + /** + * resource_link_id=88391-e1919-bb3456 + *

+ * This is an opaque unique identifier that the TC guarantees will be unique + * within the TC for every placement of the link. If the tool / activity is + * placed multiple times in the same context, each of those placements will be + * distinct. This value will also change if the item is exported from one + * system or context and imported into another system or context. This + * parameter is required. + */ + public static final String RESOURCE_LINK_ID = "resource_link_id"; + + /** + * resource_link_title=My Weekly Wiki + *

+ * A title for the resource. This is the clickable text that appears + * in the link. This parameter is recommended. + */ + public static final String RESOURCE_LINK_TITLE = "resource_link_title"; + + /** + * resource_link_description=… + *

+ * A plain text description of the link’s destination, suitable for + * display alongside the link. Typically no more than several lines + * long. This parameter is optional. + */ + public static final String RESOURCE_LINK_DESCRIPTION = "resource_link_description"; + + /** + * roles=Instructor,Student + *

+ * A comma-separated list of URN values for roles. If this list is non-empty, + * it should contain at least one role from the LIS System Role, LIS + * Institution Role, or LIS Context Role vocabularies (See Appendix A). The + * assumed namespace of these URNs is the LIS vocabulary of LIS Context Roles + * so TCs can use the handles when the intent is to refer to an LIS context + * role. If the TC wants to include a role from another namespace, a + * fully-qualified URN should be used. Usage of roles from non-LIS + * vocabularies is discouraged as it may limit interoperability. This + * parameter is recommended. + */ + public static final String ROLES = "roles"; + + /** + * tc_profile_url=http://... + *

+ * This URL specifies the address where the Tool Provider can retrieve + * the Tool Consumer Profile. This URL must be retrievable by a GET + * request by the Tool Provider. If the URL is protected from retrieval + * in general, the Tool Consumer must append the necessary parameters to + * allow the Tool Provider to retrieve the URL with nothing more than + * a GET request. It is legal for this URL to contain a security token + * that is changed for each ToolProxyRegistrationRequest so the Tool + * Provider must retrieve the tc_profile_url on each request. + */ + public static final String TC_PROFILE_URL = "tc_profile_url"; + + /** + * tool_consumer_instance_contact_email=System.Admin@school.edu + *

+ * An email contact for the TC instance. + */ + public static final String TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL = "tool_consumer_instance_contact_email"; + /** + * tool_consumer_instance_description=University of School (LMSng) + *

+ * This is a user visible field - it should be about the length of a line. + */ + public static final String TOOL_CONSUMER_INSTANCE_DESCRIPTION = "tool_consumer_instance_description"; + // global settings + /** + * tool_consumer_instance_guid=lmsng.school.edu + *

+ * This is a key to be used when setting a TC-wide password. The TP uses this + * as a key to look up the TC-wide secret when validating a message. A common + * practice is to use the DNS of the organization or the DNS of the TC + * instance. If the organization has multiple TC instances, then the best + * practice is to prefix the domain name with a locally unique identifier for + * the TC instance. This parameter is recommended. + */ + public static final String TOOL_CONSUMER_INSTANCE_GUID = "tool_consumer_instance_guid"; + /** + * tool_consumer_instance_name=SchoolU + *

+ * This is a user visible field - it should be about the length of a column. + */ + public static final String TOOL_CONSUMER_INSTANCE_NAME = "tool_consumer_instance_name"; + /** + * Missing from implementation guide. Needs documentation. Not required, but + * "tasty". + */ + public static final String TOOL_CONSUMER_INSTANCE_URL = "tool_consumer_instance_url"; + + /** + * user_id=0ae836b9-7fc9-4060-006f-27b2066ac545 + *

+ * Uniquely identifies the user. This should not contain any identifying + * information for the user. Best practice is that this field should be a + * TC-generated long-term "primary key" to the user record - not the logical + * key. This parameter is recommended. + */ + public static final String USER_ID = "user_id"; + + /** + * user_image=http://my.sakai.org/direct/profile/0ae836b9-7fc9-4060-006f-27b2066ac545/image + *

+ * This attribute specifies the URI for an image of the user who launched this request. + * This image is suitable for use as a "profile picture" or an avatar representing the user. + * It is expected to be a relatively small graphic image file using a widely supported image + * format (i.e. PNG, JPG, or GIF) with a square aspect ratio. This parameter is optional. + */ + public static final String USER_IMAGE = "user_image"; + + /** + * ext_sakai_provider_eid=jsmith26 + *

+ * If set, this will signal that the external application has provided an eid which + * should be used preferentially. Many external applications will not have access to a user's uuid + * in Sakai, so this allows integrations with those systems. + * This parameter is optional and is unique to the Sakai Basic LTI provider. + */ + public static final String EXT_SAKAI_PROVIDER_EID = "ext_sakai_provider_eid"; + + /** + * ext_sakai_provider_displayid=john.smith + *

+ * If set, this will indicate to an external application that the user is normally + * known by this ID and when displaying the ID to the user this ID should be used instead of the + * user_id and ext_sakai_provider_eid. + * This parameter is optional and is unique to the Sakai Basic LTI provider. + */ + public static final String EXT_SAKAI_PROVIDER_DISPLAYID = "ext_sakai_provider_displayid"; + + /** + * Utility array useful for validating property names when building launch + * data. + */ + public static final String[] validPropertyNames = { CONTEXT_ID, + CONTEXT_LABEL, CONTEXT_TITLE, CONTEXT_TYPE, + LAUNCH_PRESENTATION_DOCUMENT_TARGET, LAUNCH_PRESENTATION_HEIGHT, + LAUNCH_PRESENTATION_LOCALE, LAUNCH_PRESENTATION_RETURN_URL, + LAUNCH_PRESENTATION_WIDTH, LIS_PERSON_CONTACT_EMAIL_PRIMARY, + LAUNCH_PRESENTATION_CSS_URL, + TOOL_CONSUMER_INFO_PRODUCT_FAMILY_CODE, + TOOL_CONSUMER_INFO_VERSION, + LIS_PERSON_NAME_FAMILY, LIS_PERSON_NAME_FULL, LIS_PERSON_NAME_GIVEN, + LIS_PERSON_SOURCEDID, LIS_COURSE_OFFERING_SOURCEDID, + LIS_COURSE_SECTION_SOURCEDID, + LIS_OUTCOME_SERVICE_URL, LIS_RESULT_SOURCEDID, + LTI_MESSAGE_TYPE, LTI_VERSION, RESOURCE_LINK_ID, + RESOURCE_LINK_TITLE, RESOURCE_LINK_DESCRIPTION, ROLES, + TC_PROFILE_URL, + TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL, TOOL_CONSUMER_INSTANCE_DESCRIPTION, + TOOL_CONSUMER_INSTANCE_GUID, TOOL_CONSUMER_INSTANCE_NAME, + TOOL_CONSUMER_INSTANCE_URL, USER_ID, USER_IMAGE }; + + /** + * The default site type to use if a site needs to be created. Can be overriden in sakai.properties or as part of the launch. + * This contains a number of preconfigured roles, so that the IMS role vocabulary can be used. + * See BLTI-151 + */ + public static final String NEW_SITE_TYPE = "lti"; + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIUtil.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIUtil.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/BasicLTIUtil.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,888 @@ +/* + * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/basiclti/BasicLTIUtil.java $ + * $Id$ + * + * 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.lang.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(" 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 cleanupProperties( + final Map 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 cleanupProperties( + final Map rawProperties, final String[] blackList) { + final Map newProp = new HashMap( + 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 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 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 signProperties( + Map 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> params = oam.getParameters(); + + Map nextProp = new HashMap(); + // Convert to Map + for (final Map.Entry 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 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 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 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 newMap = null; + if (debug) { + // sort the properties for readability + newMap = new TreeMap(cleanProperties); + } else { + newMap = cleanProperties; + } + StringBuilder text = new StringBuilder(); + // paint form + text.append("

\n"); + text.append("
\n"); + for (Entry 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("\n"); + } + text.append("\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 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 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> theList = XMLMap.getList(tm, "/basic_lti_link/custom/parameter"); + for (Map 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 launch_info, + Map postProp, String descriptor) { + Map 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> theList = XMLMap.getList(tm, "/basic_lti_link/custom/parameter"); + for (Map 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 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 1.2.56} + * + * 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 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 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}. + * + * @param properties + * @return + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Map convertToMap(final Properties properties) { + final Map map = new HashMap(properties); + return map; + } + + /** + * Simple utility method to help with the migration from {@code Map} + * to {@code Properties}. + * + * @deprecated Should migrate to {@code Map} signatures. + * @param map + * @return + */ + public static Properties convertToProperties(final Map map) { + final Properties properties = new Properties(); + if (map != null) { + for (Entry entry : map.entrySet()) { + properties.setProperty(entry.getKey(), entry.getValue()); + } + } + return properties; + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/XMLMap.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/XMLMap.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/XMLMap.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,1332 @@ +/********************************************************************************** + * $URL: https://source.sakaiproject.org/svn/lti/trunk/lti-util/src/java/org/imsglobal/lti/XMLMap.java $ + * $Id$ + ********************************************************************************** + * + * Copyright (c) 2009 IMS GLobal Learning Consortium, Inc. + * + * 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; + +/* + * This is a little project I call mdom.org which stands for "Map-Dom" or "XML Doms in Maps" + * or "XML Documents meet Java Maps" - clearly there is homage to XPath style parsing in the + * formation of the keys in the Maps - but XPath is not used. + * + * It is my attempt to build a simple, self-contained, static class to make XML parsing + * REALLY simple in Java - the idea is to approximate the ease of taking apart and + * putting together chunks of XML in languages like Perl, Python, PHP, and Ruby. + * While the typed nature of Java makes it so there is a little extra syntax, it has + * been reduced to some pretty simple stuff with really forgiving calls. + * + * This has had several names - initially it is called XMLMap - these are pre-release + * versions ad I leave copies of this around as I move through projects and use + * the software. When I get serious, I will distribute this as org.mdom.MDom and + * distribute it as a jar with versions and all that. + * + * It is really easy to take apart XML as long as there are no repeated elements. + * The function getMap returns a map of keys and values - the keys are the path + * starting at the root node of the XML and the value is the element stored inside. + * You can get attributes as well. This example: + * + * + * B + * + * D + * + * + * + * Map xmlMap = XMLMap.getMap("BD"); + * + * Ends up with the following in the map: + * + * /a/b!x = X + * /a/b = B + * /a/c/d = D + * + * You simply use the hash get method to pull out the information. + * + * System.out.println(xmlMap.get("/a/b")) + * + * Once it is parsed into the Map - everything is quick and simple. + * + * Things are similar in creating XML - You make a TreeMap and put entries in using put() + * + * Map newMap = new TreeMap; + * newMap.put("/a/b!x","X"); + * newMap.put("/a/b", "B"); + * newMap.put("/a/b/c", "C"); + * String newXml = XMLMap.getXML(simpleMap, true); + * + * Another technique is the concept of submaps - you can extract a submap from a Map and then + * graft it directly onto some other bit of XML. + * + * Map subMap = XMLMap.selectSubMap(tm, "/a/c"); + * Map joinedMap = new TreeMap(); + * System.out.println("subMap="+subMap); + * joinedMap.put("/top/id", "1234"); + * joinedMap.put("/top/fun", subMap); // Graft the map onto this node + * String joinedXml = XMLMap.getXML(joinedMap, true); + * System.out.println("joinedXML\n"+joinedXml); + * + * Produces this XML: + * + * + * + * D + * + * 1234 + * + * + * The portion "below" /a/c was extracted and grafted onto the new XML at /top/fun. + * You can mix strings and Maps in the same map and you can have maps within maps. + * Once you switch to the Map you can even add an array of strings to an entry. + * + * Map arrayMap = new TreeMap(); + * String [] strar = { "first", "second", "third" }; + * arrayMap.put("/root/stuff", strar); + * String arrayXml = XMLMap.getXML(arrayMap, true); + * + * Produces: + * + * + * first + * second + * third + * + * + * The other major concept is how we parse XML and handle multiple items - such as in an RSS feed. + * When faced with a string of XML where you expect to get sets of items you need to parse the XML + * and get a "full" map - in this case, when the XMLMap parser sees multiple peer child nodes it + * returns a List> in the entry. This makes getting lists of Maps realy easy + * but can make the basic looking things up in the map a little harder. There are two approaches to + * this - you can either flatten the map or use the getString method to pull out all of the strings. + * The getString method does not "go into" any lists of maps - flattening does flatten through + * lists of maps, picking the first element of each list. + * + * Here is a way to look up single elements in a Full Map : + * + * Map rssFullMap = XMLMap.getFullMap(rssText); + * System.out.println("Rss Version="+XMLMap.getString(rssFullMap,"/rss!version")); + * System.out.println("Chan-title="+XMLMap.getString(rssFullMap,"/rss/channel/title")); + * + * Here is how you flatten the Map int a Map and use get to lookup + * + * Map rssStringMap = XMLMap.flattenMap(rssFullMap); + * System.out.println("Rss Version="+rssStringMap.get("/rss!version")); + * System.out.println("Chan-title="+rssStringMap.get("/rss/channel/title")); + * + * Iterating through a Full Map is pretty easy: + + * for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) { + * System.out.println("=== Item ==="); + * System.out.println(" Item-title="+XMLMap.getString(rssItem, "/title")); + * } + * + * If you have nested sets of elements - you will get back a List> that can + * also be iterated. In this example, we get a list of sites, then each site has a list of tools + * and each tool has a list of properties. The getList() method returns empty lists so + * that this code works even if the elements are not present or empty - the loops simply + * iterate zero times: + * + * Map theMap = XMLMap.getFullMap(bob); + * List> theList = XMLMap.getList(theMap, "/sites/site"); + * for ( Map siteMap : theList) { + * System.out.println("Id="+XMLMap.getString(siteMap,"/id")); + * for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) { + * System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid")); + * for ( Map property : XMLMap.getList(toolMap, "/properties/property")) { + * System.out.println("key="+XMLMap.getString(property, "/key")); + * System.out.println("val="+XMLMap.getString(property, "/val")); + * } + * } + * } + * + * You can retrieve an element within a site using getString() and then iterate through the + * sub-elements using getList(). + * + * There is a convenient variation of the getList() method which takes a String which combines + * the making of the map and retrieving of the list is you have no other use for the map: + * + * for ( Map siteMap : XMLMap.getList(bob,"/sites/site")) { + * System.out.println("Id="+XMLMap.getString(siteMap,"/id")); + * ... + * } + * + * This class has static unit tests built in and a static main that can run the sample code and produce + * output. This is to insure that the jar file is 100% Self-contained. + * + * TO DO: + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.TreeMap; +import java.util.Iterator; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.Element; +import org.w3c.dom.Text; +import org.w3c.dom.NodeList; +import org.w3c.dom.NamedNodeMap; + +/** + * a simple utility class for REST style XML + * kind of lets us act like we are in PHP. + */ +public class XMLMap { + + private static boolean DF = false; + + public static Map getMap(String str) + { + if ( str == null ) return null; + Document doc = documentFromString(str); + if ( doc == null ) return null; + return getMap(doc); + } + + public static Map getMap(Node doc) + { + Map tm = getObjectMap(doc, false); + if ( tm == null ) return null; + return flattenMap(tm); + } + + public static Map flattenMap(Map theMap) + { + if ( theMap == null ) return null; + // Reduce to the first column of elements for the simple return value + TreeMap retval = new TreeMap (); + Iterator iter = theMap.keySet().iterator(); + while( iter.hasNext() ) { + String key = iter.next(); + Object value = theMap.get(key); + // No need to handle String[] - because they will not + // be stored when doFull == false + if ( value instanceof String ) { + String svalue = (String) value; + // doDebug(d,key+" = " + value); + if ( value != null ) retval.put(key,svalue); + } + } + return retval; + } + + public static Map getFullMap(Node doc) + { + return getObjectMap(doc, true); + } + + public static Map getFullMap(String str) + { + if ( str == null ) return null; + Document doc = documentFromString(str); + if ( doc == null ) return null; + return getObjectMap(doc, true); + } + + private static Map getObjectMap(Node doc, boolean doFull) + { + if ( doc == null ) return null; + Map tm = new TreeMap(); + recurse(tm, "", doc, doFull,0); + return tm; + } + + // A Utility Method we expose so folks can reuse if they like + public static Document documentFromString(String input) + { + try{ + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + DocumentBuilder parser = factory.newDocumentBuilder(); + Document document = parser.parse(new ByteArrayInputStream(input.getBytes())); + return document; + } catch (Exception e) { + return null; + } + } + + private static String addSlash(String path) + { + if ( path == null ) return "/"; + if ( path.trim().equals("/") ) return "/"; + return path + "/"; + } + + @SuppressWarnings({ "unused", "static-access" }) + private static void recurse(Map tm, String path, Node parentNode, boolean doFull, int d) + { + if ( DF ) doDebug(d,"> recurse path="+path+" parentNode="+ nodeToString(parentNode)); + d++; + + NodeList nl = parentNode.getChildNodes(); + NamedNodeMap nm = parentNode.getAttributes(); + + // Count the TextNodes + int nodeCount = 0; + String value = null; + + // Insert the text node if we find one + if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { + Node node = nl.item(i); + if (node.getNodeType() == node.TEXT_NODE) { + value = node.getNodeValue(); + if ( value == null ) break; + if ( value.trim().length() < 1 ) break; + // doDebug(d,"Adding path="+path+" value="+node.getNodeValue()); + tm.put(path,node.getNodeValue()); + break; // Only the first one + } + } + + // Now loop through and add the attribute values + if ( nm != null ) for (int i = 0; i< nm.getLength(); i++ ) { + Node node = nm.item(i); + if (node.getNodeType() == node.ATTRIBUTE_NODE) { + String name = node.getNodeName(); + value = node.getNodeValue(); + // doDebug(d,"ATTR "+path+"("+name+") = "+node.getNodeValue()); + if ( name == null || name.trim().length() < 1 || + value == null || value.trim().length() < 1 ) continue; + + String newPath = path+"!"+name; + tm.put(newPath,value); + } + } + + // If we are not doing the full DOM - we only traverse the first child + // with the same name - so we use a set to record which nodes + // we have gone down. + if ( ! doFull ) { + // Now descend the tree to the next level deeper !! + Set done = new HashSet(); + if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { + Node node = nl.item(i); + if (node.getNodeType() == node.ELEMENT_NODE && ( ! done.contains(node.getNodeName())) ) { + if ( DF ) doDebug(d,"Going down the rabbit hole path="+path+" node="+node.getNodeName()); + recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d); + if ( DF ) doDebug(d,"Back from the rabbit hole path="+path+" node="+node.getNodeName()); + done.add(node.getNodeName()); + } + } + d--; + if ( DF ) doDebug(d,"< recurse path="+path+" parentNode="+ nodeToString(parentNode)); + return; + } + + // If we are going to do the full expansion - we need to know when + // There are more than one child with the same name. If there are more + // One child, we make list of Maps. + + Map childMap = new TreeMap(); + if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { + Node node = nl.item(i); + if (node.getNodeType() == node.ELEMENT_NODE ) { + Integer count = childMap.get(node.getNodeName()); + if ( count == null ) count = new Integer(0); + count = count + 1; + // Insert or Replace + childMap.put(node.getNodeName(), count); + } + } + + if ( childMap.size() < 1 ) return; + + // Now go through the children nodes and make a List of Maps + Iterator iter = childMap.keySet().iterator(); + Map>> nodeMap = new TreeMap>>(); + while ( iter.hasNext() ) { + String nextChild = iter.next(); + if ( nextChild == null ) continue; + Integer count = childMap.get(nextChild); + if ( count == null ) continue; + if ( count < 2 ) continue; + if ( DF ) doDebug(d,"Making a List for "+nextChild); + List> newList = new ArrayList>(); + nodeMap.put(nextChild,newList); + } + + // Now descend the tree to the next level deeper !! + if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { + Node node = nl.item(i); + if (node.getNodeType() == node.ELEMENT_NODE ) { + String childName = node.getNodeName(); + if ( childName == null ) continue; + List> mapList = nodeMap.get(childName); + if ( mapList == null ) { + if ( DF ) doDebug(d,"Going down the single rabbit hole path="+path+" node="+node.getNodeName()); + recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d); + if ( DF ) doDebug(d,"Back from the single rabbit hole path="+path+" node="+node.getNodeName()); + } else { + if ( DF ) doDebug(d,"Going down the multi rabbit hole path="+path+" node="+node.getNodeName()); + Map newMap = new TreeMap(); + recurse(newMap,"/",node,doFull,d); + if ( DF ) doDebug(d,"Back from the multi rabbit hole path="+path+" node="+node.getNodeName()+" map="+newMap); + if ( newMap.size() > 0 ) mapList.add(newMap); + } + } + } + + // Now append the multi-node maps to our current map + Iterator iter2 = nodeMap.keySet().iterator(); + while ( iter2.hasNext() ) { + String nextChild = iter2.next(); + if ( nextChild == null ) continue; + List> newList = nodeMap.get(nextChild); + if ( newList == null ) continue; + if ( newList.size() < 1 ) continue; + if ( DF ) doDebug(d,"Adding sub-map name="+nextChild+" list="+newList); + tm.put(path+"/"+nextChild, newList); + } + d--; + if ( DF ) doDebug(d,"< recurse path="+path+" parentNode="+ nodeToString(parentNode)); + } + + public static String getXML(Map tm) + { + Document document = getXMLDom(tm); + if ( document == null ) return null; + return documentToString(document, false); + } + + public static String getXMLFragment(Map tm, boolean pretty) + { + String retval = getXML(tm, pretty); + if ( retval.startsWith(" 0 ) retval = retval.substring(pos); + } + return retval; + } + + public static String getXML(Map tm, boolean pretty) + { + Document document = getXMLDom(tm); + if ( document == null ) return null; + String retval = documentToString(document, pretty); + // Since the built in transform seems unable to indent + // We patch it ourselves to keep from being ugly + if ( pretty ) { + retval = prettyPostProcess(retval); + } + return retval; + } + + // This process a pretty print from an input string - + // It does it the hard way - using the methods in this class. + // It may not be the ideal way to pretty print a XML String + // but it is our way and we want to be D.R.Y. here... + // As such you may see some error messages from + // the XMLMap class in the pretty printing. + public static String prettyPrint(String input) + { + Map theMap = XMLMap.getFullMap(input); + return XMLMap.getXML(theMap, true); + } + + private static String prettyPostProcess(String inString) + { + StringBuffer sb = new StringBuffer(); + int depth = 0; + boolean newLine = false; + for (int i=0; i tm) + { + if ( tm == null ) return null; + Document document = null; + + try{ + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + DocumentBuilder parser = factory.newDocumentBuilder(); + document = parser.newDocument(); + } catch (Exception e) { + return null; + } + + iterateMap(document, document.getDocumentElement(), tm, 0); + return document; + } + + /* Remember that the map is a linear list of entries + /a/b B1 + /a/c Map + /x X1 + /y Y1 + /y!r R1 + /a/c!q Q1 + /a/d D1 + + + B1 + + X1 + Y1 + + D1 + + */ + private static void iterateMap(Document document, Node parentNode, Map tm, int d) + { + if ( DF ) doDebug(d,"> IterateMap parentNode= "+ nodeToString(parentNode)); + d++; + Iterator iter = tm.keySet().iterator(); + while( iter.hasNext() ) { + String key = (String) iter.next(); + if ( key == null ) continue; + if ( ! key.startsWith("/") ) continue; // Skip + Object obj = tm.get(key); + if ( obj == null ) { + continue; + } if ( obj instanceof String ) { + storeInDom(document, parentNode, key, (String) obj, 0, d); + } else if ( obj instanceof String [] ) { + String [] strArray = (String []) obj; + if ( DF ) doDebug(d,"Looping through an array of length "+strArray.length); + for(int i=0; i < strArray.length; i++ ) { + storeInDom(document, parentNode, key, strArray[i], i, d); + } + } else if ( obj instanceof Map ) { + Map subMap = (Map) obj; + Node startNode = getNodeAtPath(document, parentNode, key, 0, d); + if ( DF ) doDebug(d,"descending into Map path="+key+" startNode="+ nodeToString(startNode)); + iterateMap(document, startNode, subMap, d); + if ( DF ) doDebug(d,"back from descent Map path="+key+" startNode="+ nodeToString(startNode)); + } else if ( obj instanceof List ) { + List lst = (List) obj; + if ( DF ) doDebug(d,"Have a list that is this long "+lst.size()); + Iterator listIter = lst.iterator(); + int newPos = 0; + while ( listIter.hasNext() ) { + Object listObj = listIter.next(); + if ( DF ) doDebug(d,"Processing List element@"+newPos+" "+listObj.getClass().getName()); + if ( listObj instanceof String ) { + storeInDom(document, parentNode, key, (String) listObj, newPos, d); + newPos++; + } if ( listObj instanceof Map ) { + Map subMap = (Map) listObj; + if ( DF ) doDebug(d,"Retrieving key from List-Map path="+key+"@"+newPos); + Node startNode = getNodeAtPath(document, parentNode, key, newPos, d); + if ( DF ) doDebug(d,"descending into List-Map path="+key+"@"+newPos+" startNode="+ nodeToString(startNode)); + iterateMap(document, startNode, subMap, d); + if ( DF ) doDebug(d,"back from descent List-Map path="+key+"@"+newPos+" startNode="+ nodeToString(startNode)); + newPos++; + } else { + System.out.println("XMLMap Encountered an object of type "+obj.getClass().getName()+" in a List which should contain only Map objects"); + } + } + } else { + if ( DF ) doDebug(d,"Found a "+obj.getClass().getName()+" do not know how to iterate."); + } + } + d--; + if ( DF ) doDebug(d,"< IterateMap parentNode = "+ nodeToString(parentNode)); + } + + private static void storeInDom(Document document, Node parentNode, String key, String value, int nodePos, int d) + { + if ( DF ) doDebug(d,"> storeInDom"+key+"@"+ nodePos + " = " + value + " parent="+ nodeToString(parentNode)); + d++; + if ( document == null || key == null || value == null ) return; + if ( parentNode == null ) parentNode = document; + if ( DF ) doDebug(d,"parentNode I="+ nodeToString(parentNode)); + + String [] newPath = key.split("/"); + if ( DF ) doDebug(d,"newPath = "+outStringArray(newPath)); + String nodeAttr = null; + for ( int i=1; i< newPath.length; i++ ) + { + String nodeName = newPath[i]; + if ( i == newPath.length-1 ) { + // doDebug(d,"Splitting !="+nodeName); + // check to see if we have a nodename=attributename + String [] nodeSplit = nodeName.split("!"); + if ( nodeSplit.length > 1 ) { + nodeName = nodeSplit[0]; + nodeAttr = nodeSplit[1]; + // doDebug(d,"new nodeName="+nodeName+" nodeAttr="+nodeAttr); + } + parentNode = getOrAddChildNode(document, parentNode, nodeName, nodePos, d); + } else { + parentNode = getOrAddChildNode(document, parentNode, nodeName, 0, d); + } + } + // doDebug(d,"parentNode after="+ nodeToString(parentNode)); + + if ( nodeAttr != null ) + { + if ( value!= null && parentNode instanceof Element ) + { + Element element = (Element) parentNode; + // doDebug(d,"Adding an attribute "+nodeAttr); + element.setAttribute(nodeAttr,value); + } + } + else if ( value != null ) + { + Text newNode = document.createTextNode(value); + parentNode.appendChild(newNode); + } + d--; + // doDebug(d,"xml="+documentToString(document,false)); + // doDebug(d,"< storeInDom"+key+" = " + value); + } + + // Note - sadly this does not "return" the attr name - hence we need + // to replicate this code in storeInDom :( + private static Node getNodeAtPath(Document document, Node parentNode, String path, int nodePos, int d) + { + if ( parentNode == null ) parentNode = document; + if ( DF ) doDebug(d,"> getNodeAtPath path@" + nodePos + "="+path+" parentNode="+ nodeToString(parentNode)); + d++; + + String [] newPath = path.split("/"); + // doDebug(d,"newPath = "+outStringArray(newPath)); + for ( int i=1; i< newPath.length; i++ ) + { + String nodeName = newPath[i]; + if ( i == newPath.length-1 ) { + // doDebug(d,"Splitting !="+nodeName); + // check to see if we have a nodename=attributename + String [] nodeSplit = nodeName.split("!"); + if ( nodeSplit.length > 1 ) { + nodeName = nodeSplit[0]; + // doDebug(d,"new nodeName="+nodeName); + } + parentNode = getOrAddChildNode(document, parentNode, nodeName, nodePos, d); + } else { + parentNode = getOrAddChildNode(document, parentNode, nodeName, 0, d); + } + } + d--; + if ( DF ) doDebug(d,"< getNodeAtPath returning="+ nodeToString(parentNode)); + return parentNode; + } + + @SuppressWarnings("static-access") + private static Node getOrAddChildNode(Document doc, Node parentNode, String nodeName,int whichNode, int d) + { + if ( DF ) doDebug(d,"> getOrAddChildNode name="+nodeName+"@"+whichNode+" parentNode="+ nodeToString(parentNode)); + d++; + if ( nodeName == null || parentNode == null) return null; + + // Check to see if we are somewhere in an index + int begpos = nodeName.indexOf('['); + int endpos = nodeName.indexOf(']'); + // doDebug(d,"Looking for bracket ipos="+begpos+" endpos="+endpos); + if ( begpos > 0 && endpos > begpos && endpos < nodeName.length() ) { + String indStr = nodeName.substring(begpos+1,endpos); + if ( DF ) doDebug(d,"Index String = "+ indStr); + nodeName = nodeName.substring(0,begpos); + if ( DF ) doDebug(d,"New Nodename="+nodeName); + Integer iVal = new Integer(indStr); + if ( DF ) doDebug(d,"Integer = "+iVal); + whichNode = iVal; + } + + NodeList nl = parentNode.getChildNodes(); + int foundNodes = -1; + if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { + Node node = nl.item(i); + // doDebug(d,"length= " +nl.getLength()+ " i="+i+" NT="+node.getNodeType()); + // doDebug(d,"searching nn="+nodeName+" nc="+node.getNodeName()); + if (node.getNodeType() == node.ELEMENT_NODE) { + if ( nodeName.equals(node.getNodeName()) ) { + foundNodes++; + d--; + if ( DF ) doDebug(d,"< getOrAddChildNode found name="+ nodeToString(node)); + if ( DF ) doDebug(d,"foundNodes = "+foundNodes+" looking for node="+whichNode); + if ( foundNodes >= whichNode ) return node; + } + } + } + + Element newNode = null; + while ( foundNodes < whichNode ) { + foundNodes++; + if ( DF ) doDebug(d,"Adding node at position " + foundNodes + " moving toward " + whichNode); + if ( nodeName == null ) continue; + newNode = doc.createElement(nodeName); + if ( DF ) doDebug(d,"Adding "+nodeName+" at "+ nodeToString(parentNode)+" in "+doc); + parentNode.appendChild(newNode); + if ( DF ) doDebug(d,"xml="+documentToString(doc,false)); + if ( DF ) doDebug(d,"getOrAddChildNode added newnode="+ nodeToString(newNode)); + } + d--; + if ( DF ) doDebug(d,"< getOrAddChildNode added newnode="+ nodeToString(newNode)); + return newNode; + } + + public static String outStringArray(String [] arr) + { + if ( arr == null ) return null; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < arr.length; i++ ) { + if ( i > 0 ) sb.append(" "); + sb.append("["+i+"]="); + sb.append(arr[i]); + } + return sb.toString(); + } + + public static String nodeToString(Node node) + { + if ( node == null ) return null; + String retval = node.getNodeName(); + while ( (node = node.getParentNode()) != null ) { + retval = node.getNodeName() + "/" + retval; + } + return "/" + retval; + } + + // Optionally setup indenting to "pretty print" + // Note - this is not very pretty at least in my testing - but it is better + // than all string together + public static String documentToString(Document document, boolean pretty) + { + return nodeToString(document, pretty); + } + + // Optionally setup indenting to "pretty print" + // Note - this is not very pretty at least in my testing - but it is better + // than all string together + public static String nodeToString(Node node, boolean pretty) + { + try { + javax.xml.transform.Transformer tf = + javax.xml.transform.TransformerFactory.newInstance().newTransformer(); + if ( pretty ) { + tf.setOutputProperty(OutputKeys.INDENT, "yes"); + tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + } + ByteArrayOutputStream baStream = new ByteArrayOutputStream(); + tf.transform (new javax.xml.transform.dom.DOMSource (node), + new javax.xml.transform.stream.StreamResult (baStream)); + return baStream.toString(); + } catch (javax.xml.transform.TransformerException e) { + return null; + } + } + + // Someone better at Generics can yell at me as to how this should have been + // done to use the same code for either objects or strings. Sorry. + public static Map selectSubMap(Map sm, String selection) + { + if ( sm == null ) return null; + selection = selection.trim(); + if ( badSelection(selection) ) return null; + Map retval = new TreeMap(); + selectSubMap(sm, retval, null, null, selection); + return retval; + } + + public static Map selectFullSubMap(Map om, String selection) + { + if ( om == null ) return null; + selection = selection.trim(); + if ( badSelection(selection) ) return null; + Map retval = new TreeMap(); + selectSubMap(null, null, om, retval, selection); + return retval; + } + + private static boolean badSelection(String selection) + { + if ( selection == null ) return true; + if ( selection.equals("/") ) return true; + if ( selection.length() < 2 ) return true; + if ( ! selection.startsWith("/") ) return true; + return false; + } + + private static void selectSubMap(Map sm, Map sret, + Map om, Map oret, String selection) + { + Iterator iter = null; + if ( sm != null ) { + iter = sm.keySet().iterator(); + } else { + iter = om.keySet().iterator(); + } + + while( iter.hasNext() ) { + String key = iter.next(); + boolean match = false; + String newKey = null; + if ( key.equals(selection) ) { + match = true; + newKey = "/"; + } else if ( selection.endsWith("/") && key.startsWith(selection)) { + match = true; + newKey = key.substring(selection.length()-1); + } else if ( key.startsWith(selection+"/") ) { + match = true; + newKey = key.substring(selection.length()); + } else if ( key.startsWith(selection+"!") ) { + match = true; + newKey = "/" + key.substring(selection.length()); + } + if ( ! match ) continue; + // doDebug(d,"newKey = "+newKey); + if ( sm != null ) { + String value = sm.get(key); + if ( value == null ) continue; + sret.put(newKey,value); + // doDebug(d,newKey+" = " + value); + } else { + Object value = om.get(key); + if ( value == null ) continue; + oret.put(newKey,value); + // doDebug(d,newKey+" = " + value); + } + } + } + + /* + * Remove a submap. Depending if the string ends ina slash - there are + * two behaviors. + * /x/y/ All of the children are removed but the node is left intact + * /x/y All of the children are removed and the node itself and + * any attributes are removed as well (typical case) + */ + public static void removeSubMap(Map tm, String selection) + { + if ( tm == null ) return; + selection = selection.trim(); + if ( badSelection(selection) ) return; + + // If the selection does not end with /, generate the + // Attribute and children selections + selection = selection.trim(); + String childSel = selection; + String attrSel = selection; + if ( ! selection.endsWith("/") ) { + childSel = selection + "/"; + attrSel = selection + "!"; + } + + // Track what we will delete until loop is done + Set delSet = new HashSet(); + + Iterator iter = tm.keySet().iterator(); + while( iter.hasNext() ) { + Object key = iter.next(); + if ( ! (key instanceof String) ) continue; + String strKey = (String) key; + if ( strKey.equals(selection) || strKey.startsWith(childSel) || strKey.startsWith(attrSel)) { + delSet.add(strKey); + // System.out.println("Deleting key="+key); + } + } + + // Actually remove... + Iterator setIter = delSet.iterator(); + while( setIter.hasNext() ) { + String key = setIter.next(); + tm.remove(key); + } + } + + private static void doDebug(int d, String str) { + if ( ! DF ) return; + for(int i=0; i theMap, String key) + { + if ( theMap == null ) return null; + Object obj = theMap.get(key); + if ( obj == null ) return null; + if ( obj instanceof String ) return (String) obj; + return null; + } + + /* This goes to a set of nodes that are intended to be multiple nodes and returns a list whether there + * are one or many nodes. + * + * + * + * p1keyp1val + * + * + * p2akeyp2aval + * p2bkeyp2bval + * + * + * + * List> p1s = XMLMap.getList(mnop,"/abc/p1s/p1"); + * List> p2s = XMLMap.getList(mnop,"/abc/p2s/p2"); + * + * Always return a list even if it is just an empty list so this code works: + * + * for ( Map siteMap : XMLMap.getList(mnop,"/sites/site")) { + * System.out.println("Site="+siteMap); + * } + */ + @SuppressWarnings("unchecked") + public static List> getList(Map theMap,String key) + { + ArrayList> al = new ArrayList>(); + if ( theMap == null || key == null ) return al; + + // If this is a nice little list of maps - we are golden - send the list back + Object obj = theMap.get(key); + if ( obj instanceof List ) return (List>) obj; + + // We may have a single String value - we may have a single terminal value + // perhaps with some attributes + // + // + // 1300 + // + // + + // See if there is one sub map there... + Map oneMap = selectFullSubMap(theMap, key); + // System.out.println("One submap = "+oneMap); + if ( oneMap == null ) return al; + + // If the map is not empty - return am empty list + // rather than a one element list with an empty map + if ( oneMap.isEmpty() ) return al; + + // Make a list of one submap... + al.add(oneMap); + return al; + } + + // Note that getList with the first parameter to getList is a String, it does a + // getMap and then a getList with that Map - this allows the following + // rather dense code: + + // for ( Map siteMap : XMLMap.getList(xmlString,"/sites/site")) { + + // The long form of this looks as follows: + + // Map theMap = XMLMap.getFullMap(xmlString); + // List> theList = XMLMap.getList(theMap, "/sites/site"); + // for ( Map siteMap : theList) { + + // The short form should only be used if this is the only time you will parse + // to get a FullMap - otherwise - get the FullMap once and pull out the different bits + // from the map without reparsing the xmlString. + + public static List> getList(String xmlInput,String key) + { + Map tmpMap = XMLMap.getFullMap(xmlInput); + return XMLMap.getList(tmpMap,key); + } + + /* + * Unit Tests - Keep these public in case folks want to call them when they are + * only in possession of a jar file - makes the jar file a bearer instrument at + * the cost of some extra space. + */ + + public static boolean unitTest(String xmlString, boolean doDebug) + { + + if ( xmlString == null ) return false; + DF = doDebug; + + // If Debug is turned on - let the chips fly, exceptions and + // All... + if ( doDebug ) { + DF = true; + String pretty1 = XMLMap.prettyPrint(xmlString); + String pretty2 = XMLMap.prettyPrint(pretty1); + if ( pretty1.equals(pretty2) ) return true; + System.out.println("XMLMap - unit test failed"); + return false; + } + + // For Debug off - we first try it silently and in a try/catch block + DF = false; + try { + String pretty1 = XMLMap.prettyPrint(xmlString); + String pretty2 = XMLMap.prettyPrint(pretty1); + if ( pretty1.equals(pretty2) ) return true; + } + catch (Throwable t) { + // We will re-do below so folks see the trace back - + // in the context of debug + } + + // If we failed - re-do it with verbose mode on + System.out.println("XMLMap - unit test failed"); + System.out.println(xmlString); + DF = true; + String pretty1 = XMLMap.prettyPrint(xmlString); + System.out.println("Pretty Print Version pass 1\n"+pretty1); + String pretty2 = XMLMap.prettyPrint(pretty1); + System.out.println("Pretty Print Version pass 2\n"+pretty2); + DF = false; // Always reset class-wide variable + return false; + } + + // Some Unit Test and sample Strings + private static final String simpleText = "BD"; + private static final String sitesText = " sue fred Title sakai.web.content p1key p1val p2key p2val sakai-wiki wikikey sakai-blog "; + private static final String rssText = "Dr-Chuck's MediaTelevision Shows and other mediahttp://www.dr-chuck.com/media.phpTrack Days with John Merlin WilliamsThis film is about racing street Motorcyles.http://www.dr-chuck.comMotocross RacingDr. Chuck comes in second to last and is covered with mud.http://www.dr-chuck.com/"; + + public static boolean allUnitTests() { + if ( !unitTest(simpleText, false) ) return false; + if ( !unitTest(sitesText, false) ) return false; + if ( !unitTest(rssText, false) ) return false; + return true; + } + + public static void main(String[] args) { + System.out.println("Running XMLMap (www.mdom.org) unit tests.."); + if ( !allUnitTests() ) return; + System.out.println("Unit tests passed..."); + runSamples(); + } + + public static void runSamples() { + System.out.println("Running XMLMap (www.mdom.org) Samples..."); + DF = false; + + // Test the parsing of a Basic string Map + Map tm = XMLMap.getMap(simpleText); + // System.out.println("tm="+tm); + + // Test the production of a basic map + Map simpleMap = new TreeMap(); + simpleMap.put("/a/b!x", "X"); + simpleMap.put("/a/b", "B"); + simpleMap.put("/a/c/d", "D"); + System.out.println("simpleMap\n"+simpleMap); + String simpleXml = XMLMap.getXML(simpleMap, true); + System.out.println("simpleXml\n"+simpleXml); + unitTest(simpleXml,false); + + // Do a select of a subMap + Map subMap = XMLMap.selectSubMap(tm, "/a/c"); + Map joinedMap = new TreeMap(); + System.out.println("subMap="+subMap); + joinedMap.put("/top/id", "1234"); + joinedMap.put("/top/fun", subMap); // Graft the map onto this node + System.out.println("joinedMap\n"+joinedMap); + String joinedXml = XMLMap.getXML(joinedMap, true); + System.out.println("joinedXML\n"+joinedXml); + unitTest(joinedXml,false); + + // Do an Array + Map arrayMap = new TreeMap(); + String [] arrayStr = { "first", "second", "third" }; + arrayMap.put("/root/stuff", arrayStr); + System.out.println("arrayMap\n"+arrayMap); + String arrayXml = XMLMap.getXML(arrayMap, true); + System.out.println("arrayXml\n"+arrayXml); + unitTest(arrayXml,false); + + // Make a Map that is a combination of Maps, String, and Arrays + Map newMap = new TreeMap(); + + newMap.put("/Root/milton","Root-milton"); + newMap.put("/Root/joe","Root-joe"); + + Map m2 = new TreeMap(); + m2.put("/fred/a","fred-a"); + m2.put("/fred/b","fred-b"); + newMap.put("/Root/freds", m2); + + // Add a list of maps + // + // + // + // key-0 + // val-0 + // + // + // key-1 + // val-1 + // + // + // + + List> lm = new ArrayList>(); + Map m3 = null; + m3 = new TreeMap(); + m3.put("/key","key-0"); + m3.put("/val","val-0"); + lm.add(m3); + + m3 = new TreeMap(); + m3.put("/key","key-1"); + m3.put("/val","val-1"); + lm.add(m3); + + newMap.put("/Root/maps/map", lm); + + // Add an array of Strings + // + // first + // second + // third + // + + String [] strar = { "first", "second", "third" }; + newMap.put("/Root/array", strar); + + // Add a list of Maps - this is a bit of a weird application - mostly as a + // completeness test to insure lists of maps and arrays are equivalent. Also + // since the getFullMap returns maps, not Arrays of strings, this is necessary + // to insure symmetry - i.e. we can take a map structure we produce and + // regenerate the XML. Most users will not use this form in construction. + // + // + // item-1 + // item-2 + // + + List> l1 = new ArrayList>(); + Map m4 = new TreeMap(); + m4.put("/", "item-1"); + l1.add(m4); + Map m5 = new TreeMap(); + m5.put("/", "item-2"); + l1.add(m5); + newMap.put("/Root/item", l1); + + // Put in using the XMLMap bracket Syntax - not a particularly good + // Way to represent multiple items - it is just here for completeness. + newMap.put("/Root/anns/ann[0]","Root-ann[0]"); + newMap.put("/Root/anns/ann[1]","Root-ann[1]"); + newMap.put("/Root/bobs/bob[0]/key","Root-bobs-bob[0]-key"); + newMap.put("/Root/bobs/bob[0]/val","Root-bobs-bob[0]-val"); + newMap.put("/Root/bobs/bob[1]/key","Root-bobs-bob[1]-key"); + newMap.put("/Root/bobs/bob[1]/val","Root-bobs-bob[1]-val"); + + // This is not allowed because maps cannot have duplicates + /* + Map m6 = new TreeMap(); + m5.put("/two", "two-1"); + m5.put("/two", "two-2"); + newMap.put("/Root", m6); + */ + + // Take the Map - turn it into XML and then parse the returned + // XML into a second map - take the second map and produce more XML + // If all goes well, the two generated blobs of XML should be the + // same. If anything goes wrong - we re-do it with lots of debug + String complexXml = null; + boolean success = false; + DF = false; + try { + complexXml = XMLMap.getXML(newMap, true); + success = true; + } catch(Exception e) { + success = false; + } + + // If we fail - do it again with deep levels of verbosity + if ( success ) { + unitTest(complexXml,false); + } else { + DF = true; + System.out.println("\n MISMATCH AND/OR SOME ERROR HAS OCCURED - REDO in VERBODE MODE"); + System.out.println("Starting out newMap="+newMap); + complexXml = XMLMap.getXML(newMap, true); + unitTest(complexXml,false); + DF = false; + } + + // A different example - iterating through nested sets - demonstrating the short form + // of getSites() with the first parameter a string -the commented code below is the long form. + + // Map theMap = XMLMap.getFullMap(sitesText); + // List> theList = XMLMap.getList(theMap, "/sites/site"); + // for ( Map siteMap : theList) { + + // The short form using convenience method if you don't need the map for anything else + System.out.println("\nParsing Sites Structure"); + for ( Map siteMap : XMLMap.getList(sitesText,"/sites/site")) { + System.out.println("Site="+siteMap); + System.out.println("Id="+XMLMap.getString(siteMap,"/id")); + for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) { + System.out.println("Tool="+toolMap); + System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid")); + for ( Map property : XMLMap.getList(toolMap, "/properties/property")) { + System.out.println("key="+XMLMap.getString(property, "/key")); + System.out.println("val="+XMLMap.getString(property, "/val")); + } + } + } + + // Lets parse some RSS as a final kind of easy but quite practical test + DF = false; + System.out.println("\nParsing RSS Feed"); + // System.out.println(XMLMap.prettyPrint(rssText)); + Map rssFullMap = XMLMap.getFullMap(rssText); + System.out.println("RSS Full Map\n"+rssFullMap); + System.out.println("Rss Version="+XMLMap.getString(rssFullMap,"/rss!version")); + System.out.println("Chan-desc="+XMLMap.getString(rssFullMap,"/rss/channel/description")); + System.out.println("Chan-title="+XMLMap.getString(rssFullMap,"/rss/channel/title")); + + Map rssStringMap = XMLMap.flattenMap(rssFullMap); + System.out.println("RSS Flat String Only Map\n"+rssStringMap); + System.out.println("Rss Version="+rssStringMap.get("/rss!version")); + System.out.println("Chan-desc="+rssStringMap.get("/rss/channel/description")); + System.out.println("Chan-title="+rssStringMap.get("/rss/channel/title")); + + for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) { + System.out.println("=== Item ==="); + System.out.println(" Item-title="+XMLMap.getString(rssItem, "/title")); + System.out.println(" Item-description="+XMLMap.getString(rssItem, "/description")); + System.out.println(" Item-link="+XMLMap.getString(rssItem, "/link")); + } + } +} + +/* Sample output from test run with lines wrapped a bit: + +Running XMLMap (www.mdom.org) unit tests.. +Unit tests passed... +Running XMLMap (www.mdom.org) Samples... +tm={/a/b=B, /a/b!x=X, /a/c/d=D} +simpleMap +{/a/b=B, /a/b!x=X, /a/c/d=D} +simpleXml + + + B + + D + + + +subMap={/d=D} +joinedMap +{/top/fun={/d=D}, /top/id=1234} +joinedXML + + + + D + + 1234 + + +arrayMap +{/root/stuff=[Ljava.lang.String;@6f50a8} +arrayXml + + + first + second + third + + + +Parsing Sites Structure +Site={/id=sue} +Id=sue +Site={/id=fred, /title=Title, /tools/tool=[{/properties/property=[{/key=p1key, /val=p1val}, + {/key=p2key, /val=p2val}], /toolid=sakai.web.content}, {/properties/property/key=wikikey, + /toolid=sakai-wiki}, {/toolid=sakai-blog}]} + +Id=fred +Tool={/properties/property=[{/key=p1key, /val=p1val}, {/key=p2key, /val=p2val}], /toolid=sakai.web.content} +ToolId=sakai.web.content +key=p1key +val=p1val +key=p2key +val=p2val +Tool={/properties/property/key=wikikey, /toolid=sakai-wiki} +ToolId=sakai-wiki +key=wikikey +val=null +Tool={/toolid=sakai-blog} +ToolId=sakai-blog + +Parsing RSS Feed +RSS Full Map +{/rss!version=2.0, /rss/channel/description=Television Shows and other media, + /rss/channel/item=[{/description=This film is about racing street Motorcyles., + /link=http://www.dr-chuck.com, /title=Track Days with John Merlin Williams}, + {/description=Dr. Chuck comes in second to last and is covered with mud., + /link=http://www.dr-chuck.com/, /title=Motocross Racing}], + /rss/channel/link=http://www.dr-chuck.com/media.php, /rss/channel/title=Dr-Chuck's Media} + +Rss Version=2.0 +Chan-desc=Television Shows and other media +Chan-title=Dr-Chuck's Media +RSS Flat String Only Map +{/rss!version=2.0, /rss/channel/description=Television Shows and other media, + /rss/channel/link=http://www.dr-chuck.com/media.php, /rss/channel/title=Dr-Chuck's Media} + +Rss Version=2.0 +Chan-desc=Television Shows and other media +Chan-title=Dr-Chuck's Media +=== Item === + Item-title=Track Days with John Merlin Williams + Item-description=This film is about racing street Motorcyles. + Item-link=http://www.dr-chuck.com +=== Item === + Item-title=Motocross Racing + Item-description=Dr. Chuck comes in second to last and is covered with mud. + Item-link=http://www.dr-chuck.com/ + + */ + Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiError.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiError.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiError.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,22 @@ +package org.imsglobal.lti.launch; + +/** + * Created by paul on 5/28/14. + */ +public class LtiError { + + private final String label; + + public static final LtiError INVALID_HASH = new LtiError("invalid_hash"); + public static final LtiError TIMESTAMP_MISMATCH = new LtiError("timestamp_mismatch"); + public static final LtiError BAD_REQUEST = new LtiError("bad_request"); + + private LtiError(String label){ + this.label = label; + } + + public String toString(){ + return label; + } + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiLaunch.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiLaunch.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiLaunch.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,96 @@ +package org.imsglobal.lti.launch; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * Created by paul on 5/28/14. + */ +public class LtiLaunch { + + private LtiUser user; + + private String version; + private String messageType; + private String resourceLinkId; + + private String contextId; + private String launchPresentationReturnUrl; + private String toolConsumerInstanceGuid; + + public LtiLaunch(HttpServletRequest request) { + this.user = new LtiUser(request); + this.version = request.getParameter("lti_version"); + this.messageType = request.getParameter("lti_message_type"); + this.resourceLinkId = request.getParameter("resource_link_id"); + this.contextId = request.getParameter("context_id"); + this.launchPresentationReturnUrl = request.getParameter("launch_presentation_return_url"); + this.toolConsumerInstanceGuid = request.getParameter("tool_consumer_instance_guid"); + } + + public LtiLaunch(Map parameters) { + this.user = new LtiUser(parameters); + this.version = parameters.get("lti_version"); + this.messageType = parameters.get("lti_message_type"); + this.resourceLinkId = parameters.get("resource_link_id"); + this.contextId = parameters.get("context_id"); + this.launchPresentationReturnUrl = parameters.get("launch_presentation_return_url"); + this.toolConsumerInstanceGuid = parameters.get("tool_consumer_instance_guid"); + } + + public LtiUser getUser() { + return user; + } + + public void setUser(LtiUser user) { + this.user = user; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getMessageType() { + return messageType; + } + + public void setMessageType(String messageType) { + this.messageType = messageType; + } + + public String getResourceLinkId() { + return resourceLinkId; + } + + public void setResourceLinkId(String resourceLinkId) { + this.resourceLinkId = resourceLinkId; + } + + public String getContextId() { + return contextId; + } + + public void setContextId(String contextId) { + this.contextId = contextId; + } + + public String getLaunchPresentationReturnUrl() { + return launchPresentationReturnUrl; + } + + public void setLaunchPresentationReturnUrl(String launchPresentationReturnUrl) { + this.launchPresentationReturnUrl = launchPresentationReturnUrl; + } + + public String getToolConsumerInstanceGuid() { + return toolConsumerInstanceGuid; + } + + public void setToolConsumerInstanceGuid(String toolConsumerInstanceGuid) { + this.toolConsumerInstanceGuid = toolConsumerInstanceGuid; + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthSigner.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthSigner.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthSigner.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,97 @@ +package org.imsglobal.lti.launch; + +import net.oauth.OAuthAccessor; +import net.oauth.OAuthConsumer; +import net.oauth.OAuthException; +import net.oauth.OAuthMessage; +import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; +import oauth.signpost.http.HttpParameters; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpRequest; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +/** + * This class signs LTI requests according to the Oauth 1.0 spec + * @author Paul Gray + * @since 1.1 + */ +public class LtiOauthSigner implements LtiSigner { + + private MessageDigest md; + + public LtiOauthSigner() { + try{ + md = MessageDigest.getInstance("SHA1"); + } catch(NoSuchAlgorithmException e) { + throw new RuntimeException("Could not construct new instance of LtiOauthSigner", e); + } + } + + public LtiOauthSigner(MessageDigest md) { + this.md = md; + } + + @Override + public HttpRequest sign(HttpRequest request, String key, String secret) throws LtiSigningException { + CommonsHttpOAuthConsumer signer = new CommonsHttpOAuthConsumer(key, secret); + try { + String body = getRequestBody(request); + String bodyHash = new String(Base64.encodeBase64(md.digest(body.getBytes()))); + + HttpParameters params = new HttpParameters(); + params.put("oauth_body_hash", URLEncoder.encode(bodyHash, "UTF-8")); + signer.setAdditionalParameters(params); + + signer.sign(request); + } catch (OAuthMessageSignerException|OAuthExpectationFailedException|OAuthCommunicationException|IOException e) { + throw new LtiSigningException("Exception encountered while singing Lti request...", e); + } + return request; + } + + @Override + public Map signParameters(Map parameters, String key, String secret, String url, String method) throws LtiSigningException { + OAuthMessage oam = new OAuthMessage(method, url, parameters.entrySet()); + OAuthConsumer cons = new OAuthConsumer(null, key, secret, null); + OAuthAccessor acc = new OAuthAccessor(cons); + try { + oam.addRequiredParameters(acc); + + Map signedParameters = new HashMap<>(); + for(Map.Entry param : oam.getParameters()){ + signedParameters.put(param.getKey(), param.getValue()); + } + return signedParameters; + } catch (OAuthException |IOException |URISyntaxException e) { + throw new LtiSigningException("Error signing LTI request.", e); + } + } + + private String getRequestBody(HttpRequest req) throws IOException { + if(req instanceof HttpEntityEnclosingRequest){ + HttpEntity body = ((HttpEntityEnclosingRequest) req).getEntity(); + if(body == null) { + return ""; + } else { + return IOUtils.toString(body.getContent()); + } + } else { + // requests with no entity have an empty string as the body + return ""; + } + } + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthVerifier.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthVerifier.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiOauthVerifier.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,75 @@ +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 verifies 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 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)); + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigner.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigner.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigner.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,47 @@ +package org.imsglobal.lti.launch; + +import org.apache.http.HttpRequest; + +import java.util.Map; + +/** + * This interface contains methods that sign HttpRequests + * and generic request parameters according to the LTI + * specification. + * @author Paul Gray + * @since 1.1 +*/ +public interface LtiSigner { + + /** + * This method will return a signed HttpRequest object. + * Once returned, adding new parameters or changing the + * body will invalidate the signature. This method should + * be used for server to server connection requests. + * For example, posting an LTI Outcome back to the LTI + * Consumer. + * @param request the HttpRequest that will be signed + * @param key the key that will be added to the request. + * @param secret the secret to be used + * @return a signed HttpRequest object + * @throws LtiSigningException + */ + public HttpRequest sign(HttpRequest request, String key, String secret) throws LtiSigningException; + + /** + * This method will return a list of signed parameters. + * Once returned, adding new parameters or changing the + * body will invalidate the signature. This method will + * overwrite reserved parameters from the underlying + * specification. For example, if you are using the Oauth + * implementation, oauth_signature will be removed + * & replaced with the generated signature from the properties. + * @param parameters the parameters that will be signed. mapped by key & value + * @param key the key that will be added to the request. + * @param secret the secret to be sign the parameters with + * @return a map of signed parameters (including the signature) + * @throws LtiSigningException + */ + public Map signParameters(Map parameters, String key, String secret, String url, String method) throws LtiSigningException; + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigningException.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigningException.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiSigningException.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,12 @@ +package org.imsglobal.lti.launch; + +/** + * This exception is thrown to indicate that there was an error when signing an LTI request. + * + * Created by pgray on 8/23/14. + */ +public class LtiSigningException extends Exception { + public LtiSigningException(String message, Exception exception) { + super(message, exception); + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiUser.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiUser.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiUser.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,52 @@ +package org.imsglobal.lti.launch; + +import javax.servlet.http.HttpServletRequest; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Created by paul on 5/28/14. + */ +public class LtiUser { + + private String id; + private List roles; + + public LtiUser(HttpServletRequest request) { + this.id = request.getParameter("user_id"); + this.roles = new LinkedList<>(); + if(request.getParameter("roles") != null) { + for (String role : request.getParameter("roles").split(",")) { + this.roles.add(role.trim()); + } + } + } + + public LtiUser(Map parameters) { + this.id = parameters.get("user_id"); + this.roles = new LinkedList<>(); + if(parameters.get("roles") != null) { + for (String role : parameters.get("roles").split(",")) { + this.roles.add(role.trim()); + } + } + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationException.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationException.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationException.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,10 @@ +package org.imsglobal.lti.launch; + +/** + * Created by pgray on 8/28/14. + */ +public class LtiVerificationException extends Exception { + public LtiVerificationException(String message, Exception exception) { + super(message, exception); + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationResult.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationResult.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerificationResult.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,74 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.imsglobal.lti.launch; + +import org.imsglobal.lti.launch.LtiError; +import org.imsglobal.lti.launch.LtiLaunch; + +/** + * + * @author pgray + */ +public class LtiVerificationResult { + + private Boolean success; + private LtiError error; + private String message; + private LtiLaunch ltiLaunchResult; + + public LtiVerificationResult() { + } + + public LtiVerificationResult(Boolean success, LtiError error, String message) { + this.success = success; + this.error = error; + this.message = message; + } + + public LtiVerificationResult(Boolean success, LtiLaunch ltiLaunchResult) { + this.ltiLaunchResult = ltiLaunchResult; + this.success = success; + } + + public LtiVerificationResult(Boolean success, LtiError error, String message, LtiLaunch ltiLaunchResult) { + this.success = success; + this.error = error; + this.message = message; + this.ltiLaunchResult = ltiLaunchResult; + } + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public LtiError getError() { + return error; + } + + public void setError(LtiError error) { + this.error = error; + } + + public LtiLaunch getLtiLaunchResult() { + return ltiLaunchResult; + } + + public void setLtiLaunchResult(LtiLaunch ltiLaunchResult) { + this.ltiLaunchResult = ltiLaunchResult; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerifier.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerifier.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/lti/launch/LtiVerifier.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,44 @@ +package org.imsglobal.lti.launch; + + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * This interface contains methods that verify HttpRequests + * and generic request parameters according to the LTI + * specification. + * @author Paul Gray + * @since 1.1 + */ +public interface LtiVerifier { + + /** + * This method will verify the HttpServletRequest. + * + * @param request the HttpServletRequest that will be verified + * @param secret the secret to verify the properties with + * @return an LtiVerificationResult which will + * contain information about the request (whether or + * not it is valid, and if it is valid, contextual + * information about the request). + * @throws LtiVerificationException + */ + public LtiVerificationResult verify(HttpServletRequest request, String secret) throws LtiVerificationException; + + /** + * This method will verify a list of properties (mapped + * by key & value). + * @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 an LtiVerificationResult which will + * contain information about the request (whether or + * not it is valid, and if it is valid, contextual + * information about the request). + * @throws LtiVerificationException + */ + public LtiVerificationResult verifyParameters(Map parameters, String url, String method, String secret) throws LtiVerificationException; + +} Index: 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/pox/IMSPOXRequest.java =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/pox/IMSPOXRequest.java (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/java/org/imsglobal/pox/IMSPOXRequest.java (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,650 @@ +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.UUID; +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.lang.StringEscapeUtils; +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 bodyMap = null; + private Map 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 getHeaderMap() + { + if ( headerMap != null ) return headerMap; + if ( headerElement == null ) return null; + headerMap = XMLMap.getMap(headerElement); + return headerMap; + } + + public Map 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 = + "\n" + + "\n" + + " \n" + + " \n" + + " V1.0\n" + + " %s\n" + + " \n" + + " failure\n" + + " error\n" + + " %s\n" + + " %s" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + 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 = + "\n" + + "\n" + + " \n" + + " \n" + + " V1.0\n" + + " %s\n" + + " \n" + + " %s\n" + + " %s\n" + + " %s\n" + + " %s\n" + + " %s" + + "%s\n"+ + " \n" + + " \n" + + " \n" + + " \n" + + "%s%s"+ + " \n" + + ""; + + 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 \n"); + sb.append(" \n "); + sb.append(key); + sb.append("\n "); + sb.append(StringEscapeUtils.escapeXml(value)); + sb.append("\n \n"); + } + if ( sb.length() > 0 ) sb.append(" "); + } + 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(" 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); + + } + + public static final String replaceResultMessage = + ""+ + ""+ + " "+ + " "+ + " V1.0"+ + //*LAMS* The following line was added by LAMS + " %s"+ + " "+ + " "+ + " "+ + " "+ + " "+ + " "+ + " %s"+ + " "+ + " "+ + " "+ + //*LAMS* The following line was added by LAMS + " en"+ + " %s"+ + " "+ + " %s"+ + " "+ + " "+ + " "+ + " "+ + ""; + + static final String resultDataText = "%s"; + + static final String resultDataUrl = "%s"; + + 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 { + String dataXml = ""; + if (resultData != null) { + String format = isUrl ? resultDataUrl : resultDataText; + dataXml = String.format(format, StringEscapeUtils.escapeXml(resultData)); + } + //*LAMS* the following line was added by LAMS and also messageIdentifier was added to the line after it + String messageIdentifier = UUID.randomUUID().toString(); + String xml = String.format(replaceResultMessage, 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 + + + +GUID.TYPE + +GUID.TYPE +MEMBERSHIPIDTYPE.TYPE + +GUID.TYPE + +STRING +STRING + +DATETIME +DATETIME +BOOLEAN + +LANGUAGESET.TYPE +STRING + + +STATUS.TYPE +DATETIME +GUID.TYPE + + +STRING +FIELDTYPE.TYPE +STRING + + + + +STRING +FIELDTYPE.TYPE +STRING + + + + +INTEGER +GUID.TYPE + + + + */ +} Index: 3rdParty_sources/basiclti-util/src/main/resources/META-INF/LICENSE =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/resources/META-INF/LICENSE (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/resources/META-INF/LICENSE (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,248 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +=============================================================================== + +The BasicLTI Utilities (basiclti-util) distribution includes a number of subcomponents +with separate copyright notices and license terms. Your use of the +code for the these subcomponents is subject to the terms and +conditions of the following licenses. + +=============================================================================== +OAuth: + +Copyright (c) 1998-2009 AOL LLC. +Copyright 2007, 2008 Google, Inc. +Copyright 2007, 2008 Netflix, Inc. + +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. + +=============================================================================== +Base64: + +Copyright 1999-2008 The Apache Software Foundation. + +The ASF licenses this file to You 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. + +=============================================================================== Index: 3rdParty_sources/basiclti-util/src/main/resources/META-INF/NOTICE =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/resources/META-INF/NOTICE (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/resources/META-INF/NOTICE (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,10 @@ +BasicLTI Utilities (basiclti-util) +Copyright 2010 IMS Global Learning Consortium www.imsglobal.org + +----------------------------------------------------------- + +This product includes software (OAuth) developed by +AOL LLC., Google, Inc., and Netflix, Inc. (http://code.google.com/p/oauth/). + +This product includes software (Base64) developed at +The Apache Software Foundation (http://www.apache.org/). Index: 3rdParty_sources/basiclti-util/src/main/resources/META-INF/README =================================================================== diff -u --- 3rdParty_sources/basiclti-util/src/main/resources/META-INF/README (revision 0) +++ 3rdParty_sources/basiclti-util/src/main/resources/META-INF/README (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -0,0 +1,14 @@ + IMS Global Learning Consortium BasicLTI Utilities + + What is it? + ----------- + + BasicLTI Utilities are a set of utility classes to aid in the development + of BasicLTI consumers and providers. They deal with much of the heavy lifting + and make the process more opaque to the developer. + + Licensing + --------- + + Please see the file called LICENSE source directories. + Index: 3rdParty_sources/versions.txt =================================================================== diff -u -r96f14f440726936ee35b0547416534e59d3db24c -r61e489a64fd46325ed8b232df23b9ee923ca9217 --- 3rdParty_sources/versions.txt (.../versions.txt) (revision 96f14f440726936ee35b0547416534e59d3db24c) +++ 3rdParty_sources/versions.txt (.../versions.txt) (revision 61e489a64fd46325ed8b232df23b9ee923ca9217) @@ -5,6 +5,8 @@ This project contains following versions: +basiclti-util 1.2 + CKEditor Java 2.6 Apache HttpClient 4.2.1