Java tutorial
/****************************************************************************** ** ** Advanced Distributed Learning Co-Laboratory (ADL Co-Lab) Hub grants you ** ("Licensee") a non-exclusive, royalty free, license to use, modify and ** redistribute this software in source and binary code form, provided that ** i) this copyright notice and license appear on all copies of the software; ** and ii) Licensee does not utilize the software in a manner which is ** disparaging to ADL Co-Lab Hub. ** ** This software is provided "AS IS," without a warranty of any kind. ALL ** EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ** ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE ** OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. ADL Co-Lab Hub AND ITS LICENSORS ** SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF ** USING, MODIFYING OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO ** EVENT WILL ADL Co-Lab Hub OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, ** PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, ** INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE ** THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE ** SOFTWARE, EVEN IF ADL Co-Lab Hub HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH ** DAMAGES. ** ******************************************************************************/ package org.adl.validator.contentpackage; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.StringTokenizer; import org.adl.logging.DetailedLogMessageCollection; import org.adl.parsers.dom.ADLDOMParser; import org.adl.parsers.dom.DOMTreeUtility; import org.adl.util.LogMessage; import org.adl.util.MessageType; import org.adl.util.Messages; import org.adl.util.zip.UnZipHandler; import org.adl.validator.ADLSCORMValidator; import org.adl.validator.IValidator; import org.adl.validator.RulesValidator; import org.adl.validator.sequence.SequenceValidator; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * <strong>Filename: </strong>CPValidator.java <br> * <strong>Description: </strong>The <code>CPValidator</code> determines whether * the content package test subject is conformant with the Content Package * Application Profiles( resource, content aggregation) as defined in the * Content Aggregation Model of the SCORM. The Content Package Validator * performs the following checks: 1) Determines if the required files exist at * the root of the package, 2) Parses the manifest for wellformedness, 3) Parses * the manifest for validation to the controlling documents, 4) Determines * whether or not extensions were used in the manifest, 5) Determines if the * manifest abides to the rules defined for the Content Package Application * Profile rules, and 6) Validates external metadata and scos if the using * system chooses to. The CPValidator inherites from the ADLSCORMValidator to * determine if the imsmanifest is wellformed and valid to the xsd(s). The * CPValidator object validates the content package test subject against the * rules and requirements necessary for meeting each Content Package Application * Profile. <br> * <br> * * @author ADL Technical Team <br> * <br> */ public class CPValidator extends ADLSCORMValidator implements IValidator { private static final long serialVersionUID = 1L; /** * This attribute allows full validation activities (required files check, * schema validation, application profile checks, extension detection) to be * turned off, allowing only wellformedness checks to occur. */ protected boolean mPerformFullValidation; /** * Logger object used for debug logging. */ // private transient Logger mLogger = // Logger.getLogger("org.adl.util.debug.validator"); private static Log mLogger = LogFactory.getLog(CPValidator.class); /** * This attribute serves as the object that contains the information * required for the SCORM Validation checks, containing the DOM of the XML * rules that are neccessary for meeting conformance to each of the * Application Profiles. */ private RulesValidator mRulesValidator; /** * This attribute contains the base directory of where the test subject is * located. It is used by the validator to determine the location of the * package resources, including the manifest, sco's, and/or metadata. */ private String mBaseDirectory; /** * This attribute contains the list of URLs as defined in the XML base * attributes. The XML base attribute specifies a base URI other than the * base URI of the document or external entity. */ private String[][] mXMLBase = new String[3][2]; /** * This attribute contains the list of organization level identifier values * found. This value is tracked to check that the IDREFs point to valid * identifiers. */ private List<String> mOrganizationIdentifierList; /** * This attribute serves as the data structure used to store the Launch Data * information of SCOs and Metadata referenced within the content package. */ private ManifestHandler mManifestHandler; /** * This attribute holds the value containing the environment variable. The * environment variable is set to the using systems install directory. */ private String mEnvironmentVariable; /** * This attribute contains the populated ManifestMap object, containing all * the information needed to validate manifest elements with/without the * existance of (Sub) manifest(s). */ private ManifestMap mManifestInfo; /** * This attribute identifies the manifest that is up for validation during * the application profile checks. */ private String mManifestID; /** * This attribute houses the resource nodes needed to validate that * initialization data is pointing to a sco. */ private List<Node> mResourceNodes; /** * This List will hold all the identifierReferences listed in the entire * manifest. It will be used to determine if all resources have been * referenced for the dangling resource test (excess baggage). */ private List<String> mAllIdrefsList; /** * This attribute houses the idref values that have been found to be valid * references. */ private List<String> mValidIdrefs; /** * This attribute contains a list of the controlling documents that are * required to exist at the root of the content package, including those * controlling documents required by the IMS Manifest and the adlcp:location * metadata. The purpose of this list is to prevent duplication of the * controlling documents. */ private List<String> mRequiredFilesList; /** * This attribute holds the names of all the files listed in the package. It * is used to check against the manifest to determine if there are any files * in the package that are not listed in the manifest */ private List<String> mFileList; /** * This attribute holds the identifier values of the resource elements. It * is used to validate that the dependency element references a resource * identifier within the scope of its manifest only. Referencing resource * identifiers in (sub) manifest elements is not permitted. */ private List<String> mResourceIdentifierList; /** * This List holds all of the Resource Identifiers listed in the manifest. * It will be used to verify that all the resources have been referenced for * the dangling resource test (excess baggage). */ private List<String> mManifestResourceIdentifierList; /** * This table holds a listing of the resource identifiers (as the key) along * with the files that are associated with each. If a resource does not * contain file(s), than no entry is added to the table for that resource. */ private Hashtable<String, List<String>> mResourceTable; /** * Indicates if the xsi:schemaLocation was declared in the XML instance. The * xsi:schemalocation attribute provides a hint to the XML Parser as to * where the controlling documents are located. The xsi:schemalocation * attribute is an optional attribute. */ private boolean mSchemaLocExists; /** * holds all of the hrefs to see if they are duplicated with only changes of * letters in upper or lower case */ private CaseSensitiveCollection mHrefCaseSensitiveList; /** * Default Constructor. Sets the attributes to their initial values. * * @param iEnvironmentVariable * the value specific to the location of the using system's * install. */ public CPValidator(String iEnvironmentVariable) { super("contentpackage"); mBaseDirectory = ""; // Initialize the 2-dimensional array of xml:base attributes mXMLBase[0][0] = "manifest"; mXMLBase[1][0] = "resources"; mXMLBase[2][0] = "resource"; mXMLBase[0][1] = ""; mXMLBase[1][1] = ""; mXMLBase[2][1] = ""; mOrganizationIdentifierList = new ArrayList<String>(); mRulesValidator = new RulesValidator("contentpackage"); mManifestHandler = new ManifestHandler(); mEnvironmentVariable = iEnvironmentVariable; mManifestInfo = new ManifestMap(); mManifestID = ""; mValidIdrefs = new ArrayList<String>(); mFileList = new ArrayList<String>(); mResourceNodes = new ArrayList<Node>(); mResourceIdentifierList = new ArrayList<String>(); mHrefCaseSensitiveList = new CaseSensitiveCollection(); mManifestResourceIdentifierList = new ArrayList<String>(); mRequiredFilesList = new ArrayList<String>(); mAllIdrefsList = new ArrayList<String>(); mManifestID = ""; mResourceTable = new Hashtable<String, List<String>>(); mSchemaLocExists = true; } /** * This method builds the XML base value that is to be pre-appended to the * href attribute values prior to any attempts to located the href values * locations. * * @param iHrefValue * href value to apply xml:base values to * @return String that contains the href value pre-appended with the * xml:base */ private String applyXMLBase(String iHrefValue) { mLogger.debug("mXMLBase[0][1]: " + mXMLBase[0][1]); mLogger.debug("mXMLBase[1][1]: " + mXMLBase[1][1]); mLogger.debug("mXMLBase[2][1]: " + mXMLBase[2][1]); mLogger.debug("href: " + iHrefValue); return mXMLBase[0][1] + mXMLBase[1][1] + mXMLBase[2][1] + iHrefValue; } /** * This method assists with the application profile check for the validation * of the bucket attributes. <br> * * @param iBucketNode * The resources node <br> */ private void checkBucketAttributes(Node iBucketNode) { mLogger.debug("CPValidator checkBucketAttributes"); String msgText; boolean foundValidChar = false; NamedNodeMap attrList = iBucketNode.getAttributes(); int numAttr = attrList.getLength(); Attr currentAttrNode; String currentAttrName; String attributeValue = null; // find the bucketID and bucketType attributes for (int i = 0; i < numAttr; i++) { currentAttrNode = (Attr) attrList.item(i); currentAttrName = currentAttrNode.getLocalName(); // Check bucketID and bucketType Attribute Values for nothing but // white space if (currentAttrName.equals("bucketID") || currentAttrName.equals("bucketType")) { // Find the length of bucket attribute and check if its all // whitespace attributeValue = currentAttrNode.getValue(); for (int j = 0; j < attributeValue.length(); j++) { char tempChar = attributeValue.charAt(j); if (!(Character.isWhitespace(tempChar))) { foundValidChar = true; break; } } if (!foundValidChar) { msgText = "Attribute \"" + currentAttrName + "\" must " + "contain valid characters, all whitespace found."; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } foundValidChar = false; } } } /** * This method assists with the application profile check for the validation * of the bucket attributes. <br> * * @param iResourceNode * The resources node <br> */ private void checkBucketUniqueness(Node iResourceNode) { mLogger.debug("CPValidator checkBucketUniqueness"); String msgText; List<String> idList = new ArrayList<String>(); NodeList childrenOfItem = iResourceNode.getChildNodes(); if (childrenOfItem != null) { Node currentChild; String currentChildName; int len = childrenOfItem.getLength(); for (int k = 0; k < len; k++) { currentChild = childrenOfItem.item(k); currentChildName = currentChild.getLocalName(); if (currentChildName.equals("bucket")) { // Get the bucketID attribute String bucketId = DOMTreeUtility.getAttributeValue(currentChild, "bucketID"); // Check if id already exists in the list: if it does set // the // flag if (idList.contains(bucketId)) { // ERROR, duplicate ID exists in resource msgText = "BucketID \"" + bucketId + "\" must be unqiue" + " for a <resource>."; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } else { idList.add(bucketId); } } } } } /** * This method checks a list of files created from those listed in the * imsmanifest.xml file. As files are physically located they are removed * from this list. After the package has been completely validated this * method checks the list to determine if any files are still there. If they * are still in the list then they exist in the package but are not * referenced in the imsmanifest.xml file. Therefore they are determined to * be extaneous files that do not need to be included in the package. <br> * * @param iApplicationProfileType * if the application type is content aggregation then we have * additional checks to make */ private void checkForExcessBaggage(String iApplicationProfileType) { String msgText = ""; if (mFileList.size() > 0) { // If there is only one file in the list if (mFileList.size() == 1) { msgText = "All files included in the content package should " + "be declared and referenced in the manifest when " + "interchanging packages. The following file is " + "contained in the content package but was not " + "referenced in the manifest. It appears that this " + "file may be extraneous. Appropriate action may " + "need to be taken to correct this problem"; } // If there is more than one file in the list else { msgText = "All files included in the content package should " + "be declared and referenced in the manifest when " + "interchanging packages. The following files are " + "contained in the content package but were not " + "referenced in the manifest. It appears that these " + "files may be extraneous. Appropriate action may " + "need to be taken to correct this problem:"; } mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.WARNING, msgText)); // we've displayed the warning, now list the files creating the // warning for (int i = 0; i < mFileList.size(); i++) { mLogger.debug(mFileList.get(i)); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.OTHER, mFileList.get(i))); } } // end if msgText = ""; mManifestResourceIdentifierList.removeAll(mAllIdrefsList); // check to see if there are any dangling resources in the list if (mManifestResourceIdentifierList.size() > 0 && iApplicationProfileType.equals("contentaggregation")) { if (mManifestResourceIdentifierList.size() == 1) { msgText = "All files included in the content package should " + "be declared and referenced in the manifest when " + "interchanging packages. The following resource is " + "listed in the manifest but was not referenced by " + "an \"item\" or \"dependency\". It appears that this " + "may be a dangling reference. Appropriate action " + "may need to be taken to correct this problem"; } // If there is more than one dangling resource in the list else { msgText = "All files included in the content package should " + "be declared and referenced in the manifest when " + "interchanging packages. The following resources " + "are listed in the manifest but were not " + "referenced by any \"item\" or \"dependency\". It " + "appears that these may be dangling references. " + "Appropriate action may need to be taken to correct " + "this problem:"; } mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.WARNING, msgText)); // we've displayed the warning, now list the files creating the // warning for (int j = 0; j < mManifestResourceIdentifierList.size(); j++) { mLogger.debug(mManifestResourceIdentifierList.get(j)); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.OTHER, mManifestResourceIdentifierList.get(j))); } } // end if }// end checkForExcessBaggage() /** * This method assists with the application profile check for validation * that a sco resource is referenced by an item identifierref. * * @param idrefValue * - the idrentifierref value to be matched to a sco resource. * @return boolean - result of the reference to sco check. */ private boolean checkForReferenceToSco(String idrefValue) { mLogger.debug("CPValidator checkForReferenceToSco"); mLogger.debug("Input Identifierref: " + idrefValue); boolean result = true; int len = mResourceNodes.size(); String id; String type; String msgText; for (int i = 0; i < len; i++) { Node currentResource = mResourceNodes.get(i); id = DOMTreeUtility.getAttributeValue(currentResource, "identifier"); mLogger.debug("Identifier of <resource> #" + i + " is: " + id); if (id.equals(idrefValue)) { // we have a matching reference // now check scormType and error if not sco type = DOMTreeUtility.getAttributeValue(currentResource, "scormType"); mLogger.debug("SCORM Type of <resource> #" + i + " is: " + type); if (!type.equalsIgnoreCase("sco")) { result = false; msgText = Messages.getString("CPValidator.452", idrefValue); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } } return result; } /** * This method performs the check that the required files - imsmanifest and * all supporting controlling documents - exist at the root of the package. * This method throws an error if the imsmanifest.xml, ADL CP xsd, or IMS CP * controlling documents are not detected at the root of the content package * test subject. Warnings are thrown if the IMS SS or IEEE LOM Custom * controlling documents are not detected at the root of the content package * test subject. At this time, the controlling documents are hardcoded. * Future enhancements include the detection of the controlling documents * based off of the root manifest declaration. * * @param iDirectory * The directory to check to see if the required files are * present. * @return boolean - true if the required files are present, false otherwise */ private boolean checkForRequiredFiles(String iDirectory, List<String> iDeclaredNamespaces) { mLogger.debug("CPValidator checkForRequiredFiles()"); boolean result = true; ManifestHandler mdHandler = new ManifestHandler(); String schemaLocations = ""; mLogger.debug("XMLOTHER HR: "); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.XMLOTHER, "HR")); mLogger.debug("INFO: Searching for Files Required For XML Parsing"); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, Messages.getString("CPValidator.63"))); // All control files also are required to be at the root of the package. // This includes any and all control files needed to validate XML // instances // including those referenced by the <adlcp:location> element. // first retrieve all the controlling documents needed by the metadata // referenced by the <adlcp:location> List<String> locationMD = mdHandler.getLocationMD(super.getDocument().getDocumentElement()); int locationMDSize = locationMD.size(); // loop through each MD to retrieve schemaLoc if (!locationMD.isEmpty()) { // Create an adldomparser object ADLDOMParser adldomparser = new ADLDOMParser(); for (int i = 0; i < locationMDSize; i++) { String currentMetadataData = locationMD.get(i); // Have to parser the adlcp:location metadata to determine // required controlling files String metadataFile = iDirectory; metadataFile = metadataFile.replace('\\', '/'); metadataFile = metadataFile + currentMetadataData; // retrieve adldomparser attribute values to determine if the // adlcplocation metadata was wellfomed. We cannot determine // the required controlling documents without wellformed XML adldomparser.parseForWellformedness(metadataFile, false, true); if (!adldomparser.getIsWellformed()) { result = false; DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.64", currentMetadataData, currentMetadataData))); } } // need to add the controlling docs needed by the adlcp:location schemaLocations += adldomparser.getSchemaLocation(); } // need to append controlling docs needed by IMS Manifest String allSchemaLocations = schemaLocations + super.getSchemaLocation(); // call a method to parse the controlling document names including those // needed by the IMS Manifest and the <adlcp:location> and detect // if they exist at the root int requiredFileResult = findAndLocateRequiredFiles(allSchemaLocations, iDirectory, iDeclaredNamespaces); // Determines if default checking is needed if (requiredFileResult == 0) { result = false; } else if (requiredFileResult == 1) { result = true && result; } DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.XMLOTHER, "HR")); mLogger.debug("returning the following result " + result); mLogger.debug("CPValidator checkForRequiredFiles()"); // Set the ADLSCORMValidator protected attribute for summary log info super.setIsRequiredFiles(result); return result; } /** * This method checks the values of xml:base, href, and adlcp:location to * ensure that they DO NOT begin with a slash '/' It also checks xml:base * for the required trailing slash '/' * * @param iName * Contains the name of the attribute/element being passed in * @param iValue * Contains the value of the attribute/element being passed in * @return boolean: True if xml:base contains a trailing slash '/' and/or if * href, xml:base, and location DO NOT contain leading slashes. */ private boolean checkForSlashes(String iName, String iValue) { mLogger.debug("CPValidator checkForSlashes()"); mLogger.debug("Name: " + iName); mLogger.debug("Value: " + iValue); String msgText = ""; boolean result = true; // Getting the last character in the string char tempChar = iValue.charAt(iValue.length() - 1); // if the name is xml:base, check to make sure the last character is "\" if (iName.equals("xml:base")) { if (tempChar != '/') { msgText = Messages.getString("CPValidator.624", iName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result &= false; } } // gets the first character in the string tempChar = iValue.charAt(0); // make sure the first character IS NOT a "\" if (tempChar == File.separatorChar || tempChar == '/') { msgText = Messages.getString("CPValidator.627", iName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } return result; } // end checkForSlashes() /** * This method assists with the application profile check for the validation * of the href attribute(s). This method attempts to verify that the href * values can be detected. * * @param iURIString * The URI value of the href attribute * @return boolean - result of the href check. True if the href checks * passed, false otherwise. */ private boolean checkHref(String iURIString) { mLogger.debug("CPValidator checkHref()"); mLogger.debug("iURISting is " + iURIString); boolean result = true; String msgText = ""; if (!(iURIString.equals(""))) { // check for a valid protocol if (((iURIString.length() > 4) && (iURIString.substring(0, 5).equals("http:"))) || ((iURIString.length() > 5) && (iURIString.substring(0, 6).equals("https:"))) || ((iURIString.length() > 3) && (iURIString.substring(0, 4).equals("ftp:"))) || ((iURIString.length() > 4) && (iURIString.substring(0, 5).equals("ftps:")))) { // This is an external SCO try { URL url = new URL(iURIString); URLConnection urlConn = url.openConnection(); HttpURLConnection httpUrlConn = (HttpURLConnection) urlConn; int code = httpUrlConn.getResponseCode(); // try to access the address if (code == 200) { msgText = Messages.getString("CPValidator.502", iURIString); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.505", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } catch (MalformedURLException mfue) { mLogger.debug( "MalformedURLException thrown when creating " + "URL with \"" + iURIString + "\""); msgText = Messages.getString("CPValidator.514", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } catch (IOException ioe) { mLogger.debug("IOException thrown when opening a connection " + "to \"" + iURIString + "\""); msgText = Messages.getString("CPValidator.520", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } else if (iURIString.substring(0, 5).equals("file:")) { // This is the local file system msgText = Messages.getString("CPValidator.524", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else { // Check the local URLs result = checkLocalURL(iURIString) && result; } } return result; } /** * This method assists with the application profile check for the validation * with of items. It first checks the identifierref attribute of an item for * existance on non-leaf items. It than checks the title multiplcity, as a * title is not permitted on an item that references a (sub)manifest. * * @param iOrgNode * The organizations node * @param iManifestInfo * - the populated ManifestMap object * @return boolean - result of the organization multiplicity check. True if * the href checks passed, false otherwise. */ private boolean checkItem(Node iOrgNode, ManifestMap iManifestInfo) { mLogger.debug("CPValidator checkItem"); boolean result = true; result = checkItemIdentifierRef(iOrgNode) && result; result = checkItemChildMultiplicity(iOrgNode, iManifestInfo) && result; return result; } /** * This method validates the multiplicity of the children of the item * element. The title element is required to be present. * * @param iNode * - the organization element * @param iManifestInfo * - the populated ManifestMap object * @return boolean - result of the title multiplicity check. True implies * that the title multiplicity was properly adhered to, false * implies otherwise. */ private boolean checkItemChildMultiplicity(Node iNode, ManifestMap iManifestInfo) { mLogger.debug("CPValidator checkItemChildMultiplicity"); String msgText = ""; boolean result = true; String iNodeName = iNode.getLocalName(); if (iNodeName.equals("organization")) { NodeList iNodeChildren = iNode.getChildNodes(); int iNodeChildSize = iNodeChildren.getLength(); Node currentNode; String currentName; for (int j = 0; j < iNodeChildSize; j++) { currentNode = iNodeChildren.item(j); currentName = currentNode.getLocalName(); if (currentName.equals("item")) { // search for item element and recurse result = checkItemChildMultiplicity(currentNode, iManifestInfo) && result; } } } if (iNodeName.equals("item")) { NodeList itemChild = iNode.getChildNodes(); int itemSize = itemChild.getLength(); String currentItemChildName = ""; boolean titleFound = false; for (int v = 0; v < itemSize; v++) { Node currentItemChild = itemChild.item(v); currentItemChildName = currentItemChild.getLocalName(); if (currentItemChildName != null) { if (currentItemChildName.equals("title")) { titleFound = true; } else if (currentItemChildName.equals("item")) { result = checkItemChildMultiplicity(currentItemChild, iManifestInfo) && result; } } } // title is mandatory if (!titleFound) { msgText = Messages.getString("CPValidator.287", "title"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } return result; } /** * This method checks the item to ensure that the identifierref attribute is * not used on a non-leaf item. This method also checks to ensure that a * leaf item shall reference a resource. A leaf item fails if it contains no * identifierref attribute at all, or if it contains an identifierref * attribute that is set to an empty string. * * @param iOrgNode * - the organization node containing the item element(s) * @return boolean - result of the check for the item identifierref * attribute True if the identifierref passes, false otherwise. */ private boolean checkItemIdentifierRef(Node iOrgNode) { mLogger.debug("CPValidator checkItemIdentifierRef"); String msgText = ""; NodeList orgChildren = iOrgNode.getChildNodes(); int orgChildSize = orgChildren.getLength(); Node currentNode; String currentName; boolean result = true; for (int j = 0; j < orgChildSize; j++) { currentNode = orgChildren.item(j); currentName = currentNode.getLocalName(); if (currentName.equals("item")) { NodeList itemChildren = currentNode.getChildNodes(); int itemChildrenSize = itemChildren.getLength(); boolean itemHasItemChildren = false; for (int k = 0; k < itemChildrenSize; k++) { // see if we have a child item of item // if so, this signals that the currentNode is not a leaf // and // should not have an identifierref Node currentItemChild = itemChildren.item(k); String currentItemChildName = currentItemChild.getLocalName(); if (currentItemChildName != null) { if (currentItemChildName.equals("item")) { NamedNodeMap attrList = currentNode.getAttributes(); int numAttr = attrList.getLength(); Attr currentAttrNode = null; String currentNodeName = ""; for (int i = 0; i < numAttr; i++) { currentAttrNode = (Attr) attrList.item(i); currentNodeName = currentAttrNode.getLocalName(); if (currentNodeName.equals("identifierref")) { result = result && false; msgText = Messages.getString("CPValidator.461"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } // end if ( // currentNodeName.equals("identifierref") ) } // end for // set the flag that signals that the item is // NOT a leaf // item itemHasItemChildren = true; } // end if ( currentItemChildName.equals("item") ) } } // need to check if we are dealing with a leaf item if (!itemHasItemChildren) { // Verify that a leaf item references a resource Attr identifierRef = DOMTreeUtility.getAttribute(currentNode, "identifierref"); if (identifierRef == null) { // ERROR, must reference a resource therefore it must // contain // an identifierref attribute result = result && false; msgText = Messages.getString("CPValidator.464"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } else { String identifierRefValue = identifierRef.getValue(); if (identifierRefValue.equals("")) { // ERROR, must reference a resource therefore it // cannot // contain an identifierref attibute that is set to // an // empty string result = result && false; msgText = Messages.getString("CPValidator.467"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } } } } return result; } /** * * @param iURIString * the URI passed in * @return boolean - result of the local URL check. True if the local checks * passed, false otherwise. */ private boolean checkLocalURL(String iURIString) { boolean result = true; String msgText = ""; if (iURIString.charAt(0) == '/') { // This is referencing the users home directory msgText = Messages.getString("CPValidator.527", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else { String absolutePath = getBaseDirectory() + iURIString; mLogger.debug("Absolute path is " + absolutePath); // strip off the query string and parameters int queryIndex = absolutePath.indexOf('?'); if (queryIndex > 0) { absolutePath = absolutePath.substring(0, queryIndex); } // strip off the fragment string and parameters int fragmentIndex = absolutePath.indexOf('#'); if (fragmentIndex > 0) { absolutePath = absolutePath.substring(0, fragmentIndex); } // after the parameters have been stripped off we want to check to // see if // this URI is different from any others by case (upper/lower) // if the absolutePath is in the list then we fall through, if it // isn't then // we check to see if it is in there but with a difference in case if (!mHrefCaseSensitiveList.contains(absolutePath)) { // the href is not in the list in this case, now check to see // if it is in there in a different case if (mHrefCaseSensitiveList.containsIgnoreCase(absolutePath)) { // raise a warning because the href is in the list but with // a difference in case DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.WARNING, Messages.getString("CPValidator.528", iURIString))); } else { // it's not in the list at all, so add it for future // reference mHrefCaseSensitiveList.add(absolutePath); } } // decode any encrypted URL syntax try { absolutePath = URLDecoder.decode(absolutePath, "UTF-8"); } catch (UnsupportedEncodingException uee) { mLogger.error("UnsupportedEncodingException thrown while " + "decoding the file path."); uee.printStackTrace(); } // try to access the file try { File fileToFind = new File(absolutePath); if (fileToFind.isFile()) { msgText = Messages.getString("CPValidator.534", iURIString); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); // This file has been physically located, remove from the // list...it's NOT excess baggage String tempStr = fileToFind.getPath(); tempStr = tempStr.substring(tempStr.indexOf("PackageImport\\") + 14, tempStr.length()); // we have to switch the direction of the slashes to // correspond with those in the // mFileList (had to originally change those because // backwards slashes can't exist // in the manifest tempStr = tempStr.replace('\\', '/'); mFileList.remove(tempStr); } else { msgText = Messages.getString("CPValidator.537", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } catch (NullPointerException npe) { mLogger.error("NullPointerException thrown when accessing " + absolutePath); msgText = Messages.getString("CPValidator.537", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } catch (SecurityException se) { mLogger.error("SecurityException thrown when accessing " + absolutePath); msgText = Messages.getString("CPValidator.537", iURIString); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } return result; } /** * This method performs the application profile checks for the * adlcp:objectivesGlobalToSystem attribute. The application profile checks * include verifying that the attribute belongs to the IMS namespace and * that it exists as an attribute of the IMS organization element only. * * @param iCurrentAttribute * the objectivesGlobalToSystem attribute to be tested * @param iParentNode * the parent element that the attribute belongs to. * @return boolean True implies that the application profile checks passed; * false implies that they did not */ private boolean checkObjGlobalToSystemReq(Attr iCurrentAttribute, Node iParentNode) { boolean result = false; if (DOMTreeUtility.isAppropriateElement(iParentNode, "organization", "http://www.imsglobal.org/xsd/imscp_v1p1")) { result = true; } else { String msgText = Messages.getString("CPValidator.107", iCurrentAttribute.getLocalName(), "organization"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); } return result; } /** * This method assists with the application profile check for the validation * of the resource attributes. * * @param iResourceNode * The <resources> element * @param iAttrList * - The list of resource attributes. * @return boolean - result of the overall check. True if the checks passed, * false otherwise. */ private boolean checkResourceAttributes(Node iResourceNode, NamedNodeMap iAttrList) { mLogger.debug("CPValidator checkResourceAttributes"); int idMultiplicityUsed = -1; int typeMultiplicityUsed = -1; int scormMultiplicityUsed = -1; int hrefMultiplicityUsed = -1; String msgText; boolean result = true; // check for mandatory attributes idMultiplicityUsed = getMultiplicityUsed(iAttrList, "identifier"); if (idMultiplicityUsed < 1) { msgText = Messages.getString("CPValidator.198", "identifier"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } typeMultiplicityUsed = getMultiplicityUsed(iAttrList, "type"); if (typeMultiplicityUsed < 1) { msgText = Messages.getString("CPValidator.198", "type"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } scormMultiplicityUsed = getMultiplicityUsed(iAttrList, "scormType"); if (scormMultiplicityUsed < 1) { msgText = Messages.getString("CPValidator.198", "scormType"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // special rules only apply to content aggregation application profile if (mRulesValidator.getApplicationProfile().equals("contentaggregation")) { // special checks to be enforced when an <item> references a // <resource> // retrieve resource.identifier value and compare to idref values in // the valid mValidIdrefs List. If a match is found, it is assumed // that the special checks should be enforced. String resourceID = DOMTreeUtility.getAttributeValue(iResourceNode, "identifier"); boolean referencesAResource = false; if (!resourceID.equals("")) { // loop through mValidIdrefs to find a matching reference for (int i = 0; i < mValidIdrefs.size(); i++) { String currentIdref = mValidIdrefs.get(i); if (resourceID.equals(currentIdref)) { referencesAResource = true; } } if (referencesAResource) { // (1) href is mandatory when referenced by an item hrefMultiplicityUsed = getMultiplicityUsed(iAttrList, "href"); if (hrefMultiplicityUsed < 1) { msgText = Messages.getString("CPValidator.431"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // (2) type attribute shall be set to "webcontent" when // referenced by an item String typeValue = DOMTreeUtility.getAttributeValue(iResourceNode, "type"); if (!typeValue.equals("webcontent")) { msgText = Messages.getString("CPValidator.2"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } } } return result; } /** * This method checks that if the <resource> is local to the content * package, then a <file> element is required that represents the * <resource> itself. The href attribute of the <file> element * shall be identical to the href attribute of the <resource> * * @param iResourceNode * - The <resource> element * @return boolean - Result of the overall check. True if the checks passed, * false otherwise. */ private boolean checkResourceFileHref(Node iResourceNode) { boolean result = true; String msgText = ""; // retrive the href and apply xml:base String resourceHref = DOMTreeUtility.getAttributeValue(iResourceNode, "href"); // only continue check if the resourceHref exists if (!resourceHref.equals("")) { // remove all parameters from the end of the href, if they exist // first deal with ? int questionMarkIndex = StringUtils.indexOf(resourceHref, '?'); if (questionMarkIndex > 0) { resourceHref = StringUtils.substring(resourceHref, 0, questionMarkIndex); } // also deal with # int poundIndex = StringUtils.indexOf(resourceHref, '#'); if (poundIndex > 0) { resourceHref = StringUtils.substring(resourceHref, 0, poundIndex); } // we must manually retrieve the resource xml:base attribute value // the applyXMLBase is not aware of it at this time of processing String resourceXMLBase = DOMTreeUtility.getAttributeValue(iResourceNode, "base"); // resourceID used for logging error messages String resourceID = DOMTreeUtility.getAttributeValue(iResourceNode, "identifier"); mLogger.debug("Checking for this resource now " + resourceID); // apply XML Base to the href if it exists resourceHref = mXMLBase[0][1] + mXMLBase[1][1] + resourceXMLBase + resourceHref; // only need to perform this check on local resource if (!StringUtils.startsWith(resourceHref, "http:") && !StringUtils.startsWith(resourceHref, "https:") && !StringUtils.startsWith(resourceHref, "ftp:") && !StringUtils.startsWith(resourceHref, "ftps:")) { // we have a resource that is local to the package, must have a // file // href that is identifical to it List<String> files = mResourceTable.get(resourceID); if (files != null) { if (!files.contains(resourceHref)) { // the resourceHref does not contain a matching file // href when // it should result = false; msgText = Messages.getString("CPValidator.628", resourceID); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } else { // there are no files that exist for the local resource result = false; msgText = Messages.getString("CPValidator.628", resourceID); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } } return result; } /** * This method performs the application profile checks for the * adlcp:scormType attribute. The application profile checks include * verifying that the attribute belongs to the ADL CP namespace and that it * exists as an attribute of the IMS resource element only. * * @param iCurrentAttribute * the scormType attribute to be tested * @param iParentNode * the parent element that the attribute belongs to. * @return boolean True implies that the application profile checks passed; * false implies that they did not */ private boolean checkSCORMTypeReq(Attr iCurrentAttribute, Node iParentNode) { boolean result = false; if (DOMTreeUtility.isAppropriateElement(iParentNode, "resource", "http://www.imsglobal.org/xsd/imscp_v1p1")) { result = true; } else { String msgText = Messages.getString("CPValidator.107", iCurrentAttribute.getLocalName(), "resource"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); } return result; } /** * This method assists with the application profile check for the validation * of the bucket attributes. <br> * * @param iSizeNode * The resources node <br> */ private void checkSizeAttributes(Node iSizeNode) { mLogger.debug("CPValidator checkSizeAttributes"); String msgText; NamedNodeMap attrList = iSizeNode.getAttributes(); int numAttr = attrList.getLength(); Attr currentAttrNode; String currentAttrName; String attributeValue = null; int minVal = 0; // find the minimum and requested attributes for (int i = 0; i < numAttr; i++) { currentAttrNode = (Attr) attrList.item(i); currentAttrName = currentAttrNode.getLocalName(); // Check bucketID and bucketType Attribute Values for even numbers if (currentAttrName.equals("minimum") || currentAttrName.equals("requested")) { // Get the value and check if it is a valid even integer attributeValue = currentAttrNode.getValue(); // Assume the value is valid boolean valid = true; if (attributeValue == null) { // A null value can never be valid valid = false; } try { int value = Integer.parseInt(attributeValue, 10); if (value < minVal || (value % 2) != 0) { valid = false; } } catch (Exception e) { valid = false; } if (!valid) { msgText = "Size Attribute \"" + currentAttrName + "\" must " + "contain a valid even integer value."; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } } } /** * This method assists with the application profile check of the smallest * permitted maximums. The smallest permitted maximum value of an element * describes the maximum number of characters that a system that is going to * process that data at a minimum must support. * * @param iElementName * Name of the element being checked for spm * @param iElementValue * value being checked for smp * @param iSPMRule * value allowed for spm ( value retrieved from rules ) * @param iAmAnAttribute * flags determines if its an attribute (true), or an element * that is being validated for valid vocabulary tokens. * @return - boolean result of spm check. True if the spm checks passed, * false otherwise. */ private boolean checkSPMConformance(String iElementName, String iElementValue, int iSPMRule, boolean iAmAnAttribute) { boolean result = true; String msgText = ""; int elementValueLength = iElementValue.length(); if (iSPMRule != -1) { if (elementValueLength > iSPMRule) { if (iAmAnAttribute) { if (iElementName.equals("base")) { msgText = Messages.getString("CPValidator.552", "xml:base", iSPMRule); } else if (iElementName.equals("href") && doesXMLBaseExist()) { String replace = Integer.toString(iSPMRule); msgText = Messages.getString("CPValidator.553", "xml:base", "href", replace); } else { msgText = Messages.getString("CPValidator.552", iElementName, iSPMRule); } } else { msgText = Messages.getString("CPValidator.555", iElementName, iSPMRule); } mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.WARNING, msgText)); } else if (elementValueLength < 1) { if (iAmAnAttribute) { msgText = Messages.getString("CPValidator.559", iElementName); } else { msgText = Messages.getString("CPValidator.561", iElementName); } mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else { if (iAmAnAttribute) { if (iElementName.equals("base")) { msgText = Messages.getString("CPValidator.564", "xml:base"); } else { msgText = Messages.getString("CPValidator.564", iElementName); } } else { msgText = Messages.getString("CPValidator.566", iElementName); } mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.PASSED, msgText)); } } else if (elementValueLength < 1) { if (iAmAnAttribute) { msgText = Messages.getString("CPValidator.559", iElementName); } else { msgText = Messages.getString("CPValidator.561", iElementName); } mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else { if (iAmAnAttribute) { msgText = Messages.getString("CPValidator.564", iElementName); if (iElementName.equals("base")) { msgText = Messages.getString("CPValidator.564", "xml:base"); } else if (iElementName.equals("href")) { msgText = Messages.getString("CPValidator.565", "xml:base", "href"); } else { msgText = Messages.getString("CPValidator.564", iElementName); } } else { msgText = Messages.getString("CPValidator.566", iElementName); } mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.PASSED, msgText)); } return result; } /** * This method performs the validation on the imsmanifest.xml file - * including wellformedness and validation to the schema checks. This method * is called when the validate method is spanned. * * @param iManifestFileName * - The URI location of the manifest that is to be parsed for * wellformedness and validation to the schema(s). * @return - boolean result of the parse. True implies that the manifest was * wellformed and valid to the schema, false implies otherwise. */ private boolean checkValidityToSchema(String iManifestFileName) { mLogger.debug("CPValidator checkValidityToSchema()"); boolean manifestResult = true; // Send imsmanifest.xml for wellformedness and validation parse super.setPerformFullValidation(mPerformFullValidation); super.performValidatorParse(iManifestFileName); boolean isValid = super.getIsValidToSchema(); boolean isWellformed = super.getIsWellformed(); if (mPerformFullValidation) { if (isValid) { mLogger.debug("PASSED: " + "The manifest instance is valid to the controlling documents"); } else { mLogger.debug("FAILED: " + "The manifest instance is not valid to the controlling documents"); } manifestResult = isWellformed && isValid; } else { manifestResult = isWellformed; } mLogger.debug("CPValidator checkValidityToSchema()"); return manifestResult; } /** * This method assists with the application profile check for valid * vocabularies. The vocabulary value is compared to those defined by the * application profile rules. It is assumed that only 1 vocabulary token may * exist for an element/attribute * * @param iName * Name of the element/attribute being checked for valid * vocabulary. * @param iValue * Vocabulary string value that exists for the element/attribute * in the test subject * @param iVocabValues * List containing a list of the valid vocabulary values for * the element/attribute. * @param iAmAnAttribute * Flags determines if its an attribute (true), or an element * that is being validated for valid vocabulary tokens. * @return boolean - true if the value is a valid vocab token, false * otherwise */ private boolean checkVocabulary(String iName, String iValue, List<String> iVocabValues, boolean iAmAnAttribute) { mLogger.debug("CPValidator checkVocabulary()"); boolean result = false; String msgText; // loop through the valid vocabulary List to see if the // attribute value matches a valid token for (int i = 0; i < iVocabValues.size(); i++) { if (iValue.equals(iVocabValues.get(i))) { result = true; // special warning produced if we find 2nd Edition scorm token if (iName.equals("schemaversion")) { if (iVocabValues.get(i).equals("CAM 1.3")) { msgText = Messages.getString("CPValidator.630", "CAM 1.3", "2004 3rd Edition"); mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.WARNING, msgText)); } } } } if (result) { if (iAmAnAttribute) { msgText = Messages.getString("CPValidator.581", iValue, iName); } else { msgText = Messages.getString("CPValidator.584", iValue, iName); } mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { if (iAmAnAttribute) { msgText = Messages.getString("CPValidator.588", iValue, iName); } else { msgText = Messages.getString("CPValidator.592", iValue, iName); } mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, msgText)); } mLogger.debug("CPValidator checkVocabulary()"); return result; } /** * This method performs the validation on the imsmanifest.xml file - * including wellformedness. This method is called when the * CPValidator.validate method is spanned. When we complete this method, we * will have a wellformed document that has been stripped of extenions * attributes/elements, comments, and whitespace. * * @param iManifestFileName * - The URI location of the manifest that is to be parsed for * wellformedness and validation to the schema(s). * @return - boolean result of the parse. True implies that the manifest was * wellformed and valid to the schema, false implies otherwise. */ private boolean checkWellformedness(String iManifestFileName) { mLogger.debug("CPValidator checkWellformedness()"); boolean wellnessResult = true; // Send imsmanifest for wellformedness and validation parse super.setPerformFullValidation(false); super.performValidatorParse(iManifestFileName); boolean isWellformed = super.getIsWellformed(); wellnessResult = isWellformed; mLogger.debug("CPValidator checkWellformedness"); return wellnessResult; } /** * This method performs the meat of the application profile checks. The * application profiles are described in XML format. Each application * profile has its own XML representation. These XML rules are parsed in * order to form a document object. The test subject manifest is also * available in document format. This method compares the manifest to the * rules described in the XML dom. This recursive method is driven by the * test subject dom. * * @param iTestSubjectNode * Test Subject DOM * @param iPath * Path of the rule to compare to * @return - boolean result of the checks performed. True if the check was * conformant, false otherwise. */ private boolean compareToRules(Node iTestSubjectNode, String iPath) { // is there anything to do? if (iTestSubjectNode == null) return false; mLogger.debug("CPValidator compareToRules"); mLogger.debug("Node: " + iTestSubjectNode.getLocalName()); mLogger.debug("Namespace: " + iTestSubjectNode.getNamespaceURI()); mLogger.debug("Path: " + iPath); boolean result = true; String msgText = ""; // Determine which type of DOM Tree Node we are dealing with switch (iTestSubjectNode.getNodeType()) { case Node.PROCESSING_INSTRUCTION_NODE: { // Skip any processing instructions, nothing for us to do break; } case Node.DOCUMENT_NODE: { // Found the root document node Node rootNode = ((Document) iTestSubjectNode).getDocumentElement(); String rootNodeName = rootNode.getLocalName(); mLogger.debug("DOCUMENT_NODE found"); mLogger.debug("Namespace: " + rootNode.getNamespaceURI()); mLogger.debug("Node Name: " + rootNodeName); mLogger.debug("INFO: Testing element <" + rootNodeName + "> for minimum conformance"); DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.INFO, Messages.getString("CPValidator.131", rootNodeName))); mLogger.debug("PASSED: Multiplicity for element <" + rootNodeName + "> has been verified"); DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.PASSED, Messages.getString("CPValidator.135", rootNodeName))); result = compareToRules(rootNode, "") && result; break; } case Node.ELEMENT_NODE: { // Found an Element Node String parentNodeName = iTestSubjectNode.getLocalName(); if (parentNodeName.equals("manifest")) { // retrieve resources nodes for sco reference validation Node resourcesNode = DOMTreeUtility.getNode(iTestSubjectNode, "resources"); if (resourcesNode != null) { // retrieve resource nodes for sco reference validation mResourceNodes = DOMTreeUtility.getNodes(resourcesNode, "resource"); // Must also track resource identifier values for // dependency identifierref scope validation trackResourceIdentifiers(resourcesNode); } } String dataType = null; int multiplicityUsed = -1; int minRule = -1; int maxRule = -1; int spmRule = -1; mLogger.debug("Looping through attributes for the input " + "element <" + parentNodeName + ">"); // Look for the attributes of this element NamedNodeMap attrList = iTestSubjectNode.getAttributes(); int numAttr = attrList.getLength(); mLogger.debug("There are " + numAttr + " attributes of <" + parentNodeName + "> elememt to test"); Attr currentAttrNode = null; String currentNodeName = ""; String attributeValue = ""; // Loop throught attributes for (int i = 0; i < numAttr; i++) { currentAttrNode = (Attr) attrList.item(i); currentNodeName = currentAttrNode.getLocalName(); mLogger.debug("Processing the [" + currentAttrNode.getNamespaceURI() + "] " + currentNodeName + " attribute of the <" + parentNodeName + "> element."); // If the current attribute is persistState then additional // checks may be necessary if (currentNodeName.equalsIgnoreCase("persistState")) { // we must fail. SCORM 3rd Edition Addendum has removed the // persistState attribute from the adlcp namespaced schema msgText = Messages.getString("CPValidator.274", currentNodeName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // If the current attribute is scormType then additional // checks may be necessary if (currentNodeName.equalsIgnoreCase("scormType")) { // Application Profile Check: Check to make sure that the // adlcp:scormType attribute can only appear on an // <imscp:resource> element result = checkSCORMTypeReq(currentAttrNode, iTestSubjectNode) && result; } // If the current attribute is objectivesGlobalToSystem then // additional checks may be necessary if (currentNodeName.equalsIgnoreCase("objectivesGlobalToSystem")) { // Application Profile Check: Check that the // adlseq:objectivesGlobalToSystem attribute can only appear // on an <imscp:organization> element. result = checkObjGlobalToSystemReq(currentAttrNode, iTestSubjectNode) && result; } // Retrieve the application profile rules only if the the // current // attribute being processed has SCORM application profile // requirements mLogger.debug("Additional checks needed for attribute [" + currentNodeName + "].\r\n"); // Retreive the data type rules dataType = mRulesValidator.getRuleValue(parentNodeName, iPath, "datatype", currentNodeName); // If the data type rules are for an xml:base, then there is // more processing that needs to take place. if (dataType.equalsIgnoreCase("xmlbase")) { // This is a xml:base data type msgText = Messages.getString("CPValidator.164", currentNodeName); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); multiplicityUsed = getMultiplicityUsed(attrList, currentNodeName); // We will assume that no attribute can exist more than // once (ever). According to W3C. Therefore, min and max // rules must exist. // Get the min rule and convert to an int minRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "min", currentNodeName)); // Get the max rule and convert to an int maxRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "max", currentNodeName)); if ((minRule != -1) || (maxRule != -1)) { if (multiplicityUsed >= minRule && multiplicityUsed <= maxRule) { msgText = Messages.getString("CPValidator.169", currentNodeName); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.175", currentNodeName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // mult used >= minRule && mult used <= maxRule } // end minRule != -1, maxRule != -1 // Get the spm rule and convert to an int spmRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "spm", currentNodeName)); attributeValue = currentAttrNode.getValue(); // Check the attributes for smallest permitted maximum(spm) // conformance. result = checkSPMConformance(currentNodeName, attributeValue, spmRule, true) && result; // Check to make sure slashes are correct result = checkForSlashes("xml:base", attributeValue) && result; if (parentNodeName.equals("manifest")) { mXMLBase[0][1] = attributeValue; mLogger.debug(" XML:base found in manifest, value is " + attributeValue); } else if (parentNodeName.equals("resources")) { mXMLBase[1][1] = attributeValue; mLogger.debug(" XML:base found in resources, value is " + attributeValue); } else if (parentNodeName.equals("resource")) { mXMLBase[2][1] = attributeValue; mLogger.debug(" XML:base found in resource, value is " + attributeValue); } } // end if xml:base } // end looping over set of attributes for the element // If we are processing an <imscp:manifest> element, then there // are special cases application profile checks needed. if (parentNodeName.equalsIgnoreCase("manifest")) { mLogger.debug("Manifest node, additional check's being done."); mLogger.debug("Determining how many times the " + "identifier attribute is present."); multiplicityUsed = getMultiplicityUsed(attrList, "identifier"); if (multiplicityUsed < 1) { mLogger.debug("FAILED: Mandatory attribute \"identifier\"" + " could not be found"); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.198", "identifier"))); result = false; } } else if (parentNodeName.equalsIgnoreCase("organizations") && (mRulesValidator.getApplicationProfile()).equals("contentaggregation")) { // multiple <organization> elements exist, but there is no // default attribute. // not a conformance check, warning only multiplicityUsed = getMultiplicityUsed(attrList, "default"); if (multiplicityUsed < 1) { mLogger.debug("ERROR: Mandatory attribute \"default\" " + "could not be found"); DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.198", "default"))); result = false; } } else if (parentNodeName.equalsIgnoreCase("organization") && (mRulesValidator.getApplicationProfile()).equals("contentaggregation")) { multiplicityUsed = getMultiplicityUsed(attrList, "identifier"); if (multiplicityUsed < 1) { mLogger.debug("FAILED: Mandatory attribute \"identifier\" " + "could not be found"); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.198", "identifier"))); result = false; } } else if (parentNodeName.equalsIgnoreCase("item") && (mRulesValidator.getApplicationProfile()).equals("contentaggregation")) { multiplicityUsed = getMultiplicityUsed(attrList, "identifier"); if (multiplicityUsed < 1) { mLogger.debug("FAILED: Mandatory attribute \"identifier\" " + "could not be found"); DetailedLogMessageCollection.getInstance().addMessage(new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.198", "identifier"))); result = false; } // need to perform a special check to warn when parameters // are present but no identifierref is int idrefMult = -1; int paramMult = -1; idrefMult = getMultiplicityUsed(attrList, "identifierref"); paramMult = getMultiplicityUsed(attrList, "parameters"); if ((idrefMult < 1) && !(paramMult < 1)) { // we have a parameters but no identifierref - warning msgText = Messages.getString("CPValidator.220"); mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.WARNING, msgText)); } // have to store the idref values in a List for future // app profile checking of resource attributes if (idrefMult >= 1) { String iDREFValue = DOMTreeUtility.getAttributeValue(iTestSubjectNode, "identifierref"); boolean validIdref = mResourceIdentifierList.contains(iDREFValue); if (validIdref) { mValidIdrefs.add(iDREFValue); } // Whether or not it is true we need to keep track of ALL of // the idrefs so we can look for dangling references after // the entire list of refs, including those on sub-manifests // have been inventoried. mAllIdrefsList.add(iDREFValue); } // Perform a special check to ensure that initialization data // only exists on items that reference SCOs NodeList childrenOfItem = iTestSubjectNode.getChildNodes(); if (childrenOfItem != null) { Node currentItemChild; String currentItemChildName; int len = childrenOfItem.getLength(); for (int k = 0; k < len; k++) { currentItemChild = childrenOfItem.item(k); currentItemChildName = currentItemChild.getLocalName(); if (currentItemChildName.equals("timeLimitAction") || currentItemChildName.equals("dataFromLMS") || currentItemChildName.equals("completionThreshold")) { if (idrefMult < 1) { // we have an item that contains initialization // data // and does not reference a resource at all result = false; msgText = Messages.getString("CPValidator.226", currentItemChildName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } else { // we must verify that the resource it is // referencing // is a sco String idrefValue = DOMTreeUtility.getAttributeValue(iTestSubjectNode, "identifierref"); result = result && checkForReferenceToSco(idrefValue); } } } } } else if (parentNodeName.equalsIgnoreCase("resource")) { checkBucketUniqueness(iTestSubjectNode); boolean resourceResult = checkResourceAttributes(iTestSubjectNode, attrList); result = result && resourceResult; } else if (parentNodeName.equalsIgnoreCase("bucket")) { checkBucketAttributes(iTestSubjectNode); } else if (parentNodeName.equalsIgnoreCase("size")) { checkSizeAttributes(iTestSubjectNode); } // test the attributes for (int j = 0; j < numAttr; j++) { currentAttrNode = (Attr) attrList.item(j); currentNodeName = currentAttrNode.getLocalName(); dataType = mRulesValidator.getRuleValue(parentNodeName, iPath, "datatype", currentNodeName); // we do not want to test for xml namespaces or extensions if (dataType.equalsIgnoreCase("idref") || dataType.equalsIgnoreCase("id") || dataType.equalsIgnoreCase("vocabulary") || dataType.equalsIgnoreCase("text") || dataType.equalsIgnoreCase("uri")) { msgText = Messages.getString("CPValidator.164", currentNodeName); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); multiplicityUsed = getMultiplicityUsed(attrList, currentNodeName); // We will assume that no attribute can exist more than // once (ever). According to W3C. Therefore, min and max // rules must exist. // get the min rule and convert to an int minRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "min", currentNodeName)); // get the max rule and convert to an int maxRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "max", currentNodeName)); if ((minRule != -1) || (maxRule != -1)) { if (multiplicityUsed >= minRule && multiplicityUsed <= maxRule) { msgText = Messages.getString("CPValidator.169", currentNodeName); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.175", currentNodeName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } // get the spm rule and convert to an int spmRule = Integer .parseInt(mRulesValidator.getRuleValue(parentNodeName, iPath, "spm", currentNodeName)); attributeValue = currentAttrNode.getValue(); if (dataType.equalsIgnoreCase("idref")) { // This is a IDREF data type // check the attributes for smallest permitted maximum // (spm) conformance. result = checkSPMConformance(currentNodeName, attributeValue, spmRule, true) && result; // check the Default Idref to make sure it points to an // valid identifier. if (currentNodeName.equalsIgnoreCase("default")) { boolean foundDefaultIdentifier = false; // check for this identifer in the organization list int numOrganizationIdentifiers = mOrganizationIdentifierList.size(); for (int i = 0; i < numOrganizationIdentifiers; i++) { String identifier = (mOrganizationIdentifierList.get(i)); if (identifier.equals(attributeValue)) { foundDefaultIdentifier = true; break; } } if (foundDefaultIdentifier) { msgText = Messages.getString("CPValidator.251", currentNodeName); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.254", currentNodeName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } if (currentNodeName.equalsIgnoreCase("identifierref") && parentNodeName.equalsIgnoreCase("dependency")) { mAllIdrefsList.add(currentAttrNode.getValue()); } } else if (dataType.equalsIgnoreCase("id")) { // This is a id data type // check the attributes for smallest permitted maximum // (spm) conformance. result = checkSPMConformance(currentNodeName, attributeValue, spmRule, true) && result; if (parentNodeName.equals("manifest")) { mManifestID = currentAttrNode.getValue(); mLogger.debug("mManifestID is " + mManifestID); } // imsssp id attributes here } else if (dataType.equalsIgnoreCase("uri")) { // This is a URI data type // check the attributes for smallest permitted maximum // (spm) conformance. Only perform these checks if // the value is not an empty string String myAttributeValue = currentAttrNode.getValue(); if (!myAttributeValue.equals("")) { // check to ensure there are no leading slashes result = checkForSlashes("href", myAttributeValue) && result; // check if the file exists // apply xml:base on href value before href checks if (doesXMLBaseExist()) { mLogger.debug("APPLYING XML BASE"); myAttributeValue = applyXMLBase(myAttributeValue); } if (myAttributeValue.indexOf('\\') != -1) { msgText = Messages.getString("CPValidator.265", myAttributeValue); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result &= false; } // check href spm after it is pre-appended with // xml:base result = checkSPMConformance(currentNodeName, myAttributeValue, spmRule, true) && result; result = checkHref(myAttributeValue) && result; } } else if (dataType.equalsIgnoreCase("vocabulary")) { // This is a VOCAB data type // retrieve the vocab rule values and check against the // vocab values that exist within the test subject msgText = "Testing attribute \"" + currentNodeName + "\" for valid vocabulary"; mLogger.debug("INFO: " + msgText); List<String> vocabAttribValues = mRulesValidator.getAttribVocabRuleValues(parentNodeName, iPath, currentNodeName); // we are assuming that only 1 vocabulary value may // exist for an attribute result = checkVocabulary(currentNodeName, attributeValue, vocabAttribValues, true) && result; } else if (dataType.equalsIgnoreCase("text")) { // This is a TEXT data type // check the attributes for smallest permitted maximum // (spm) conformance. result = checkSPMConformance(currentNodeName, attributeValue, spmRule, true) && result; // test the parameter attribute for valid syntax if (currentNodeName.equalsIgnoreCase("parameters")) { ParameterChecker pc = new ParameterChecker(); result = pc.checkParameters(attributeValue) && result; } } } } // done with attributes // Test the child Nodes NodeList children = iTestSubjectNode.getChildNodes(); if (children != null) { int numChildren = children.getLength(); // update the path for this child element String path; if (iPath.equals("")) { // the node is a DOCUMENT or a root <manifest> path = parentNodeName; } else if ((iPath.equals("manifest.manifest")) && (parentNodeName.equals("manifest"))) { path = iPath; } else if (parentNodeName.equalsIgnoreCase("item")) { // the Node is an <imscp:item> if (iPath.equals("manifest.organizations.organization.item")) { path = iPath; } else { path = iPath + "." + parentNodeName; } } else { path = iPath + "." + parentNodeName; } // SPECIAL CASE: check for mandatory elements if (parentNodeName.equalsIgnoreCase("manifest")) { // check for mandatory metadata element at package level multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "metadata"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "metadata"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else // check for mandatory children { Node caMetadataNode = DOMTreeUtility.getNode(iTestSubjectNode, "metadata"); // check for mandatory <imscp:schema> element multiplicityUsed = getMultiplicityUsed(caMetadataNode, "schema"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "schema"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // check for mandatory <imscp:schemaversion> element multiplicityUsed = getMultiplicityUsed(caMetadataNode, "schemaversion"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "schemaversion"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "organizations"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "organizations"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "resources"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "resources"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } else if (parentNodeName.equalsIgnoreCase("organizations") && (mRulesValidator.getApplicationProfile()).equals("contentaggregation")) { multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "organization"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "organization"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } // have to check to ensure that empty organizations exist // for resource package else if (parentNodeName.equalsIgnoreCase("organizations") && (mRulesValidator.getApplicationProfile()).equals("resource")) { multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "organization"); if (multiplicityUsed > 0) { msgText = Messages.getString("CPValidator.311"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } else { msgText = Messages.getString("CPValidator.312"); // we have an empty <orgs> element, display a valid msg mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } } else if (parentNodeName.equalsIgnoreCase("organization") && (mRulesValidator.getApplicationProfile()).equals("contentaggregation")) { multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "title"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "title"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, "item"); if (multiplicityUsed < 1) { msgText = Messages.getString("CPValidator.287", "item"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } // special checks for item result = checkItem(iTestSubjectNode, mManifestInfo) && result; } for (int z = 0; z < numChildren; z++) { Node currentChild = children.item(z); String currentChildName = currentChild.getLocalName(); msgText = "Currentchild is " + currentChildName + " and path is " + path; mLogger.debug(msgText); if (currentChildName != null) { if (((currentChildName.equals("timeLimitAction")) || (currentChildName.equals("dataFromLMS")) || (currentChildName.equals("completionThreshold")) || (currentChildName.equals("presentation"))) && (!parentNodeName.equals("item"))) { result = false; msgText = Messages.getString("CPValidator.328", currentChildName, "item"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } if (((currentChildName.equals("constrainedChoiceConsiderations")) || (currentChildName.equals("rollupConsiderations"))) && (!parentNodeName.equals("sequencing"))) { result = false; msgText = Messages.getString("CPValidator.328", currentChildName, "sequencing"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } // must enforce that the adlcp:location exist // as a child of metadata only - warning for best // practice. if ((currentChildName.equals("location")) && (!parentNodeName.equals("metadata"))) { result = false; msgText = Messages.getString("CPValidator.328", currentChildName, "metadata"); mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.WARNING, msgText)); } if ((currentChildName.equals("sequencing")) && (!parentNodeName.equals("item")) && (!parentNodeName.equals("organization"))) { result = false; msgText = Messages.getString("CPValidator.345", currentChildName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } dataType = mRulesValidator.getRuleValue(currentChildName, path, "datatype"); // must enforce that the imsssp:bucket exist // as a child of a resource only. if ((currentChildName.equals("bucket")) && (!parentNodeName.equals("resource"))) { // Check to enforce that bucket is a child of a // resource msgText = "<" + currentChildName + "> can only " + "exist as a child of a <resource>"; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } // must enforce that the imsssp:size exist // as a child of a bucket only. if ((currentChildName.equals("size")) && (!parentNodeName.equals("bucket"))) { msgText = "<" + currentChildName + "> can only " + "exist as a child of a <bucket>"; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } // Check the SCORMType of the resource; it must be sco if ((currentChildName.equals("bucket")) && (parentNodeName.equals("resource"))) { // Now check to ensure that the resource type is SCO String typeS = DOMTreeUtility.getAttributeValue(iTestSubjectNode, "scormType"); if (!typeS.equalsIgnoreCase("sco")) { // result = false; msgText = "The <" + currentChildName + "> shall" + " only exist in a resource that is " + " scormType = \"sco\"."; mLogger.debug("SSP: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); } } // we do not want to test for extensions here if (dataType.equalsIgnoreCase("parent") || dataType.equalsIgnoreCase("vocabulary") || dataType.equalsIgnoreCase("text") || dataType.equalsIgnoreCase("sequencing") || dataType.equalsIgnoreCase("metadata") || dataType.equalsIgnoreCase("decimal")) { // SCORM 3rd edition -- we need to ignore // (sub)manifest // and warn only if (currentChildName.equals("manifest") && path.equals("manifest")) { msgText = Messages.getString("CPValidator.100"); mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.WARNING, msgText)); // Make cleansing call for excess baggage here // pass (sub)manifest node into a new function // retrieve all adlcp:location uri's contained // in the // (sub)manifest List<String> submanifestURIList = mManifestHandler.getLocationMD(currentChild); trackSubManifest(currentChild, submanifestURIList); } else // we are not dealing with (sub)manifest { msgText = Messages.getString("CPValidator.131", currentChildName); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); multiplicityUsed = getMultiplicityUsed(iTestSubjectNode, currentChildName); // get the min rule and convert to an int minRule = Integer .parseInt(mRulesValidator.getRuleValue(currentChildName, path, "min")); // get the max rule and convert to an int maxRule = Integer .parseInt(mRulesValidator.getRuleValue(currentChildName, path, "max")); if ((minRule != -1) && (maxRule != -1)) { if (multiplicityUsed >= minRule && multiplicityUsed <= maxRule) { msgText = Messages.getString("CPValidator.135", currentChildName); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.364", currentChildName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } else if ((minRule != -1) && (maxRule == -1)) { if (multiplicityUsed >= minRule) { msgText = Messages.getString("CPValidator.135", currentChildName); mLogger.debug("PASSED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.PASSED, msgText)); } else { msgText = Messages.getString("CPValidator.364", currentChildName); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = false; } } // test for each particular data type if (dataType.equalsIgnoreCase("parent")) { // need to populate the files that belong to // each resource if (currentChildName.equals("resources")) { populateResourceTable(currentChild); } // Verify that if the resource href matches // the file href if the resource is local if (currentChildName.equals("resource")) { result = checkResourceFileHref(currentChild) && result; } // This is a parent element, need to recurse result = compareToRules(currentChild, path) && result; } else if (dataType.equalsIgnoreCase("sequencing")) { // This is a sequencing data type SequenceValidator sequenceValidator = new SequenceValidator(); result = sequenceValidator.validate(currentChild) && result; } else if (dataType.equalsIgnoreCase("metadata")) { // This is a metadata data type - no longer // need // to // check for lom and location to coexist // must detect that the metadata exists in // location if (currentChildName.equals("location")) { String currentLocationValue = mRulesValidator.getTaggedData(currentChild); // check to ensure there are no leading // slashes result = checkForSlashes("location", currentLocationValue) && result; currentLocationValue = applyXMLBase(currentLocationValue); result = result && checkHref(currentLocationValue); } } else if (dataType.equalsIgnoreCase("text")) { // This is a text data type // check spm // first must retrieve the value of this // child // element String currentChildValue = mRulesValidator.getTaggedData(currentChild); // get the spm rule and convert to an int spmRule = Integer .parseInt(mRulesValidator.getRuleValue(currentChildName, path, "spm")); result = checkSPMConformance(currentChildName, currentChildValue, spmRule, false) && result; } else if (dataType.equalsIgnoreCase("vocabulary")) { // This is a vocabulary data type // more than one vocabulary token may exist msgText = Messages.getString("CPValidator.383", currentChildName); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); // retrieve the value of this element String currentChildValue = mRulesValidator.getTaggedData(currentChild); List<String> vocabValues = mRulesValidator.getVocabRuleValues(currentChildName, path); result = checkVocabulary(currentChildName, currentChildValue, vocabValues, false) && result; } else if (dataType.equalsIgnoreCase("decimal")) { // This is a decimal data type // only adlcp:completionThreshold is of this // type // and currently all checks are enforced by // the schema. No additional checks needed // at // this time. result = true && result; } } } // end ignorning and warning (sub)manifest } // end something } } // remove the xml:base value for this particular element if (parentNodeName.equals("manifest")) { mXMLBase[0][1] = ""; } else if (parentNodeName.equals("resources")) { mXMLBase[1][1] = ""; } else if (parentNodeName.equals("resource")) { mXMLBase[2][1] = ""; } break; } // handle entity reference nodes case Node.ENTITY_REFERENCE_NODE: { break; } // text case Node.COMMENT_NODE: { break; } case Node.CDATA_SECTION_NODE: { break; } case Node.TEXT_NODE: { break; } default: { // Do nothing - no defined requirements to process any other // type of node type break; } }// end switch statement mLogger.debug("CPValidator compareToRules()"); return result; } /** * This method checks all files and folders in a content package and creates * a list of the files. This list is used to determine if any files are * present and not referenced in the imsmanifest.xml file. <br> * <br> * * @param rootDirectory * A string containing the path of the root directory of the * content package contents. */ private void createManifestFileList(String rootDirectory) { File fileOrFolder = new File(rootDirectory); // This will hold a list of all files and folders in this directory File[] filesInDirectory; // if this is a directory we need to drill down to all other folders and // files to make sure we account for everything if (fileOrFolder.isDirectory() && !fileOrFolder.getName().equals("common") && !fileOrFolder.getName().equals("vocab") && !fileOrFolder.getName().equals("extend") && !fileOrFolder.getName().equals("unique")) { filesInDirectory = fileOrFolder.listFiles(); int fileCount = 0; // recursive call to drill down to all included levels of the // root directory while (fileCount < filesInDirectory.length) { createManifestFileList(filesInDirectory[fileCount].getPath()); fileCount++; } // end while } // end if // if it is a file and it isn't an xsd or the manifest add it to the // list if (fileOrFolder.isFile() && !fileOrFolder.getName().endsWith(".xsd") && !fileOrFolder.getName().equals("imsmanifest.xml") && !fileOrFolder.getName().endsWith(".dtd")) { String tempStr = fileOrFolder.getPath(); tempStr = tempStr.substring(tempStr.indexOf("PackageImport\\") + 14, tempStr.length()); tempStr = tempStr.replace('\\', '/'); // if the filename is already in the List don't add it if (!mFileList.contains(tempStr)) { mFileList.add(tempStr); } } }// end createManifestmFileList() /** * This method determines if xml:base is declared in the IMS Manifest. * * @return boolean describing if xml:base is declared. True implies that * xml:base does exist within the IMS Manifest, false implies that * xml:base was not declared in the IMS Manifest. */ private boolean doesXMLBaseExist() { boolean xmlBaseExists = true; // determine if the xml:base array contains values if ((mXMLBase[0][1].equals("")) && (mXMLBase[1][1].equals("")) && (mXMLBase[2][1].equals(""))) { // xml:base was not declared for a manifest, resources, or resource // element xmlBaseExists = false; } return xmlBaseExists; } /** * This method parses the mSchemaLocation value and retrieves only the * schema name. Once the schema name is tokenized, it is then checked for * existance at the root of the package. * * @param iSchemaLocations * -- the string value that houses the schemaLocations * @param iDirectory * -- the base directory of the test suite that is used to build * the location of the schemas. * @return integer - 1 if files are found, 0 if files are not found. */ private int findAndLocateRequiredFiles(String iSchemaLocations, String iDirectory, List<String> iDeclaredNamespaces) { int result = 1; File currentFile; boolean currentResult; String msgText = ""; String tok = ""; String defaultSchemaLocList = ""; String xsdLocation = iDirectory.replace('\\', '/'); // determine if we are dealing with the defaulted schema location list if (!mSchemaLocExists) { // the default schemas have been set. // retrieve namespace section of schemaLocation value to determine // which // required files apply String[] namespace = iSchemaLocations.split(" "); for (int x = 0; x < namespace.length; x++) { if (namespace[x].startsWith("http")) { // we have a namespace that needs compared to the declared // namespaces if (iDeclaredNamespaces.contains(namespace[x])) { // remove namespace and schemalocation from // schemalocation value defaultSchemaLocList += namespace[x] + " " + namespace[x + 1] + " "; } } } iSchemaLocations = defaultSchemaLocList; } StringTokenizer st = new StringTokenizer(iSchemaLocations, " "); // Parse the schemaLocation value to obtain only the schemaName // then check each to see if it exists in the package while (st.hasMoreTokens()) { // This is the namespace value tok = st.nextToken(); if (st.hasMoreTokens()) { // this is the schemaLocation value tok = st.nextToken(); StringTokenizer schemaLocationTokens = new StringTokenizer(tok, "/"); String schemaName = ""; String toke = ""; String previousToke = ""; boolean verifyExistance = true; while (schemaLocationTokens.hasMoreTokens()) { // Need to obtain only the schema files name from the string previousToke = toke; toke = schemaLocationTokens.nextToken(); if (!schemaLocationTokens.hasMoreTokens()) { if (!previousToke.equals("vocab")) { schemaName = xsdLocation + toke; } else { // Do not need to verify schemas that aren't located // at the root (i.e. custom vocab schemas). verifyExistance = verifyExistance && false; } } } if (verifyExistance) { // Test if this file exists // do not test duplicates if (!mRequiredFilesList.contains(toke)) { currentFile = new File(schemaName); currentResult = currentFile.exists(); if (currentResult) { mLogger.debug( "PASSED: File \"" + toke + "\" found " + "at the root of the content package"); DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.PASSED, Messages.getString("CPValidator.81", toke))); } else { msgText = Messages.getString("CPValidator.83", toke); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); result = 0; } // add to List to account for future duplicate files mRequiredFilesList.add(toke); } } } } return result; } /** * This recursive method is used to gather all the files that belong to the * <resource> element. The files are tracked from the <file> and * <dependency> elements(s). * * @param resourceNode * <resource> element * @param resourcesXMLBase * xml:base attribute value of the <resources> element * @param fileHrefList * list of files being tracked for the <resource> element * @param resourceNodes * List containing all the <resource> elements that * belong to the <resources> element * * @param resourcesVisited * list contains the <resource> elements tracked for * dependency referencing to prevent circular dependencies. */ private void findFileHrefs(Node resourceNode, String resourcesXMLBase, List<String> fileHrefList, List<Node> resourceNodes, List<String> resourcesVisited) { // we must manually retrieve the resource xml:base attribute value // the applyXMLBase is not aware of it at this time of processing String resourceXMLBase = DOMTreeUtility.getAttributeValue(resourceNode, "base"); String resourceID = DOMTreeUtility.getAttributeValue(resourceNode, "identifier"); resourcesVisited.add(resourceID); // add all file elements that are direct children of resource to the // array List<Node> fileNodes = DOMTreeUtility.getNodes(resourceNode, "file"); if (!fileNodes.isEmpty()) { Node currentFileChild; int len = fileNodes.size(); for (int k = 0; k < len; k++) { currentFileChild = fileNodes.get(k); String fileHref = DOMTreeUtility.getAttributeValue(currentFileChild, "href"); // apply XMLBase to fileHRef add it to a list fileHref = mXMLBase[0][1] + resourcesXMLBase + resourceXMLBase + fileHref; fileHrefList.add(fileHref); mLogger.debug("Added this to fileHrefList " + fileHref); } } // now check for dependency elements and trace to retrieve the file // elements List<Node> dependencyNodes = DOMTreeUtility.getNodes(resourceNode, "dependency"); if (!dependencyNodes.isEmpty()) { // determine identifierref and add to the hashtable based on this // key Node currentDependency; int len = dependencyNodes.size(); mLogger.debug("number of dependency is " + len); for (int y = 0; y < len; y++) { currentDependency = dependencyNodes.get(y); String dependencyIDRef = DOMTreeUtility.getAttributeValue(currentDependency, "identifierref"); // need to avoid circular dependency here (where a dependency // references the resource it belongs to) if (!dependencyIDRef.equals(resourceID)) { // retrieving resource element with identifier that matches // identifierref Node current; String currentResourceID = ""; Node dependencyMatch = null; int numChildren = resourceNodes.size(); for (int k = 0; k < numChildren; k++) { current = resourceNodes.get(k); currentResourceID = DOMTreeUtility.getAttributeValue(current, "identifier"); mLogger.debug("Comparing resource id " + currentResourceID + " to this dependencyIDRef " + dependencyIDRef); if (currentResourceID.equals(dependencyIDRef)) { // first determine if we have a circular dependency // before settting the match if (!resourcesVisited.contains(dependencyIDRef)) { dependencyMatch = current; break; } } } // retrieve all the file elements of the resource referenced // by // the dependency and add them to the list if we have a // match if (dependencyMatch != null) { findFileHrefs(dependencyMatch, resourcesXMLBase, fileHrefList, resourceNodes, resourcesVisited); } } } } } /** * Returns the mBaseDirectory attribute that contains the base directory of * where the test subject is located. It is used by the validator to * determine the location of the package resources, including the * imsmanifest, sco's, and/or metadata. * * @return String that contains the path of the base directory */ private String getBaseDirectory() { return mBaseDirectory; } /** * This method retrives the launch data from the manifestHandler object. The * launch data returned contains information for the launching of SCOs found * within the content package test subject. This method is only called if * the user chooses to validate SCOs. * * @param iDefaultOrganizationOnly * A boolean representing whether or not launch data should be * collected from the default organization only. * @param iRemoveAssets * A boolean representing whether or not assets should be removed * in the LaunchData list. * @return List: Containing the launch information */ public List<LaunchData> getLaunchData(boolean iDefaultOrganizationOnly, boolean iRemoveAssets) { return mManifestHandler.getLaunchData((super.getDocument()).getDocumentElement(), iDefaultOrganizationOnly, iRemoveAssets); } /** * This method accesses the manifest handler to extract all SCO launch * information * * @param iRolledUpDocument * - Dom with manifest rollup * @param iDefaultOrganizationOnly * - boolean describing which organization to read from * @param iRemoveAssets * - boolean describing if to include assets in launch locatation * @return Returns a List of launch data */ public List<LaunchData> getLaunchData(Document iRolledUpDocument, boolean iDefaultOrganizationOnly, boolean iRemoveAssets) { return mManifestHandler.getLaunchData(iRolledUpDocument.getDocumentElement(), iDefaultOrganizationOnly, iRemoveAssets); } /** * This method retrives the metadata information from the manifestHandler * object. The metadata information returned contains information for the * validation of the metadata found within the content package test subject. * This method is only called if the user chooses to validate metadata. * * @return List: Containing the metadata launch information */ public List<MetadataData> getMetadataDataList() { return mManifestHandler.getMetadata((super.getDocument()).getDocumentElement(), mBaseDirectory); } /** * This method assists with the application profile check for the * multiplicity of attributes. This method returns the number of times the * attribue was detected based on the given attribute name and the given * parent node of that element name. * * @param iAttributeMap * A list of attributes * @param iNodeName * The name of the element being searched for * @return - int: number of instances of a given attribute */ public int getMultiplicityUsed(NamedNodeMap iAttributeMap, String iNodeName) { mLogger.debug("CPValidator getMultiplicityUsed() - Attributes"); mLogger.debug("Input Node we are looking for: " + iNodeName); int result = 0; int length = iAttributeMap.getLength(); String currentName; for (int i = 0; i < length; i++) { currentName = ((Attr) iAttributeMap.item(i)).getLocalName(); if (currentName.equals(iNodeName)) { result++; } // end if current name equals node name } // end looping over attributes mLogger.debug("The " + iNodeName + ", appeared " + result + " times."); return result; } /** * This method assists with the application profile check for the * multiplicity of elements. This method returns the number of times the * element was detected based on the given element name and the given parent * node of that element name. * * @param iParentNode * The parent node of the element being searched * @param iNodeName * The name of the element being searched for * @return - int: number of instances of a given element */ public int getMultiplicityUsed(Node iParentNode, String iNodeName) { mLogger.debug("CPValidator getMultiplicityUsed() - Elements"); mLogger.debug("Input Parent Node: " + iParentNode.getLocalName()); mLogger.debug("Input Node we are looking for: " + iNodeName); // Need a list to find how many kids to cycle through NodeList kids = iParentNode.getChildNodes(); int count = 0; int kidsLength = kids.getLength(); for (int i = 0; i < kidsLength; i++) { if (kids.item(i).getNodeType() == Node.ELEMENT_NODE) { String currentNodeName = kids.item(i).getLocalName(); // String currentNodeNamespace = kids.item(i).getNamespaceURI(); if (currentNodeName.equals(iNodeName)) { count++; } // end if the current node name equals the name we are looking // for } // end of the node type is ELEMENT_NODE } // end looping over children mLogger.debug("The " + iNodeName + ", appeared " + count + " times."); return count; } /** * This method retrieves the directory location of the test subject by * truncating the filename off of the URL passed in. * * @param iFileName * The absolute path of the test subject file * @return String - the directory that the file is located */ private String getPathOfFile(String iFileName) { mLogger.debug("CPValidator getPathOfFile()"); String result = ""; String tmp = ""; try { StringTokenizer token = new StringTokenizer(iFileName, File.separator, true); int numTokens = token.countTokens(); // We want all but the last token added numTokens--; for (int i = 0; i < numTokens; i++) { tmp = token.nextToken(); mLogger.debug("token = " + tmp); result = result + tmp; } } catch (NullPointerException npe) { npe.printStackTrace(); } mLogger.debug("CPValidator getPathOfFile()"); return result; } /** * This method extracts the selected test subject, in the form of a zip * file, to a temporary "/PackageImport" directory in order to perform * validation. * * @param iPIF * The content package zip test file URI * @param encoding * @return extractDir returnst the path of the directory the file is * extracted to */ private String importContentPackage(String iPIF, String encoding) { // get the extract dir mLogger.debug("CPValidator importContentPackage()"); String extractDir = mEnvironmentVariable + File.separator + "PackageImport" + File.separator; mLogger.debug("extractDir = " + extractDir); // Unzip the content package into a local directory for processing UnZipHandler uzh = new UnZipHandler(iPIF, extractDir, encoding); uzh.extract(); mLogger.debug("CPValidator importContentPackage()"); return extractDir; } /** * This method populates the mResourceTable attribute which is responsible * for storing an array of files (from the <file> and * <dependency> elements) that belong to each resourceID. If a * resource does not contain an association of files, then an key will not * be added to the hashtable for that resource. This method assists in the * checking of the requirement that if a resource is local to a content * package, than it must contain an identifical <file> element in * association with it. * * @param iResourcesNode * - The <resources> node */ private void populateResourceTable(Node iResourcesNode) { // we must manually retrieve the resources xml:base attribute value String resourcesXMLBase = DOMTreeUtility.getAttributeValue(iResourcesNode, "base"); // retrieve all resource nodes from resource parent List<Node> resourceNodes = DOMTreeUtility.getNodes(iResourcesNode, "resource"); List<String> fileHrefList; if (!resourceNodes.isEmpty()) { Node currentResource; int numChildren = resourceNodes.size(); mLogger.debug("Number of resource children are " + numChildren); // loop through each resource and track all files per resource // identifer for (int t = 0; t < numChildren; t++) { fileHrefList = new ArrayList<String>(); currentResource = resourceNodes.get(t); // resourceID used for hash table key String resourceID = DOMTreeUtility.getAttributeValue(currentResource, "identifier"); mLogger.debug("Calling findFiles on resource " + resourceID); // call our recursive function here // pass in a resource node and expect back an arrayList of files List<String> resourcesVisited = new ArrayList<String>(); findFileHrefs(currentResource, resourcesXMLBase, fileHrefList, resourceNodes, resourcesVisited); if (!fileHrefList.isEmpty()) { mResourceTable.put(resourceID, fileHrefList); } else { mLogger.debug("fileHrefList is empty"); } } } } /** * This method is used to turn full content package validation off - * including required files check, validation to the schema, and application * profile checks. Turning full validation off allows only a parse for * well-formedness to be preformed. * * @param iValue * True implies to parse for well-formedness and validation to * the schema, false implies to parse for well-formedness only. */ public void setPerformValidationToSchema(boolean iValue) { mPerformFullValidation = iValue; } /** * This method tracks all identifier values found in the manifest for the * organization elements only. Tracking of organization identifiers is * performed in order to verify that default attribute points to valid * organization identifier value. These identifers are tracked recursively * by walking through test subject dom and adding the identifier elements * found to a list. * * @param iParentNode * Root node of the test subject */ private void trackOrgIdentifiers(Node iParentNode) { // recursively find the organization ids and add them to the List mLogger.debug("CPValidator trackOrgIdentifiers()"); String msgText = ""; if (iParentNode != null) { int type = iParentNode.getNodeType(); switch (type) { case Node.DOCUMENT_NODE: { Node rootNode = ((Document) iParentNode).getDocumentElement(); trackOrgIdentifiers(rootNode); break; } case Node.ELEMENT_NODE: { String nodeName = iParentNode.getLocalName(); if (nodeName.equals("manifest")) { Node orgsNode = DOMTreeUtility.getNode(iParentNode, "organizations"); if (orgsNode != null) { List<Node> orgNodes = DOMTreeUtility.getNodes(orgsNode, "organization"); // Loop through the oganization elements to retrieve the // identifier attribute values int orgNodesSize = orgNodes.size(); for (int i = 0; i < orgNodesSize; i++) { Node currentChild = orgNodes.get(i); String orgIdentifier = DOMTreeUtility.getAttributeValue(currentChild, "identifier"); mOrganizationIdentifierList.add(orgIdentifier); msgText = "Just added " + orgIdentifier + "to the org List"; mLogger.debug(msgText); } } } // Get (sub)manifests and make call recursively List<Node> subManifestList = DOMTreeUtility.getNodes(iParentNode, "manifest"); int subManifestListSize = subManifestList.size(); for (int j = 0; j < subManifestListSize; j++) { Node currentSubManifest = subManifestList.get(j); trackOrgIdentifiers(currentSubManifest); } break; } default: { // Do nothing - no defined requirements to process any other // type of node type break; } } } mLogger.debug("CPValidator trackOrgIdentifiers()"); } /** * This method tracks all identifier values found in the manifest for the * resource elements only. Tracking of resource identifiers is performed in * order to verify that dependency element points to valid resource * identifier value. The dependency element shall only point within its * parents scope. Sub-manifest resources may not be referenced. <br> * * @param iResourcesNode * The <resources> node that parents the <resource> * elements. <br> */ private void trackResourceIdentifiers(Node iResourcesNode) { // recursively find the resource ids and add them to the list mLogger.debug("CPValidator trackResourceIdentifiers()"); List<Node> resourceNodes = new ArrayList<Node>(); if (iResourcesNode != null) { // this will return a List of all child nodes of iResourceNode // whos // name = "resource" resourceNodes = DOMTreeUtility.getNodes(iResourcesNode, "resource"); int resourceNodesSize = resourceNodes.size(); for (int i = 0; i < resourceNodesSize; i++) { Node currentChild = resourceNodes.get(i); String resourceId = DOMTreeUtility.getAttributeValue(currentChild, "identifier"); // This list is cleaned out for each manifest/sub-manifest mResourceIdentifierList.add(resourceId); // This list holds ALL of the identifier values listed in the // entire manifest. mManifestResourceIdentifierList.add(resourceId); } } // end if mLogger.debug("CPValidator trackResourceIdentifiers()"); } /** * Recursively walk the (sub)manifest node and track resource.href, * file.href and adlcp:location URI values. We will attempt to detect these * files. If found, they will be removed from the mFileList in order to * account for excessive baggage that is caused from (sub)manifests. * * @param iSubmanifestNode * the (sub)manifest element node * @param iSubmanifestURIList * used to keep track of file and href references in * (sub)manifests to exclude them from the excess * baggage/dangling resource checks */ private void trackSubManifest(Node iSubmanifestNode, List<String> iSubmanifestURIList) { // must add URIs from the resource href and the file href if (iSubmanifestNode.getNodeName().equals("manifest")) { // retrieve xml:base at manifest if it exists String manifestXmlBase = DOMTreeUtility.getAttributeValue(iSubmanifestNode, "base"); Node resourcesNode = DOMTreeUtility.getNode(iSubmanifestNode, "resources"); if (resourcesNode != null) { String resourcesXmlBase = DOMTreeUtility.getAttributeValue(resourcesNode, "base"); List<Node> resourceNodes = DOMTreeUtility.getNodes(resourcesNode, "resource"); // Loop through the resource elements to retrieve the // href attribute values for (int i = 0; i < resourceNodes.size(); i++) { Node currentResourceChild = resourceNodes.get(i); String resourceHref = DOMTreeUtility.getAttributeValue(currentResourceChild, "href"); String resourceXmlBase = DOMTreeUtility.getAttributeValue(currentResourceChild, "base"); String xmlBase = manifestXmlBase + resourcesXmlBase + resourceXmlBase; iSubmanifestURIList.add(xmlBase + resourceHref); mLogger.debug("Just added " + xmlBase + resourceHref + "to the sub List"); // get children to add file href if it exists List<Node> fileNodes = DOMTreeUtility.getNodes(currentResourceChild, "file"); // Loop through the file elements to retrieve the // href attribute values for (int j = 0; j < fileNodes.size(); j++) { Node currentFileChild = fileNodes.get(j); String fileHref = DOMTreeUtility.getAttributeValue(currentFileChild, "href"); iSubmanifestURIList.add(xmlBase + fileHref); mLogger.debug("Just added " + xmlBase + fileHref + "to the sub List"); } } } } // recursivly call for nested child (sub)manifest NodeList manifestChildren = iSubmanifestNode.getChildNodes(); for (int k = 0; k < manifestChildren.getLength(); k++) { String currentNodeName = manifestChildren.item(k).getLocalName(); if (currentNodeName.equals("manifest")) { trackSubManifest(manifestChildren.item(k), iSubmanifestURIList); } } // start cleansing process for (int m = 0; m < iSubmanifestURIList.size(); m++) { mFileList.remove(iSubmanifestURIList.get(m)); } } /** * This method initiates the validation process. The checks called from this * method include the required files check, manifest checks (wellformedness * and schema validation), and application profile checks. If the test * subject is in the form of a zip, then the extract is performed prior to * the calls mentioned above. * * @param iFileName * The name of the SCORM Content Package test subject * @param iTestType * The type of test subject ( pif or media ) * @param iApplicationProfileType * The Application Profile type of the test subject (content * aggregation or resource ) * @param iManifestOnly * The boolean describing whether or not the IMS Manifest is to * be the only subject validated. True implies that validation * occurs only on the IMS Manifest (checks include * wellformedness, schema validation, and application profile * checks). False implies that the entire Content Package be * validated (IMS Manifest checks with the inclusion of the * required files checks, metadata, and sco testing). * @param encoding * @return - Boolean value indicating the outcome of the validation checks * True implies that validation was error free, false implies * otherwise. */ public boolean validate(String iFileName, String iTestType, String iApplicationProfileType, boolean iManifestOnly, String encoding) { boolean validateResult = true; String msgText; String rootDirectory; if (mLogger.isDebugEnabled()) { mLogger.debug("CPValidator validate()"); mLogger.debug(" iXMLFileName coming in is " + iFileName); mLogger.debug(" iApplicationProfileType coming in is " + iApplicationProfileType); mLogger.debug(" iTestType coming in is " + iTestType); } // If the CTS is testing a Package Interchange File, then import the // content package (unzip and set up the CTS). if (iTestType.equals("pif")) { rootDirectory = importContentPackage(iFileName, encoding); mBaseDirectory = mEnvironmentVariable + File.separator + "PackageImport" + File.separator; } else { rootDirectory = mBaseDirectory = getPathOfFile(iFileName); } try { // Create the absolute URL for the location of the imsmanifest.xml String imsManifestFile = mBaseDirectory + "imsmanifest.xml"; // Now check to see if the manifest file is present. File manifestFile = new File(imsManifestFile); boolean manifestFound = true; // Test to see whether or not the imsmanifest.xml file exists as // defined // by the pathname. if (!manifestFile.exists()) { mLogger.debug( "FAILED: Required file \"imsmanifest.xml\" not " + "found at the root of the package"); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, Messages.getString("CPValidator.22"))); manifestFound = false; } // Set the ADLSCORMValidator protected attribute with this value. // This value is needed for summary logging and ADLValidatorOutcome super.setIsIMSManifestPresent(manifestFound); // Retrieve imsmanifest for wellformedness and validation parse if // high level checks passed if (manifestFound) { mLogger.debug("INFO: Testing the manifest for Well-formedness"); validateResult = checkWellformedness(imsManifestFile) && validateResult; boolean wellformed = super.getIsWellformed(); mSchemaLocExists = super.getSchemaLocExists(); // This is only done if the manifest is present, no reason to do // so // otherwise createManifestFileList(rootDirectory); // determine if the root element belongs to the IMS namespace // can only perform this check if we have a wellformed document. boolean validRoot = false; if (wellformed) { validRoot = super.isRootElementValid(); super.setIsRootElement(validRoot); } // continue to validate only if we are dealing with an IMS // Manifest if (validRoot) { if (wellformed && !iManifestOnly && mPerformFullValidation) { // If the imsmanifest.xml is well-formed, then check to // make // sure all of the required files are present. // first determine if the default schemas are being // used. If so, warn to inform if (!mSchemaLocExists) { msgText = Messages.getString("CPValidator.1"); mLogger.debug("WARNING: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.WARNING, msgText)); } List<String> declaredNamespaceList = super.getDeclaredNamespaces(); validateResult = checkForRequiredFiles(mBaseDirectory, declaredNamespaceList) && validateResult; } if (validateResult && mPerformFullValidation) { msgText = "Testing the manifest for Validity to the Controlling Documents"; mLogger.debug("INFO: " + msgText); // The imsmanifest.xml file is well-formed and all of // the // required // files were present, now check to see if the // imsmanifest.xml // is valid according to the controlling schemas. validateResult = checkValidityToSchema(imsManifestFile) && validateResult; mLogger.debug("OTHER: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.XMLOTHER, "HR")); msgText = Messages.getString("CPValidator.28"); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); // Prepare for manifest idref verification msgText = Messages.getString("CPValidator.30"); mLogger.debug("INFO: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.INFO, msgText)); boolean populateResult = mManifestInfo.populateManifestMap(super.getDocument()); boolean idrefResult = true; if (populateResult) { List<String> idrefs = mManifestInfo.checkAllIdReferences(); if (!idrefs.isEmpty()) { mLogger.debug("invalid idrefs exist"); validateResult = false; idrefResult = false; super.setIsValidToApplicationProfile( super.getIsValidToApplicationProfile() && false); } else { mLogger.debug("INFO: ID/IDRef Validation checks passed"); DetailedLogMessageCollection.getInstance().addMessage( new LogMessage(MessageType.INFO, Messages.getString("CPValidator.34"))); } } // Read in XML rules for Application Profile Checks boolean doXMLRulesExist = mRulesValidator.readInRules(iApplicationProfileType); if (doXMLRulesExist) { trackOrgIdentifiers(super.getDocument()); boolean isAppProfileResult = compareToRules(super.getDocument(), ""); checkForExcessBaggage(iApplicationProfileType); // Set the ADLSCORMValidator protected attribute // with this // value. This value is needed for summary logging // and // ADLValidatorOutcome super.setIsValidToApplicationProfile(idrefResult && isAppProfileResult); validateResult = isAppProfileResult && validateResult; } else { // XML rules cannot be found to perform app profile // checking, // system error msgText = Messages.getString("CPValidator.36"); mLogger.debug("FAILED: " + msgText); DetailedLogMessageCollection.getInstance() .addMessage(new LogMessage(MessageType.FAILED, msgText)); validateResult = false; } // end if the XML Rules Exist } // end if perform full validation is requested } // end if dealing with IMS Namespace Manifest else { validateResult = false; } } // end if the manifest was found mLogger.debug("CPValidator validate()"); return validateResult; } finally { if (new File(mBaseDirectory).exists()) { cleanImportDirectory(mBaseDirectory); } } } }// end class CPValidator