/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/api/src/main/java/org/sakaiproject/event/api/LearningResourceStoreService.java $ * $Id: LearningResourceStoreService.java 123516 2013-05-02 15:08:50Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.event.api; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; /** * Provides support for Sakai to work with Learning Record Stores (LRS) * Allows centralized registration of LRS activity statements which Sakai * will then route over to the configured LRS system (via the Experience API (XAPI)). * See https://jira.sakaiproject.org/browse/KNL-1042 * * http://en.wikipedia.org/wiki/Learning_Record_Store * A Learning Record Store (LRS) is a data store that serve as a repository for learning records * necessary for using the Experience API (XAPI). The Experience API (XAPI) is also known as "next-gen SCORM" * or previously the TinCanAPI. The concept of the LRS was introduced to the e-learning industry in 2011, * and is a shift to the way e-learning specifications function. * Experience API current spec: * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md * * @author Aaron Zeckoski (azeckoski @ vt.edu) */ public interface LearningResourceStoreService { static String XAPI_ACTIVITIES_PREFIX = "http://adlnet.gov/expapi/activities/"; /** * Register an activity statement with the LRS * This is generally only for internal/system/service use, though it can be used by tools if needed * NOTE: this will run asynchronously to avoid slowing anything down so there is no return * * @param statement the LRS statement representing the activity statement * @param origin [OPTIONAL] a key identifying the origin of the statement, used for logging and filtering * (typically the Sakai toolId if known OR null if not known) * @throws IllegalArgumentException if the input statement is invalid or cannot be handled * @throws RuntimeException if there is a FATAL failure */ public void registerStatement(LRS_Statement statement, String origin); /** * @return true if LRS tracking is enabled, false otherwise */ public boolean isEnabled(); /** * Allows for manual registration of an LRSP, * it is best to simply allow the system to discover all the * LRSPs which the main Sakai Spring AC knows about instead but * this allows for some testing and also for cases where Spring is not being used * * NOTE: there is no "unregister", the system will simply ignore the provider * if it is completely destroyed some time after registration * * @param provider an instantiated LRSP * @return true if the provider replaced another one with the same ID, false if it was the first one */ public boolean registerProvider(LearningResourceStoreProvider provider); /** * Converts the Sakai event object data into an Actor * * @param event a Sakai Event with userID or sessionId set (so we can try to determine the user) * @return the actor for the user related to the event OR null if no user can be determined from the event */ public LRS_Actor getEventActor(Event event); // Service CLASSes public static class LRS_Statement { // actor, verb, and object are required /** * if true then this LRS_Statement is populated with all required fields (actor, verb, and object), * if false then check the raw data fields instead: {@link #rawMap} first and if null or empty then {@link #rawJSON}, * it should be impossible for object to have none of these fields populated */ boolean populated = false; /** * A raw map of the keys and values which should be able to basically be converted directly into a JSON statement, * MUST contain at least actor, verb, and object keys and the values for those cannot be null or empty */ Map<String, Object> rawMap; /** * The raw JSON string to send as a statement * WARNING: this will not be validated */ String rawJSON; /** * UUID assigned by LRS or other trusted source. * Set by LRS. */ String id = null; /** * Timestamp of when what this statement describes happened. * If null, the LRS will set this to the stored time. */ Date timestamp; /** * Timestamp of when this statement was recorded. * Set by LRS. */ Date stored; /** * REQUIRED: * Who the statement is about, as an Agent or Group object. "I" */ LRS_Actor actor; /** * REQUIRED: * Action of the Learner or Team object. "Did" */ LRS_Verb verb; /** * REQUIRED: * Activity, agent, or another statement that is the object of the statement, "this". * NOTE that objects which are provided as a value for this field should include a 'objectType' field. If not specified, the object is assumed to be an activity. */ LRS_Object object; /** * Result object, further details relevant to the specified verb. */ LRS_Result result; /** * Context that gives the statement more meaning. * Examples: Team actor is working with, altitude in a flight simulator, course in a classroom activity. */ LRS_Context context; /** * use of the empty constructor is restricted */ protected LRS_Statement() { timestamp = new Date(); } /** * MINIMAL objects constructor * @param actor * @param verb * @param object */ public LRS_Statement(LRS_Actor actor, LRS_Verb verb, LRS_Object object) { this(); if (actor == null) { throw new IllegalArgumentException("LRS_Actor cannot be null"); } if (verb == null) { throw new IllegalArgumentException("LRS_Verb cannot be null"); } if (object == null) { throw new IllegalArgumentException("LRS_Object cannot be null"); } this.actor = actor; this.verb = verb; this.object = object; this.populated = true; } /** * FULL objects constructor * @param actor * @param verb * @param object * @param result * @param context */ public LRS_Statement(LRS_Actor actor, LRS_Verb verb, LRS_Object object, LRS_Result result, LRS_Context context) { this(actor, verb, object); this.result = result; this.context = context; } /** * Construct a simple LRS statement * * @param actorEmail the user email address, "I" * @param verbStr a string indicating the action, "did" * @param objectURI URI indicating the object of the statement, "this" */ public LRS_Statement(String actorEmail, String verbStr, String objectURI) { this(new LRS_Actor(actorEmail), new LRS_Verb(verbStr), new LRS_Object(objectURI)); } /** * Construct a simple LRS statement with Result * * @param actorEmail the user email address, "I" * @param verbStr a string indicating the action, "did" * @param objectURI URI indicating the object of the statement, "this" * @param resultSuccess true if the result was successful (pass) or false if not (fail), "well" * @param resultScaledScore Score from -1.0 to 1.0 where 0=0% and 1.0=100% */ public LRS_Statement(String actorEmail, String verbStr, String objectURI, boolean resultSuccess, float resultScaledScore) { this(new LRS_Actor(actorEmail), new LRS_Verb(verbStr), new LRS_Object(objectURI)); this.result = new LRS_Result(resultScaledScore, resultSuccess); } /** * EXPERT USE ONLY * @param rawData map of the keys and values which MUST contain at least actor, verb, and object keys and the values for those cannot be null or empty * @throws IllegalArgumentException if any required keys are missing * @see #rawMap */ public LRS_Statement(Map<String, Object> rawData) { this(); this.populated = false; this.rawMap = rawData; if (rawData != null) { if (!rawData.containsKey("actor") || rawData.get("actor") == null) { throw new IllegalArgumentException("actor key MUST be set and NOT null"); } if (!rawData.containsKey("verb") || rawData.get("verb") == null) { throw new IllegalArgumentException("verb key MUST be set and NOT null"); } if (!rawData.containsKey("object") || rawData.get("object") == null) { throw new IllegalArgumentException("object key MUST be set and NOT null"); } this.rawMap = new LinkedHashMap<String, Object>(rawData); this.rawJSON = null; } } /** * INTERNAL USE ONLY * Probably will not work for anything that is NOT the Experience API * @param rawJSON JSON string to send as a statement * WARNING: this will NOT be validated! * @see #rawJSON */ public LRS_Statement(String rawJSON) { this(); this.populated = false; this.rawJSON = rawJSON; if (rawJSON != null) { this.rawMap = null; } } /** * Set or clear (using null) the context for this statement * @param context * @see #context */ public void setContext(LRS_Context context) { this.context = context; } /** * Set or clear (using null) the result for this statement * @param result * @see #result */ public void setResult(LRS_Result result) { this.result = result; } // GETTERS /** * @see #populated */ public boolean isPopulated() { return populated; } /** * @see #rawMap */ public Map<String, Object> getRawMap() { return rawMap; } /** * @see #rawJSON */ public String getRawJSON() { return rawJSON; } /** * @see #id */ public String getId() { return id; } /** * @see #timestamp */ public Date getTimestamp() { return timestamp; } /** * @see #stored */ public Date getStored() { return stored; } /** * @see #actor */ public LRS_Actor getActor() { return actor; } /** * @see #verb */ public LRS_Verb getVerb() { return verb; } /** * @see #object */ public LRS_Object getObject() { return object; } /** * @see #result */ public LRS_Result getResult() { return result; } /** * @see #context */ public LRS_Context getContext() { return context; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { String s; if (this.getRawJSON() != null && !"".equals(this.getRawJSON())) { s = "Statement(json):"+this.getRawJSON(); } else if (this.getRawMap() != null && !this.getRawMap().isEmpty()) { s = "Statement(map):"+this.getRawJSON(); } else { s = "Statement[pop=" + populated + ", id=" + id + ", timestamp=" + timestamp + ", actor=" + actor + ", verb=" + verb + ", object=" + object + (result==null?"":", result=" + result) + (context==null?"":", context=" + context) + "]"; } return s; } } public static class LRS_Actor { /* * NOTE: for now we are only representing an Agent type of Actor (no Group actors) * NOTE: For now we are ignoring the account and openid agent inverse functional identifiers * * An Agent object is identified by an email address (or its hash), OpenID, or account on some system (such as twitter), * but only for values where any two Agents that share the same identifying property definitely represent the same identity. * The term used for properties with that characteristic is "inverse functional identifiers”. * In addition to the standard inverse functional properties from FOAF of mbox, mbox_sha1sum, and openid, * account is an inverse functional property in XAPI Agents. * For reasons of practicality and privacy, TCAPI Agents MUST be identified by one and only one inverse functional identifier. * Agents MUST NOT include more than one inverse functional identifier. * If an Activity Provider is concerned about revealing identifying information such as emails, * it SHOULD instead use an account with an opaque account name to identify the person. */ /** * "Agent" or "Group" (Optional, except when used as a statement’s object) */ String objectType; /** * Display String (Optional) */ String name; /** * String in the form "mailto:email address". * (Note: Only emails that have only ever been and will ever be assigned to this Agent, but no others, should be used for this property and mbox_sha1sum). */ String mbox; /** * @param email the user email address * @return an actor built using the given email address */ static LRS_Actor makeFromEmail(String email) { LRS_Actor actor = new LRS_Actor(email); return actor; } /** * use of the empty constructor is restricted */ protected LRS_Actor() { objectType = "Agent"; } /** * Construct an actor using an email address * @param email the user email address */ public LRS_Actor(String email) { this(); if (email == null) { throw new IllegalArgumentException("LRS_Actor email cannot be null"); } mbox = "mailto:"+email; } /** * @param name OPTIONAL display value for this actor */ public void setName(String name) { this.name = name; } // GETTERS /** * @see #objectType */ public String getObjectType() { return objectType; } /** * @see #name */ public String getName() { return name; } /** * @see #mbox */ public String getMbox() { return mbox; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "Actor[mbox=" + mbox + ", name=" + name + "]"; } } public static class LRS_Verb { /* * A verb defines what the action is between actors, activities, or most commonly, between an actor and activity. * The Experience API does not specify any particular verbs, but rather defines how verbs are to be created. * It is expected that verb lists exist for various communities of practice. Verbs appear in statements as objects * consisting of a URI and a set of display names. * * The Verb URI should identify the particular semantics of a word, not the word itself. * For example, the English word "fired" could mean different things depending on context, * such as "fired a weapon", "fired a kiln", or "fired an employee". * In this case, a URI should identify one of these specific meanings, not the word "fired". */ static String XAPI_VERBS_PREFIX = "http://www.adlnet.gov/expapi/verbs/"; static String SAKAI_VERBS_PREFIX = "http://sakaiproject.org/expapi/verbs/"; /** * Set of Sakai verbs (limited set of verbs that make sense for use in Sakai) * Based on ADL approved verbs for 1.0 * http://www.adlnet.gov/expapi/verbs/ */ public enum SAKAI_VERB { answered, asked, attempted, attended, commented, completed, exited, experienced, failed, imported, initialized, interacted, launched, mastered, passed, preferred, progressed, registered, responded, resumed, scored, shared, suspended, terminated, voided, } /** * REQUIRED: * A URI that corresponds to a verb definition. * Each verb definition corresponds to the meaning of a verb, not the word. * A URI should be human-readable and contain the verb meaning. * Example: www.adlnet.gov/XAPIprofile/ran(travelled_a_distance) */ String id; /** * OPTIONAL: * A language map containing the human readable display representation * of the verb in at least one language. This does not have any impact * on the meaning of the statement, but only serves to give a human-readable display * of the meaning already determined by the chosen verb. * Example: { "en-US" => "ran", "es" => "corrió" } */ Map<String, String> display; /** * use of the empty constructor is restricted */ protected LRS_Verb() {} /** * Create a verb to indicate what the user did. * Limited to the restricted set of applicable verbs * * @param verb an ADL approved verb for 1.0 */ public LRS_Verb(SAKAI_VERB verb) { this(); if (verb == null) { throw new IllegalArgumentException("LRS_Verb SAKAI_VERB verb cannot be null"); } id = XAPI_VERBS_PREFIX + verb.name(); } /** * Create a verb to indicate what the user did. * Open to any verb (recommend using lowercase for consistency) * * The verb should probably come from this listing: * http://tincanapi.wikispaces.com/Verbs+and+Activities * * @param verb a string indicating the action */ public LRS_Verb(String verb) { this(); if (verb == null) { throw new IllegalArgumentException("LRS_Verb verb cannot be null"); } this.id = (verb.indexOf("://") == -1 ? SAKAI_VERBS_PREFIX + verb : verb); } /** * OPTIONAL: * A language map containing the human readable display representation * of the verb in at least one language. This does not have any impact * on the meaning of the statement, but only serves to give a human-readable display * of the meaning already determined by the chosen verb. * Example: { "en-US" => "ran", "es" => "corrió" } */ public void setDisplay(Map<String, String> display) { this.display = display; } // GETTERS /** * @see #id */ public String getId() { return id; } /** * @see #display */ public Map<String, String> getDisplay() { return display; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "Verb[id=" + id + "]"; } } public static class LRS_Object { /* * NOTE: For our use, objectType will always be "Activity" and we will only use a limited set of the detail fields * * The object of a statement is the Activity, Agent, or Statement that is the object of the statement, "this". * Note that objects which are provided as a value for this field should include an "objectType" field. * If not specified, the object is assumed to be an Activity. * * An activity URI must always refer to a single unique activity. * There may be corrections to that activity's definition. Spelling fixes would be appropriate, * for example, but changing correct responses would not. * The activity URI is unique, and any reference to it always refers to the same activity. * Activity Providers must ensure this is true and the LRS may not attempt to treat multiple references to * the same URI as references to different activities, regardless of any information which indicates * two authors or organizations may have used the same activity URI. */ /** * URI. An activity URI must always refer to a single unique activity. * If a URL, the URL should refer to metadata for this activity * Example: http://example.adlnet.gov/xapi/example/simpleCBT */ String id; /** * URI, the type of activity. (e.g. http://sakaiproject.org/expapi/activity/assessment) * Note, URI fragments (sometimes called relative URLs) are not valid URIs. * Similar to verbs, we recommend that Learning Activity Providers look for and use established, widely adopted, activity types. */ String activityType; /** * OPTIONAL: * A language map containing the human readable display representation * of the object in at least one language. This does not have any impact * on the meaning of the statement, but only serves to give a human-readable display * of the meaning already determined by the chosen verb. * Example: { "en-US" => "ran", "es" => "corrió" } */ Map<String, String> activityName; /** * OPTIONAL: * A language map containing the human readable description of the Activity. * Example: { "en-US" => "User completed quiz 1" } */ Map<String, String> descMap; /** * use of the empty constructor is restricted */ protected LRS_Object() { } // TODO include the other optional Interaction Activities and Activities fields? /** * Create an LRS object * * @param uri activity URI that refers to a single unique activity. * Example: http://example.adlnet.gov/xapi/example/simpleCBT */ public LRS_Object(String uri) { this(); if (uri == null) { throw new IllegalArgumentException("LRS_Object uri cannot be null"); } id = uri; } /** * @param uri activity URI that refers to a single unique activity. (e.g. http://example.com/activity/spelling-test) * @param activityType activity URI that refers to the type of activity. (e.g. http://adlnet.gov/expapi/activities/assessment) */ public LRS_Object(String uri, String activityType) { this(uri); if (activityType == null) { throw new IllegalArgumentException("LRS_Object type cannot be null"); } this.activityType = (activityType.indexOf("://") == -1 ? XAPI_ACTIVITIES_PREFIX + activityType : activityType); } /** * @param activityType activity URI that refers to the type of activity. (e.g. assessment) */ public void setActivityType(String type) { this.activityType = type; } /** * OPTIONAL: * A language map containing the human readable description of the Activity. * Example: { "en-US" => "User completed quiz 1" } */ public void setDescription(Map<String, String> desc) { this.descMap = desc; } /** * OPTIONAL: * A language map containing the human readable display representation * of the object in at least one language. This does not have any impact * on the meaning of the statement, but only serves to give a human-readable display * of the meaning already determined by the chosen verb. * Example: { "en-US" => "ran", "es" => "corrió" } */ public void setActivityName(Map<String, String> name) { this.activityName = name; } // GETTERS /** * @see #id */ public String getId() { return id; } /** * @see #name */ public Map<String, String> getActivityName() { return activityName; } /** * @see #type */ public String getActivityType() { return activityType; } /** * @see #descMap */ public Map<String,String> getDescription() { return descMap; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return "Object[id=" + id + ", activityType=" + activityType + "]"; } } public static class LRS_Result { /* * The result field represents a measured outcome related to the statement, such as completion, success, or score. * It is also extendible to allow for arbitrary measurements to be included. * NOTE: the API score fields types are unclear in the spec (maybe int or float) */ /** * Score from -1.0 to 1.0 where 0=0% and 1.0=100% */ Float scaled; /** * Raw score - any number */ Number raw; /** * Minimum score (range) - any number */ Number min; /** * Maximum score (range) - any number */ Number max; /** * string representation of the grade (e.g. A, B, C, D, F, pass, fail, first, second, etc.) * NOTE: this should be encoded into the XAPI extension for the result. Example for "A": * "extensions": { * "http://sakaiproject.org/xapi/activities/grade": "A" * } * * Or the more complex and supposedly portable way (lowercase, strip spaces, and append in the id): * "result" : { * ..... * "extensions" : { * "http://sakaiproject.org/xapi/extensions/result/classification" : { * "objectType" : "activity", * "id":"http://sakaiproject.org/xapi/activities/grade-a", * "definition" : { * "type" : "http://sakaiproject.org/xapi/activitytypes/grade_classification", * "name" : { * "en-US":"A" * } * } * } * } * } */ String grade; /** * true if successful, false if not, or null for unknown */ Boolean success; /** * true if completed, false if not, or null for unknown */ Boolean completion; /** * Duration of the activity in seconds * Have to convert this to https://en.wikipedia.org/wiki/ISO_8601#Durations for sending to the Experience API, * ignore the value if it is less than 0 */ int duration = -1; /** * A string response appropriately formatted for the given activity. */ String response; /** * use of the empty constructor is restricted */ protected LRS_Result() { } /** * Simplest possible result, only indicates if it was completed or not, * generally should be used only when nothing else will fit * @param completion true if completed, false if not (cannot be null) */ public LRS_Result(boolean completion) { this(); this.completion = completion; } /** * @param scaled Score from -1.0 to 1.0 where 0=0% and 1.0=100% * @param success true if successful, false if not, or null for not specified * @throws IllegalArgumentException if scaled is not valid */ public LRS_Result(Float scaled, Boolean success) { this(); if (scaled == null) { throw new IllegalArgumentException("LRS_Result scaled cannot be null"); } setScore(scaled); this.success = success; } /** * @param raw Raw score - any number, must be >= min and <= max (if they are set) * @param min Minimum score (range) - any number (can be null) * @param max Maximum score (range) - any number (can be null) * @param success true if successful, false if not, or null for not specified * @throws IllegalArgumentException if the minimum is not less than (or equal to) the maximum OR raw is not within the range OR all values are null */ public LRS_Result(Number raw, Number min, Number max, Boolean success) { this(); if (raw == null) { throw new IllegalArgumentException("LRS_Result raw cannot be null"); } setScore(null, raw, min, max); this.success = success; } /** * NOTE: always use the numeric score when possible, this is only to be used when you cannot convert to a numeric score * @param grade a string grade value (will be stored as an extension), cannot be null or empty * @param success true if successful, false if not, or null for not specified * @see #grade */ public LRS_Result(String grade, Boolean success) { this(); if (grade == null || "".equals(grade)) { throw new IllegalArgumentException("LRS_Result grade cannot be null or empty"); } this.success = success; } // TODO optional extensions? /** * Set the score to a floating point scaled range value * * @param scaled Score from -1.0 to 1.0 where 0=0% and 1.0=100% * @throws IllegalArgumentException if the scaled value is outside the -1 to 1 (inclusive) range */ public void setScore(Float scaled) { this.scaled = scaled; if (scaled != null) { if (scaled.floatValue() < -1.0f) { throw new IllegalArgumentException("LRS_Result scaled cannot be < -1"); } else if (scaled.floatValue() > 1.0f) { throw new IllegalArgumentException("LRS_Result scaled cannot be > 1"); } } } /** * @param raw Raw score - any number (can be null), must be >= min and <= max (if they are set) * @throws IllegalArgumentException if raw is not within the min-max range */ public void setRawScore(Number raw) { if (raw != null) { if (this.min != null && raw.floatValue() < min.floatValue()) { throw new IllegalArgumentException("score raw ("+raw+") must not be less than min ("+this.min+")"); } if (this.max != null && raw.floatValue() > max.floatValue()) { throw new IllegalArgumentException("score raw ("+raw+") must not be greater than max ("+this.max+") inclusive"); } } this.raw = raw; } /** * Set up a completely detailed score, * NOTE: scaled MUST be within the range of -1 to 1 inclusive * NOTE: raw MUST be within the range of min to max inclusive * * @param scaled Score from -1.0 to 1.0 where 0=0% and 1.0=100% * @param raw Raw score - any number (can be null), must be >= min and <= max (if they are set) * @param min Minimum score (range) - any number (can be null) * @param max Maximum score (range) - any number (can be null) * @throws IllegalArgumentException if the scaled value is outside the -1 to 1 (inclusive) range OR if the minimum is not less than (or equal to) the maximum OR raw is not within the range OR all values are null */ public void setScore(Float scaled, Number raw, Number min, Number max) { if (scaled == null && raw == null && min == null && max == null) { throw new IllegalArgumentException("score inputs cannot all be null"); } setScore(scaled); this.min = min; this.max = max; if (this.min != null && this.max != null) { if (min.floatValue() > max.floatValue()) { throw new IllegalArgumentException("score min ("+this.min+") must be less than max ("+this.max+")"); } } setRawScore(raw); } /** * NOTE: always use the numeric score when possible, this is only to be used when you cannot convert to a numeric score * @param grade a string grade value (will be stored as an extension), null to clear * @see #grade */ public void setGrade(String grade) { this.grade = grade; } /** * @param success true if successful, false if not, or null if not specified * @see #success */ public void setSuccess(Boolean success) { this.success = success; } /** * @param completion true if completed, false if not, or null if not specified * @see #completion */ public void setCompletion(Boolean completion) { this.completion = completion; } /** * @param duration Time spent on the activity in seconds, set to -1 to clear this * @see #duration */ public void setDuration(int duration) { this.duration = duration; } /** * @param A response appropriately formatted for the given Activity. * @see #response */ public void setResponse(String response) { this.response = response; } // GETTERS /** * @see #scaled */ public Float getScaled() { return scaled; } /** * @see #raw */ public Number getRaw() { return raw; } /** * @see #min */ public Number getMin() { return min; } /** * @see #max */ public Number getMax() { return max; } /** * @see #success */ public Boolean getSuccess() { return success; } /** * @see #completion */ public Boolean getCompletion() { return completion; } /** * @see #duration */ public int getDuration() { return duration; } /** * @see #response */ public String getResponse() { return response; } /** * @see #grade */ public String getGrade() { return grade; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { String points = ""; if (scaled != null) { points = "scaled=" + scaled; } if (raw != null) { points += ",raw=" + scaled; } if (min != null && max != null) { points += ",min=" + min + ",max=" + max; } return "Result["+points+(grade!=null?" "+grade:"")+(response!=null?" response="+response:"")+(success!=null?(success?" success":" fail"):"")+(completion!=null?(completion?" complete":" incomplete"):"")+ "]"; } } public static class LRS_Context { /* * The context field provides a place to add some contextual information to a statement. * We can add information such as the instructor for an experience, if this experience * happened as part of a team activity, or how an experience fits into some broader activity. */ /** * OPTIONAL * Instructor that the statement relates to, * if not included as the actor or object of the overall statement. */ LRS_Actor instructor; /** * OPTIONAL * Revision of the learning activity associated with this statement. * Revisions are to track fixes of minor issues (like a spelling error), * if there is any substantive change to the learning objectives, pedagogy, * or assets associated with an activity, a new activity ID should be used. * Revision format is up to the owner of the associated activity. */ String revision; /** * A map of the types of context to learning activities “activity” this statement is related to. * Many Statements do not just involve one Object Activity that is the focus, but relate to other contextually relevant Activities. * "Context Activities" allow for these related Activities to be represented in a structured manner. * Valid context types are: "parent", "grouping", "category", and "other". * For example, if I am studying a textbook, for a test, the textbook is the activity the statement is about, * but the test is a context activity, and the context type is "other". * "other" : {"id" : "http://example.adlnet.gov/xapi/example/test"} * There could be an activity hierarchy to keep track of, for example question 1 on test 1 for the course Algebra 1. * When recording results for question 1, it we can declare that the question is part of test 1, * but also that it should be grouped with other statements about Algebra 1. This can be done using parent and grouping: * { * "parent" : {"id" : "http://example.adlnet.gov/xapi/example/test 1"}, * "grouping" : {"id" : "http://example.adlnet.gov/xapi/example/Algebra1"} * } */ Map<String, Map<String, String>> activitiesMap; /** * Platform used in the experience of this learning activity. */ String platform = "SakaiCLE"; // TODO include fields like team, platform, language, statement, and extensions /** * use of the empty constructor is restricted */ protected LRS_Context() { } /** * @param instructor Instructor user that the statement relates to */ public LRS_Context(LRS_Actor instructor) { this(); if (instructor == null) { throw new IllegalArgumentException("LRS_Context instructor cannot be null"); } this.instructor = instructor; } /** * @param contextType must be "parent", "grouping", "category", and "other" * @param activityId a URI or key identifying the activity type (e.g. http://example.adlnet.gov/xapi/example/test) */ public LRS_Context(String contextType, String activityId) { this(); setActivity(contextType, activityId); } /** * @param instructor Instructor user that the statement relates to */ public void setInstructor(LRS_Actor instructor) { this.instructor = instructor; } /** * @param instructorEmail Instructor user email that the statement relates to */ public void setInstructor(String instructorEmail) { this.instructor = new LRS_Actor(instructorEmail); } /** * @param contextType must be "parent", "grouping", and "other" * @param activityId a URI or key identifying the activity type (e.g. http://adlnet.gov/expapi/activities/test) */ public void setActivity(String contextType, String activityId) { if (contextType == null || "".equals(contextType)) { throw new IllegalArgumentException("contextType MUST be set"); } if (activityId == null || "".equals(activityId)) { throw new IllegalArgumentException("activityId MUST be set"); } if (this.activitiesMap == null) { this.activitiesMap = new LinkedHashMap<String, Map<String, String>>(); } if (!this.activitiesMap.containsKey(contextType) || this.activitiesMap.get(contextType) == null) { this.activitiesMap.put(contextType, new LinkedHashMap<String, String>()); } activityId = (activityId.indexOf("://") == -1 ? XAPI_ACTIVITIES_PREFIX + activityId : activityId); this.activitiesMap.get(contextType).put("id", activityId); } /** * A map of the types of context to learning activities “activity” this statement is related to. * Valid context types are: "parent", "grouping", and "other". * For example, if I am studying a textbook, for a test, the textbook is the activity the statement is about, * but the test is a context activity, and the context type is "other". * "other" : {"id" : "http://example.adlnet.gov/xapi/example/test"} * There could be an activity hierarchy to keep track of, for example question 1 on test 1 for the course Algebra 1. * When recording results for question 1, it we can declare that the question is part of test 1, * but also that it should be grouped with other statements about Algebra 1. This can be done using parent and grouping: * { * "parent" : {"id" : "http://example.adlnet.gov/xapi/example/test 1"}, * "grouping" : {"id" : "http://example.adlnet.gov/xapi/example/Algebra1"} * } * @param activitiesMap map where the values should be strings or other maps */ public void setActivitiesMap(Map<String, Map<String, String>> activitiesMap) { this.activitiesMap = activitiesMap; } // GETTERS /** * @see #instructor */ public LRS_Actor getInstructor() { return instructor; } /** * @see #revision */ public String getRevision() { return revision; } /** * @see #activitiesMap */ public Map<String, Map<String, String>> getActivitiesMap() { return activitiesMap; } /** * @see #revision */ public String getPlatform() { return platform; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "Context[instructor=" + instructor + ", rev=" + revision + ", activities=" + activitiesMap + "]"; } } }