/////////////////////////////////////////////////////////////////////////////////////////////////// // // Copyright © 2000, Microsoft Corp. All rights reserved. // // @doc INTERNAL // // @module LMS_API.inc | // Contains the AICC LMS API Implementation for rich clients (IE5+). // // @end // Creator: a-mbatem // Created: 11/29/99 // Current Owner: tmarsh // /////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// // // Client-Side script // /////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// // // Global variables // /////////////////////////////////////////////////////////////////////////////////////////////////// // Localizable strings var L_Success_Message = "Success"; var Trace = NoTrace; var gc_strUserDataPrefix = "lrnpers&"; var gc_strPersDBShortName = "lrnpers"; var gc_strMainXMLAttribute = "mainXML"; var gc_strPropertyPropName = "property"; var gc_strValuePropName = "property_value"; var gc_strXMLValuePropName = "property__value"; var gc_strScopePropName = "scope"; var gc_strScopeProperty = "lrn.startlocation"; var gc_strLRNPrefix = "lrn."; // Error details types var gc_strParseError = "XML_Parse_Error"; var gc_strJScriptError = "JScript_Error"; var gc_strMultiStatusError = "Multi-Status_Error"; var gc_strString = "String"; var gc_strNoDetails = "Null"; /////////////////////////////////////////////////////////////////////////////////////////////////// // // Error handling and other utility functions // /////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc INTERNAL // // @jsfunc NoTrace | Default trace function -- don't do anything with trace output. // // @parm String | strMessage | The message to trace. Not used. // // @rdesc None. // //////////////////////////////////////////////////////////////////////////////////////////////////// function NoTrace ( strMessage ) { return; } //////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc INTERNAL // // @jsfunc ErrorStringFromID | Searches error XML for the description of an error. // // @parm Integer | iNumber | The numeric identifier of the error for which to search. // // @rdesc If the errors XML has not yet finished loading, returns null. If the ID cannot be found // in the errors XML, an empty string is returned. If the ID is found, the error // description is returned. // //////////////////////////////////////////////////////////////////////////////////////////////////// function ErrorStringFromID ( iNumber ) { if("complete" != oLMSErrorsXML.readyState) { // Return null for now. When a client requests the description again, we will make another // attempt to load the string. return null; } else { // Search for the string corresponding to the current number var oError = oLMSErrorsXML.XMLDocument.selectSingleNode("/root/PersError[@ID=" + iNumber + "]"); if(null == oError) { return ""; } else { return oError.text; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc INTERNAL // // @jsfunc PersError | Constructor function for personalization error objects. // // @parm Integer | iNumber | The numeric identifier of this error. The description // string will be loaded from the XML. // @parm String | strDetailsType | A string description of the type of information in // oErrorDetails. Several constants have been defined // above as types we understand. This parameter is // required if oErrorDetails is not null, but is ignored // if oErrorDetails is null. // @parm Object | oErrorDetails | Additional information about the error that occurred. // The type of this object is indicated by the // strDetailsType parameter. This parameter is optional. // // @rdesc Javascript personalization error object. // //////////////////////////////////////////////////////////////////////////////////////////////////// function PersError ( iNumber, strDetailsType, oErrorDetails ) { if("string" != typeof(strDetailsType)) { strDetailsType = gc_strNoDetails; } // Validate the inputs. if("number" != typeof(iNumber)) { // $ TODO: M2: Assert here. return null; } // Store the error number, details type, and details this.number = iNumber; if("undefined" != typeof(oErrorDetails) && null != oErrorDetails) { this.strDetailsType = strDetailsType; this.oErrorDetails = oErrorDetails; } else { this.strDetailsType = gc_strNoDetails; this.oErrorDetails = null; } // Fetch and store the string. this.description = ErrorStringFromID(iNumber); return this; } /////////////////////////////////////////////////////////////////////////////////////////////////// // // Implementation of the private methods of the LMSAPI object // /////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc Internal // // @jsfunc LMSAPI_Instance | Constructor for an LMSAPI object. One LMSAPI object should be // created for each personalization context (e.g. // the current course, or a specific syllabus item). // // @parm String | strContext| A URL indicating the context for the LMSAPI object. For example, // the caller would supply the URL of a course to retrieve and // modify properties related to the current user in the specified // course. // @parm Object | oUserData | An object with the userData behavior to load and save the offline // personalization XML cache. This parameter is optional and may // be null. // // @rdesc Returns a newly created LMSAPI object on success. On failure, returns null. // Failure is only caused by passing invalid arguments. // // @ex Usage: | // // Create an LMSAPI object for installation-wide personalization properties // var g_oInstallationLMSAPI = new LMSAPI_Instance(g_strInstallationURL, oUserData); // // // Now, create an LMSAPI object for personalization properties related to the // // current course. // var g_oCourseLMSAPI = new LMSAPI_Instance(g_strCourseURL, oUserData); // // @comm // @end // /////////////////////////////////////////////////////////////////////////////////////////////////// function LMSAPI_Instance ( strContext, strSaveName, oUserData ) { Trace("Entering LMSAPI_Instance context=" + strContext + " savename=" + strSaveName + " userData=" + oUserData); try { // Make nominal checks of the validity of the inputs if("string" != typeof(strContext) || "" == strContext) { Trace("Invalid context string"); return null; } // Make nominal checks of the validity of the inputs if("string" != typeof(strSaveName) || "" == strSaveName) { Trace("Invalid context string"); return null; } if("undefined" == typeof(oUserData)) { if(null == oUserData) { // $ TODO: M2: Uncommented, the next line will alert. // $ TODO: M2: Figure out how to allow the caller to specify not to use user data. //alert("null is not an object"); } oUserData = oDefaultUserData; } if("object" != typeof(oUserData) && null != oUserData) { Trace("Invalid user data object"); return null; } this.strContextURL = strContext; this.strSaveName = strSaveName; this.oUserData = oUserData; // Add private methods this.LRNInitialize = LRNInitialize; this.LRNTerminate = LRNTerminate; // Add LMS functions as our methods this.LMSInitialize = LMSInitialize; this.LMSGetValue = LMSGetValue; this.LMSSetValue = LMSSetValue; this.LMSCommit = LMSCommit; this.LMSGetLastError = LMSGetLastError; this.LMSGetErrorString = LMSGetErrorString; this.LMSGetDiagnostic = LMSGetDiagnostic; this.LMSFinish = LMSFinish; Trace("Returning a new LMSAPI object"); return this; } catch(e) { Trace("LMSAPI_Instance error: " + e.description); return null; } } /////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc Internal // // @jsfunc LRNInitialize | LRN initialization method for LMSAPI objects. This function loads // the personalization XML from the server, if the user is online, or // from the client-side userData cache, if the user is offline. This // method must be called before calling any LMS methods. If this // method returns false, only the LMS error methods may be called on // the LMSAPI object. // // @rdesc Returns true to indicate that the initialization was successful or false to // indicate a failure. // // @ex Usage: | // // Initialize the Interface & check for any errors. // // if(!LMSAPI.LRNInitialize()) // { // alert(LMSAPI.LMSGetErrorString(LMSAPI.LMSGetLastError())); // } // // @end // /////////////////////////////////////////////////////////////////////////////////////////////////// function LRNInitialize ( ) { Trace("Entering LRNInitialize"); try { if(this.bInitialized) { // We're already initialized. Trace("Already initialized -- returning"); return; } // Reset the API's last error so the current call starts clean. this.oLastError = null; // Check that the context URL has been set. if(null == this.strContextURL) { Trace("Context string was null!"); // $ TODO: M2: Create a constant for 302. throw new PersError(302); } if(null != this.oUserData) { // Load the personalization userData store. Trace("Attempting to load user data"); this.oUserData.load(gc_strUserDataPrefix + this.strSaveName); this.oMainXML = new ActiveXObject("Microsoft.XMLDOM"); if(null == this.oMainXML) { throw new PersError(304); } var strMainXML = this.oUserData.getAttribute(gc_strMainXMLAttribute); if("string" != typeof(strMainXML) || "" == strMainXML) { Trace("No XML in user data. Starting fresh."); this.oMainXML = oMainBaseXML.XMLDocument.cloneNode(true); } else { this.oMainXML.loadXML(strMainXML); } if(0 != this.oMainXML.parseError.errorCode) { // $ TODO: M2: Better error ID. throw new PersError(101, gc_strParseError, this.oMainXML.parseError); } } else { throw new PersError(304); } Trace("LRNInitialize succeeded."); // Successful initialization this.bInitialized = true; return true; } catch(oPersError) { Trace("Failed in LRNInitialize because " + oPersError.description); this.oLastError = oPersError; return false; } } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc Internal // // @jsfunc LRNTerminate | Commits the changes made to personalization data since calling // LRNInitialize. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and commit the change. // // (error checking omitted for clarity) // LMSAPI.LRNInitialize(); // LMSAPI.LMSInitialize(null); // LMSAPI.LMSSetValue("LastPageVisited", location.href); // LMSAPI.LMSFinish(); // LMSAPI.LRNTerminate(); // // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LRNTerminate ( oLMSAPI ) { try { this.LMSCommit(); this.bInitialized = false; } catch(e) { this.oLastError = new PersError(101, gc_strJScriptError, e); } } /////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc External // // @jsfunc LMSInitialize | LRN implementation of LMSInitialize, which initializes the LMS API // for use by the personalization client. The real work of // initializing the LMSAPI object is done in LRNInitialize, which is // a private method. This function does little or no initialization. // // @parm Object | null | A null must be passed for conformance to the current LMS standard. // // @rdesc Returns true to indicate that the initialization was successful or false to indicate // a failure. // // @ex Usage: | // // Initialize the Interface & check for any errors. // // if(!LMSAPI.LMSInitialize()) // { // alert(LMSAPI.LMSGetErrorString(LMSAPI.LMSGetLastError())); // } // // @comm // This function should not be called directly. It is meant to be called through an // instance of the LMSImplentation object that is returned by FindLMSAPI(). // @end // /////////////////////////////////////////////////////////////////////////////////////////////////// function LMSInitialize() { Trace("Entering LMSInitialize"); try { // Reset the API's last error so the current call starts clean. this.oLastError = null; if(!this.bInitialized) { Trace("LRNInitialize has not been called successfully!"); // $ TODO: M2: Better error ID throw PersError(101); } Trace("LMSInitialize succeeded."); return true; } catch(oPersError) { Trace("Failed in LMSInitialize because " + oPersError.description); this.oLastError = oPersError; return false; } } //////////////////////////////////////////////////////////////////////////////////////////////////// // // @doc External // // @jsfunc LMSGetValue | LRN implementation of LMSGetValue, which retrieves the value of the named // personalization property. // // @parm String | strPropertyName | The name of the property to retrieve. // // @rdesc If the property exists, its value is returned as a string. Otherwise, null is returned. // // @ex Usage: | // // Get a value (error checking ommited for clarity) // var strScore = LMSAPI.LMSGetValue("Assessment1_Score"); // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // Note: The current LRN implementation of the LMS API does not support datamodels or // the _children, _count, or _version keywords. // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSGetValue ( strPropertyName ) { try { Trace("Entering LMSGetValue for " + strPropertyName); // Reset the API's last error so the current call starts clean. this.oLastError = null; // Make sure that we were passed a reasonable argument if("string" != typeof(strPropertyName) || "" == strPropertyName) { // $ TODO: M2: Use string replacement tool to replace error IDs too. throw new PersError(201); } // Make sure we are initialized // $ TODO: M2: Assert that null != this.oMainXML instead. if(!this.bInitialized || null == this.oMainXML) { Trace("The LMSAPI has not been initialized!"); throw new PersError(301); } var strScope = null; // If it's one of our global properties, do not attach scope if (strPropertyName.indexOf(gc_strLRNPrefix) != 0) strScope = this.LMSGetValue(gc_strScopeProperty); if (null == strScope) strScope = ""; // Find the value of the current property var strPattern = "/root/" + gc_strPersDBShortName + "[@" + gc_strPropertyPropName + "=\'" + strPropertyName + "\' && @" + gc_strScopePropName + "=\'" + strScope + "\']"; Trace("Using the following XSL pattern: " + strPattern); var oProperty = this.oMainXML.selectSingleNode(strPattern); if(null == oProperty) { // We don't consider this an error. Trace("Could not find the specified property"); return null; } return oProperty.attributes.getNamedItem(gc_strXMLValuePropName).value; Trace("LMSGetValue succeeded."); } catch(e) { if("undefined" == typeof(e.strDetailsType)) { this.oLastError = new PersError(101, gc_strJScriptError, e); } else { this.oLastError = e; } Trace("LMSGetValue failed because " + this.oLastError.description); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSSetValue | LRN implementation of LMSSetValue, which sets the value of the named // personalization property. The change is not commited to the store until // LMSCommit or LMSFinish is called. // // @parm String | strPropertyName | The name of the property to modify. // @parm String | strPropertyValue | The value to set. This must be a number, a boolean, or a // string. We will convert the value to a string before // setting it. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location. // // (error checking omitted for clarity) // LMSAPI.LMSSetValue("LastPageVisited", location.href); // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSApI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSSetValue ( strPropertyName, strPropertyValue ) { Trace("Entering LMSSetValue to set " + strPropertyName + " to " + strPropertyValue); try { // Reset the API's last error so the current call starts clean. this.oLastError = null; // Make sure that we were passed reasonable inputs if("string" != typeof(strPropertyName) || "" == strPropertyName) { Trace("Bad property name"); throw new PersError(201); } if("string" != typeof(strPropertyValue)) { if("number" == typeof(strPropertyValue) || "boolean" == typeof(strPropertyValue)) { Trace("Converting value from " + typeof(strPropertyValue) + " to a string."); strPropertyValue = strPropertyValue.toString(); // $ TODO: M2: Assert that the value is now a string } else if(null != strPropertyValue) { Trace("Bad property value"); throw new PersError(201); } } // Make sure we are initialized // $ TODO: M2: Assert that null != this.oMainXML instead. if(!this.bInitialized || null == this.oMainXML) { Trace("LMSAPI has not been initialized"); throw new PersError(301); } var strScope = null; // If it's one of our global properties, do not attach scope if (strPropertyName.indexOf(gc_strLRNPrefix) != 0) strScope = this.LMSGetValue(gc_strScopeProperty); if (null == strScope) strScope = ""; // If the named property already exists, get a handle to its row in the main XML. var strPattern = "/root/" + gc_strPersDBShortName + "[@" + gc_strPropertyPropName + "=\'" + strPropertyName + "\' && @" + gc_strScopePropName + "=\'" + strScope + "\']"; Trace("Using the following XSL pattern: " + strPattern); var oOldRow = this.oMainXML.selectSingleNode(strPattern); // If a property is being modified or deleted, add the appropriate row to the before // section of the update gram. If the property is being deleted, also delete the old row // from the main XML. if(null != oOldRow) { Trace("Updating an existing row"); oOldRow.attributes.getNamedItem(gc_strXMLValuePropName).value = strPropertyValue; } else { // Create a new after row from scratch. oNewRow = this.oMainXML.createElement(gc_strPersDBShortName); // Set the property and value on the new after row. var oProperty = this.oMainXML.createAttribute(gc_strPropertyPropName); var oPropertyValue = this.oMainXML.createAttribute(gc_strXMLValuePropName); var oScope = this.oMainXML.createAttribute(gc_strScopePropName); oProperty.value = strPropertyName; oPropertyValue.value = strPropertyValue; oScope.value = strScope; oNewRow.attributes.setNamedItem(oProperty); oNewRow.attributes.setNamedItem(oPropertyValue); oNewRow.attributes.setNamedItem(oScope); var oRootNode = this.oMainXML.selectSingleNode("/root"); if(null == oRootNode) { // $ TODO: M2: Assert here. This is unexpected. Trace("Could not find root node in main XML!"); throw new PersError(101); } oRootNode.appendChild(oNewRow); } Trace("LMSSetValue succeeded."); } catch(e) { if("undefined" == e.strDetailsType) { // Return a general error, because we don't know exactly what happened. this.oLastError = new PersError(101, gc_strJScriptError, e); } else { this.oLastError = e; } Trace("LMSSetValue error: " + e.description); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSCommit | LRN implementation of LMSCommit, which saves the current set of // personalization data to the personalization store. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and commit the change. // // (error checking omitted for clarity) // LMSAPI.LMSSetValue("LastPageVisited", location.href); // LMSAPI.LMSCommit(); // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSCommit ( ) { try { Trace("Entering LMSCommit"); // Reset the last error this.oLastError = null; if(null != this.oUserData) { // Try saving to user data Trace("Attempting to save to userData."); this.oUserData.setAttribute(gc_strMainXMLAttribute, this.oMainXML.xml); this.oUserData.save(gc_strUserDataPrefix + this.strSaveName); } } catch(e) { Trace("LMSCommit failed because " + e.description); this.oLastError = new PersError(101, gc_strJScriptError, e); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSFinish | LRN implementation of LMSFinish, which saves the current set of // personalization data to the personalization store and terminates the // connection to the personalization store. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and commit the change. // // (error checking omitted for clarity) // LMSAPI.LMSInitialize(null); // LMSAPI.LMSSetValue("LastPageVisited", location.href); // LMSAPI.LMSFinish(); // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSFinish() { this.oLastError = null; // We don't actually commit the changes until LRNTerminate is called. return; } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSGetLastError | LRN implementation of LMSGetLastError, which returns the ID of the // last error to occur. A return value of zero indicates that the // last operation was successful. Multiple consecutive calls to this // method will return the same value. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and check for errors. // LMSAPI.LMSSetValue("LastPageVisited", location.href); // lErrorNumber = LMSAPI.LMSGetLastError(); // if(0 != lErrorNumber) // { // alert(LMSAPI.LMSGetErrorString(lErrorNumber)); // } // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSGetLastError() { if(null == this.oLastError) { return 0; } return this.oLastError.number; } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSGetErrorString | LRN implementation of LMSGetErrorString, which returns the string // associated with a specific error ID. // // @parm Integer | iErrorNumber | ID of the error for which to return a description. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and check for errors. // LMSAPI.LMSSetValue("LastPageVisited", location.href); // lErrorNumber = LMSAPI.LMSGetLastError(); // if(0 != lErrorNumber) // { // alert(LMSAPI.LMSGetErrorString(lErrorNumber)); // } // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSGetErrorString ( iErrorNumber ) { if(0 == iErrorNumber) { return L_Success_Message; } else { var strErrorMessage = ErrorStringFromID(iErrorNumber); if(null == strErrorMessage) { return ""; } return strErrorMessage; } } //////////////////////////////////////////////////////////////////////////////////////////////////// // @doc External // // @jsfunc LMSGetDiagnostic | LRN implementation of LMSGetErrorString, which returns additional // information regarding a specific error. The LRN implementation // will only return additional information about the last error to // occur. // // @parm Integer | iErrorNumber | ID of the error for which to return a description. Any value // other than null will cause an empty string to be returned. // // @rdesc none // // @ex Usage: | // // Set the last visited page to our current location and check for errors. // LMSAPI.LMSSetValue("LastPageVisited", location.href); // lErrorNumber = LMSAPI.LMSGetLastError(); // if(0 != lErrorNumber) // { // alert(LMSAPI.LMSGetErrorString(lErrorNumber)); // alert(LMSAPI.LMSGetDiagnostic(null)); // } // // @comm // This function should not be called directly. It is meant to be called through an // initialized instance of the LMSImplementation object that is returned by // FindLMSAPI(). // @end // //////////////////////////////////////////////////////////////////////////////////////////////////// function LMSGetDiagnostic ( iErrorNumber ) { try { if(null != iErrorNumber) { return ""; } if(null == this.oLastError || 0 == this.oLastError.number) { return L_Success_Message; } switch(this.oLastError.strDetailsType) { case gc_strParseError: return this.oLastError.oErrorDetails.reason; case gc_strJScriptError: return this.oLastError.oErrorDetails.description; case gc_strMultiStatusError: return this.oLastError.oErrorDetails.xml; case gc_strString: return this.oLastError.oErrorDetails; default: return ""; } } catch(e){} return ""; } // This variable can be used to see if the whole script block has been parsed. var g_bLMSAPIIsLoaded = true;