All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.movielabs.mddflib.manifest.validation.CpeValidator Maven / Gradle / Ivy

/**
 * Copyright (c) 2017 MovieLabs

 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.movielabs.mddflib.manifest.validation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;

import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;

import com.movielabs.mddflib.logging.LogMgmt;
import com.movielabs.mddflib.logging.LogReference;
import com.movielabs.mddflib.manifest.validation.profiles.CpeIP1Validator;
import com.movielabs.mddflib.manifest.validation.profiles.ProfileValidator;
import com.movielabs.mddflib.util.xml.MddfTarget;
import com.movielabs.mddflib.util.xml.XmlIngester;

import net.sf.json.JSONObject;

/**
 * Handles validation of a CPE-Manifest as specified in TR-CPE-M1. As a
 * CPE-Manifest must also conform to the requirements of a Common Media Manifest
 * (CMM), the CpeValidator class extends the ManifestValidator
 * class.
 * 

* CpeValidator supports profile validation by building an * information model. The tests to see if the model conforms to a * specific profile will, however, be performed by helper classes specific to * each CPE profile. *

* * @author L. Levin, Critical Architectures LLC * */ public class CpeValidator extends ManifestValidator implements ProfileValidator { /** * @author L. Levin, Critical Architectures LLC * */ public class ExperienceNode extends DefaultMutableTreeNode { private Element xmlEl; private String cid; /** * @param xmlEl */ public ExperienceNode(Element xmlEl) { this.xmlEl = xmlEl; cid = xmlEl.getChildTextNormalize("ContentID", manifestNSpace); } /** * @return */ public Element getExpEl() { return xmlEl; } public List getChildren() { Enumeration kinder = this.children(); List expList = new ArrayList(); while(kinder.hasMoreElements()) { expList.add((ExperienceNode) kinder.nextElement()); } return expList; } public List getDescendents() { List dList = new ArrayList(); for (ExperienceNode child : this.getChildren()) { dList.addAll(child.getDescendents()); dList.add(child); } return dList; } /** * @return */ public Element getMetadata() { return cid2MDataMap.get(cid); } /** * @return the cid */ public String getCid() { return cid; } } public static final String LOGMSG_ID = "CpeValidator"; private static final String LOGREFDOC = "CPR"; protected HashMap cid2MDataMap; private CpeIP1Validator profileIP1Val; /** * @param loggingMgr */ public CpeValidator(LogMgmt loggingMgr) { super(true, loggingMgr); rootNS = manifestNSpace; logMsgSrcId = LOGMSG_ID; logMsgDefaultTag = LogMgmt.TAG_MANIFEST; profileIP1Val = new CpeIP1Validator(this, loggingMgr); } public boolean process(MddfTarget target, String profileId) throws JDOMException, IOException { super.process(target); if (!curFileIsValid) { String msg = "CPE validation terminated.. file is not a valid Media Manifest"; loggingMgr.log(LogMgmt.LEV_INFO, logMsgDefaultTag, msg, curTarget, logMsgSrcId); return curFileIsValid; } /* * Continue with additional checks for compliance with CPE-Manifest spec. */ /* * Note that validateConstraints() will invoke validateMetadata() which will in * turn initialize the 'cid2MDataMap'. */ validateConstraints(); DefaultTreeModel infoModel = buildInfoModel(); validateModel(infoModel); if (profileId == null || (profileId.isEmpty())) { return curFileIsValid; } if (!curFileIsValid) { String msg = "CPE validation terminated prior to Profile Validation.. file is not a valid CPE Manifest"; loggingMgr.log(LogMgmt.LEV_INFO, logMsgDefaultTag, msg, curTarget, logMsgSrcId); return curFileIsValid; } switch (profileId) { case "IP-0": /* * Profile IP-0 assumes no specific interactivity guidance within the Manifest * and supports any content structure. This is used when the Retailer determines * where and how bonus material is displayed. Validation is, therefore, not * required (i.e., it is equivalent to profile='none'. */ break; case "IP-01": String msg = "Profile ID 'IP-01' has been deprecated. 'IP-1' should be used instead."; loggingMgr.log(LogMgmt.LEV_WARN, logMsgDefaultTag, msg, curTarget, logMsgSrcId); case "IP-1": profileIP1Val.validateInfoModel(infoModel); break; default: msg = "Unrecognized CPE Profile '" + profileId + "'"; loggingMgr.log(LogMgmt.LEV_ERR, logMsgDefaultTag, msg, curTarget, logMsgSrcId); return false; } return curFileIsValid; } /** * Validate everything that is not fully specified via the XSD. */ protected void validateConstraints() { loggingMgr.log(LogMgmt.LEV_DEBUG, logMsgDefaultTag, "Validating constraints", curTarget, LOGMSG_ID); /* * Start with constraints defined for all Media Manifest files. */ super.validateConstraints(); } /* * (non-Javadoc) * * @see com.movielabs.mddflib.util.CMValidator#validateUsage() */ protected void validateUsage() { super.validateUsage(); /* * Load JSON that defines the BASIC (i.e., generic) CPE constraints. These are * independent of which CPE Profile is specified. */ String cur_cpe_struc_defs = "structure_cpe_v1.0"; JSONObject structDefs = XmlIngester.getMddfResource(cur_cpe_struc_defs); if (structDefs == null) { // LOG a FATAL problem. String msg = "Unable to process; missing CPE structure definitions " + cur_cpe_struc_defs; loggingMgr.log(LogMgmt.LEV_FATAL, LogMgmt.TAG_MODEL, msg, curTarget, logMsgSrcId); return; } JSONObject rqmtSet = structDefs.getJSONObject("StrucRqmts"); Iterator keys = rqmtSet.keys(); while (keys.hasNext()) { String key = keys.next(); JSONObject rqmtSpec = rqmtSet.getJSONObject(key); // NOTE: This block of code requires a 'targetPath' be defined if (rqmtSpec.has("targetPath")) { loggingMgr.log(LogMgmt.LEV_DEBUG, LogMgmt.TAG_MODEL, "Structure check; key= " + key, curTarget, logMsgSrcId); curFileIsValid = structHelper.validateDocStructure(curRootEl, rqmtSpec, curTarget, null) && curFileIsValid; } } return; } /** * Check for conformance with Sec 5.1 of CPE-Manifest (TR-CPE-M1) Specification. */ // protected void validateMetadata() { // super.validateMetadata(); // cid2MDataMap = new HashMap(); // /* // * Each Experience instance must include a ContentID element referencing // * metadata (i.e., ContentID is mandatory). The referenced metadata must be in // * the Inventory (i.e., Inventory/Metadata). The Metadata/Alias mechanism may be // * used. // */ // XPathExpression xpe1 = xpfac.compile(".//" + manifestNSpace.getPrefix() + ":Experience", // Filters.element(), null, manifestNSpace); // List elementList = xpe1.evaluate(curRootEl); // for (Element expEl : elementList) { // String cid = expEl.getChildTextNormalize("ContentID", manifestNSpace); // // ContentID is mandatory for CPE // if (cid == null || cid.isEmpty()) { // String msg = "Missing required ContentID"; // LogReference srcRef = LogReference.getRef(LOGREFDOC, "MData01"); // loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, expEl, msg, null, srcRef, logMsgSrcId); // curFileIsValid = false; // continue; // } // /* // * The referenced metadata must be in the Inventory (i.e., Inventory/Metadata). // */ // XPathExpression xpe2 = xpfac.compile( // ".//" + manifestNSpace.getPrefix() + ":Metadata[@ContentID='" + cid + "']", Filters.element(), null, // manifestNSpace); // Element metaDataEl = xpe2.evaluateFirst(curRootEl); // if (metaDataEl == null) { // String msg = "Missing required Metadata"; // String details = "Experience CID must reference metadata in Inventory"; // LogReference srcRef = LogReference.getRef(LOGREFDOC, "MData01"); // loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, expEl, msg, details, srcRef, logMsgSrcId); // curFileIsValid = false; // continue; // } else { // /* // * Find the BasicMetadata and save in HashMap to facilitate later processing. // * The Metadata/Alias mechanism may be used. // */ // Element basicMDEl = metaDataEl.getChild("BasicMetadata", manifestNSpace); // if (basicMDEl == null) { // // must be using indirect Alias mechanism. // Element aliasMDEl = metaDataEl.getChild("Alias", manifestNSpace); // if (aliasMDEl == null) { // /* // * Only way to pass XSD checks and reach this point is if they used a // * ContainerReference. // */ // String msg = "Missing required Metadata/BasicMetadata or Metadata/Alias"; // String details = "Experience CID must reference metadata in Inventory"; // LogReference srcRef = LogReference.getRef(LOGREFDOC, "MData01"); // loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, expEl, msg, details, srcRef, // logMsgSrcId); // curFileIsValid = false; // } else { // // make sure Alias points to BasicMetadata in Inventory // String aliasedCid = aliasMDEl.getAttributeValue("ContentID", "not specified"); // XPathExpression xpe3 = xpfac.compile( // ".//" + manifestNSpace.getPrefix() + ":BasicMetadata[@ContentID='" + aliasedCid + "']", // Filters.element(), null, manifestNSpace); // basicMDEl = xpe3.evaluateFirst(curRootEl); // if (basicMDEl == null) { // String msg = "Metadata/Alias does not reference BasicMetadata in Inventory"; // String details = "Experience CID must reference metadata in Inventory"; // LogReference srcRef = LogReference.getRef(LOGREFDOC, "MData01"); // loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, aliasMDEl, msg, details, srcRef, // logMsgSrcId); // curFileIsValid = false; // } // } // } // cid2MDataMap.put(cid, basicMDEl); // /* // * Any additional checking of the actual content of the metadata must wait until // * the Info Model has been built. // */ // } // } // } /** * Validates general model requirements (i.e., not specific to a given Profile). * This includes checking an Experience's metadata entry for compliance with the * Experience's position and context within the Information Model. See Section * 5.1.2.3 of [TR-CPE-M1] * * @param infoModel */ protected void validateModel(DefaultTreeModel infoModel) { ExperienceNode modelRoot = (ExperienceNode) infoModel.getRoot(); Enumeration kinder = modelRoot.children(); for (TreeNode topNode : Collections.list(kinder)) { validateTopMetadata((ExperienceNode) topNode); List descendants = ((ExperienceNode) topNode).getDescendents(); for (ExperienceNode lowerNode : descendants) { validateLowerMetadata(lowerNode); } } } /** * The following rules apply to top-level grouping nodes (i.e., those that are * not presented to the user), such as in-movie and out-of-movie in the * examples above: *
    *
  • There is only one instance of LocalizedInfo
  • *
  • That instance has TitleSort set to the name of the grouping category * (e.g., in-movie or out-of-movie).
  • *
  • LocalizedInfo@language may contain any language code, and it is * ignored.
  • *
  • No other metadata is present unless it is a required element or attribute * in the schema.
  • *
* * @param topNode */ private void validateTopMetadata(ExperienceNode topNode) { Element basicMDataEl = topNode.getMetadata(); List locElList = basicMDataEl.getChildren("LocalizedInfo", mdNSpace); if (locElList.size() != 1) { String errMsg = "top-level grouping nodes require EXACTLY one instance of LocalizedInfo"; loggingMgr.logIssue(LogMgmt.TAG_MODEL, LogMgmt.LEV_ERR, basicMDataEl, errMsg, null, null, LOGMSG_ID); curFileIsValid = false; } } /** *
    *
  • An instance of LocalizedInfo should be included for each language * supported for the title.
  • *
  • LocalizedInfo/TitleDisplayUnlimited and TitleSort contains the * user-visible name for that node. Note that even when an image is the intended * UI element, text should still be provided for accessibility (text to * speech).
  • *
  • LocalizedInfo/ArtReference includes images associated with the node. * Implementers note: Implementations should accept ImageID, PictureID, * PictureGroupID and URL.
  • *
* * @param lowerNode */ private void validateLowerMetadata(ExperienceNode lowerNode) { Element basicMDataEl = lowerNode.getMetadata(); List localMDataList = basicMDataEl.getChildren("LocalizedInfo", mdNSpace); for (int i = 0; i < localMDataList.size(); i++) { Element locMDEl = localMDataList.get(i); String title = locMDEl.getChildTextNormalize("TitleSort", mdNSpace); if (title.isEmpty()) { curFileIsValid = false; String errMsg = "TitleSort is empty"; loggingMgr.logIssue(LogMgmt.TAG_MODEL, LogMgmt.LEV_ERR, locMDEl, errMsg, null, null, logMsgSrcId); } String titleDU = locMDEl.getChildTextNormalize("TitleDisplayUnlimited", mdNSpace); if ((titleDU == null) || (titleDU.isEmpty())) { curFileIsValid = false; String errMsg = "TitleDisplayUnlimited is empty or missing"; loggingMgr.logIssue(LogMgmt.TAG_MODEL, LogMgmt.LEV_ERR, locMDEl, errMsg, null, null, logMsgSrcId); } } } /** * Identify all ALID and determine which experience is used when the ALID is * accessed by a consumer. * * @param root * @return */ public List extractAlidMap(Element root) { Set idSet = new HashSet(); List primaryExpSet = new ArrayList(); Element mapsEl = root.getChild("ALIDExperienceMaps", manifestNSpace); if (mapsEl == null) { return null; } List mapEList = mapsEl.getChildren("ALIDExperienceMap", manifestNSpace); Object[] targets = mapEList.toArray(); for (int i = 0; i < targets.length; i++) { Element target = (Element) targets[i]; /* get the values for ALID and ExperienceID */ Element expIdEl = target.getChild("ExperienceID", manifestNSpace); String expId = expIdEl.getTextNormalize(); if (!idSet.contains(expId)) { idSet.add(expId); XPathExpression xpExpression = xpfac.compile( ".//" + manifestNSpace.getPrefix() + ":Experience[@ExperienceID='" + expId + "']", Filters.element(), null, manifestNSpace); Element expEl = xpExpression.evaluateFirst(root); if (expEl != null) { primaryExpSet.add(expEl); } else { String errMsg = "Unable to locate ALID's root experience; expId = " + expId; loggingMgr.logIssue(LogMgmt.TAG_MODEL, LogMgmt.LEV_ERR, expIdEl, errMsg, null, null, LOGMSG_ID); curFileIsValid = false; } } } return primaryExpSet; } /** * Builds a hierarchical structure of Experience Elements to facilitate * validation of specific Information Models. */ protected DefaultTreeModel buildInfoModel() { ExperienceNode rootNode = new ExperienceNode(curRootEl); DefaultTreeModel infoModel = new DefaultTreeModel(rootNode); /* * Get all ALID and identify for each the 'root' (i.e., main) Experience element */ List primaryExpSet = extractAlidMap(curRootEl); if (primaryExpSet == null || (primaryExpSet.isEmpty())) { String msg = "Terminating Model validation due to empty or missing ALIDExperienceMaps"; loggingMgr.log(LogMgmt.LEV_ERR, LogMgmt.TAG_MODEL, msg, curTarget, LOGMSG_ID); curFileIsValid = false; return infoModel; } for (int i = 0; i < primaryExpSet.size(); i++) { Element nextExpEL = primaryExpSet.get(i); ExperienceNode nextExpNode = new ExperienceNode(nextExpEL); rootNode.add(nextExpNode); addChildExperiences(nextExpNode); } return infoModel; } /** * Expand hierarchical structure of Experience Elements by recursively * descending and adding all ExperienceChild elements found. * * @param nextExpNode */ private void addChildExperiences(ExperienceNode curExpNode) { Element curExpEl = curExpNode.getExpEl(); if (curExpEl == null) { return; } List allChildList = curExpEl.getChildren("ExperienceChild", manifestNSpace); // Recursively descend tree for (int i = 0; i < allChildList.size(); i++) { Element nextChildEl = allChildList.get(i); String expXRef = nextChildEl.getChildTextNormalize("ExperienceID", manifestNSpace); XPathExpression xpExpression = xpfac.compile( ".//manifest:Experience[@ExperienceID='" + expXRef + "']", Filters.element(), null, manifestNSpace); Element childExpEl = xpExpression.evaluateFirst(curRootEl); if (childExpEl == null) { String errMsg = "Unable to locate child experience; expId = " + expXRef; loggingMgr.logIssue(LogMgmt.TAG_MODEL, LogMgmt.LEV_ERR, nextChildEl, errMsg, null, null, LOGMSG_ID); curFileIsValid = false; } else { ExperienceNode nextExpNode = new ExperienceNode(childExpEl); curExpNode.add(nextExpNode); addChildExperiences(nextExpNode); } } } /** * Return an ordered list of child Elements that have the specified relationship * with the parent or null if unable to sort the children. A return value of * null is, therefore, not the same as returning an empty List as the * later indicates no matching child Elements were found rather than a problem * while sorting. *

* Ordering is determined in one of two ways. If <SequenceInfo> * elements are present they will be used to determine the order of the list * entries returned. Otherwise ordering is indeterminate. *

*

* Additional restrictions on the use of <SequenceInfo>: *

*
    *
  • <SequenceInfo> elements shall be used for either all of * the children or none of the children. Mixed usage is not allowed. *
  • SequenceInfo/Number starts with one and increases monotonically for each * child
  • *
* * @param parentEl * @param childName * @param relationship * @param seqInfoRequired * @return List of child Elements or null if unable to sort the * children. */ public List getSortedChildren(Element parentEl, String childName, String relationship, boolean seqInfoRequired) { boolean hasErrors = false; // .......... List allChildList = parentEl.getChildren(childName, manifestNSpace); Element[] firstPass = new Element[allChildList.size()]; int lastIndex = -1; for (int i = 0; i < allChildList.size(); i++) { Element nextEl = allChildList.get(i); String relType = nextEl.getChildTextNormalize("Relationship", manifestNSpace); if (relType == null) { String summaryMsg = "Missing Relationship element in " + childName + " element"; loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_WARN, nextEl, summaryMsg, null, null, logMsgSrcId); } else if (relType.equalsIgnoreCase(relationship)) { Element seqEl = nextEl.getChild("SequenceInfo", manifestNSpace); String seqNum = null; if (seqEl != null) { seqNum = seqEl.getChildTextNormalize("Number", mdNSpace); // check for empty string.. if (seqNum.isEmpty()) { seqNum = null; } } if (seqInfoRequired && (seqNum == null)) { // Flag as ERROR loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, seqEl, "Missing required SequenceInfo", null, null, logMsgSrcId); hasErrors = true; } else if (seqNum != null) { // seqNum should be monotonically increasing starting with 1 int index = -1; try { index = Integer.parseInt(seqNum) - 1; } catch (NumberFormatException e) { } if (index < 0) { loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, seqEl, "Invalid Number for SequenceInfo (must be a positive integer).", null, null, logMsgSrcId); hasErrors = true; } else if (index >= firstPass.length) { loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, seqEl, "Invalid Number for SequenceInfo (must increase monotonically).", null, null, logMsgSrcId); hasErrors = true; } else { if (firstPass[index] != null) { loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, seqEl, "Duplicate Number in SequenceInfo", null, null, logMsgSrcId); hasErrors = true; } else { firstPass[index] = nextEl; lastIndex = Math.max(lastIndex, index); } } } else { /* * Explicit SeqInfo is neither provided nor required so use as-is order */ firstPass[i] = nextEl; lastIndex = i; } } } if (hasErrors) { return null; } List childElList = new ArrayList(); /* * Transfer the Elements from the 'firstPass' array to the List. While * transferring, check to make sure the SequenceInfo/Number starts with one and * increases monotonically for each child. * */ for (int i = 0; i <= lastIndex; i++) { if (firstPass[i] != null) { childElList.add(firstPass[i]); } else { int gap = i + 1; loggingMgr.logIssue(logMsgDefaultTag, LogMgmt.LEV_ERR, parentEl, "Incomplete SequenceInfo... missing Number=" + gap, null, null, logMsgSrcId); hasErrors = true; } } if (hasErrors) { return null; } return childElList; } /** * @param expEl * @return */ public Element getMetadataEl(Element expEl) { String cid = expEl.getChildTextNormalize("ContentID", manifestNSpace); Element metaDataEl = cid2MDataMap.get(cid); if (metaDataEl == null) { // do it the hard way XPathExpression xpExpression = xpfac.compile(".//manifest:Metadata[@ContentID='" + cid + "']", Filters.element(), null, manifestNSpace); List elementList = xpExpression.evaluate(curRootEl); metaDataEl = elementList.get(0); } return metaDataEl; } /* * (non-Javadoc) * * @see com.movielabs.mddflib.manifest.validation.profiles.ProfileValidator# * getSupportedProfiles() */ @Override public List getSupportedProfiles() { // TODO Auto-generated method stub return null; } /* * (non-Javadoc) * * @see com.movielabs.mddflib.manifest.validation.profiles.ProfileValidator# * getSupporteUseCases(java.lang.String) */ @Override public List getSupporteUseCases(String profile) { // TODO Auto-generated method stub return null; } /* * (non-Javadoc) * * @see com.movielabs.mddflib.manifest.validation.profiles.ProfileValidator# * setLogger(com.movielabs.mddflib.logging.LogMgmt) */ @Override public void setLogger(LogMgmt logMgr) { // TODO Auto-generated method stub } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy