/* * Copyright 2013-2015 Skynav, Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.skynav.ttv.verifier.smpte; import java.util.List; import javax.xml.namespace.QName; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.xml.sax.Locator; import com.skynav.ttv.model.Model; import com.skynav.ttv.model.smpte.ST20522010; import com.skynav.ttv.model.smpte.tt.rel2010.Data; import com.skynav.ttv.model.smpte.tt.rel2010.Image; import com.skynav.ttv.model.smpte.tt.rel2010.Information; import com.skynav.ttv.util.Location; import com.skynav.ttv.util.Message; import com.skynav.ttv.util.Reporter; import com.skynav.ttv.util.URIs; import com.skynav.ttv.verifier.VerifierContext; import com.skynav.ttv.verifier.ttml.TTML1SemanticsVerifier; import com.skynav.ttv.verifier.util.Base64; import com.skynav.ttv.verifier.util.IdReferences; import com.skynav.ttv.verifier.util.Keywords; import com.skynav.ttv.verifier.util.Lengths; import com.skynav.ttv.verifier.util.MixedUnitsTreatment; import com.skynav.ttv.verifier.util.NegativeTreatment; import com.skynav.ttv.verifier.util.Strings; import com.skynav.xml.helpers.Nodes; import static com.skynav.ttv.model.smpte.ST20522010.Constants.*; public class ST20522010SemanticsVerifier extends TTML1SemanticsVerifier { public ST20522010SemanticsVerifier(Model model) { super(model); } public boolean inSMPTEPrimaryNamespace(QName name) { return ST20522010.inSMPTEPrimaryNamespace(name); } public boolean inSMPTESecondaryNamespace(QName name) { return ST20522010.inSMPTESecondaryNamespace(name); } public boolean inSMPTENamespace(QName name) { return inSMPTEPrimaryNamespace(name) || inSMPTESecondaryNamespace(name); } @Override public boolean verifyOtherElement(Object content, Locator locator, VerifierContext context) { if (!super.verifyOtherElement(content, locator, context)) return false; else return verifySMPTEOtherElement(content, locator, context); } protected boolean verifySMPTEOtherElement(Object content, Locator locator, VerifierContext context) { boolean failed = false; assert context == getContext(); Node node = context.getXMLNode(content); if (node == null) { if (content instanceof Element) node = (Element) content; } if (node != null) { String nsUri = node.getNamespaceURI(); String localName = node.getLocalName(); if (localName == null) localName = node.getNodeName(); QName name = new QName(nsUri != null ? nsUri : "", localName); Model model = getModel(); if (inSMPTENamespace(name)) { if (!model.isElement(name)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY", "Unknown element in SMPTE namespace ''{0}''.", name)); failed = true; } else if (isSMPTEImageElement(content)) { failed = !verifySMPTEImage(content, locator, context); } else if (isSMPTEInformationElement(content)) { failed = !verifySMPTEInformation(content, locator, context); } else if (isSMPTEDataElement(content)) { failed = !verifySMPTEData(content, locator, context); } else { return unexpectedContent(content); } } } return !failed; } protected boolean verifySMPTEElement(Object content, Locator locator, VerifierContext context) { boolean failed = false; assert context == getContext(); if (isSMPTEImageElement(content)) { failed = !verifySMPTEImage(content, locator, context); } else if (isSMPTEInformationElement(content)) { failed = !verifySMPTEInformation(content, locator, context); } else if (isSMPTEDataElement(content)) { failed = !verifySMPTEData(content, locator, context); } else { return unexpectedContent(content); } return !failed; } protected boolean isSMPTEDataElement(Object content) { return content instanceof Data; } protected boolean isSMPTEImageElement(Object content) { return content instanceof Image; } protected boolean isSMPTEInformationElement(Object content) { return content instanceof Information; } protected boolean verifySMPTEImage(Object image, Locator locator, VerifierContext context) { boolean failed = false; if (!verifyOtherAttributes(image)) failed = true; // Unable to test the following since no TT|SMPTE element type other than tt:metadata accepts xs:any children, // consequently, phase 3 (schema validation) will already have reported error if smpte:image isn't a child of tt:metadata. if (!verifyAncestry(image, locator, context)) failed = true; if (!verifyBase64Content(image, getImageValue(image), locator, context)) failed = true; return !failed; } protected String getImageValue(Object image) { return ((Image) image).getValue(); } protected boolean verifySMPTEInformation(Object information, Locator locator, VerifierContext context) { boolean failed = false; if (!verifyOtherAttributes(information)) failed = true; if (!verifyAncestry(information, locator, context)) failed = true; if (!verifyDuplicate(information, locator, context)) failed = true; return !failed; } private boolean verifyDuplicate(Object information, Locator locator, VerifierContext context) { boolean failed = false; String key = getModel().getName() + ".informationAlreadyPresent"; Boolean informationAlreadyPresent = (Boolean) context.getResourceState(key); if ((informationAlreadyPresent != null) && informationAlreadyPresent) { QName name = context.getBindingElementName(information); Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "SMPTE element '" + name + "' is already present, only one instance allowed.", name)); failed = true; } else { context.setResourceState(key, Boolean.TRUE); } return !failed; } protected boolean verifySMPTEData(Object data, Locator locator, VerifierContext context) { boolean failed = false; if (!verifyDataType(data, getDataDatatype(data), locator, context)) failed = true; if (!verifyOtherAttributes(data)) failed = true; // Unable to test the following since no TT|SMPTE element type other than tt:metadata accepts xs:any children, // consequently, phase 3 (schema validation) will already have reported error if smpte:image isn't a child of tt:metadata. if (!verifyAncestry(data, locator, context)) failed = true; if (!verifyBase64Content(data, getDataValue(data), locator, context)) failed = true; return !failed; } protected String getDataDatatype(Object data) { return ((Data) data).getDatatype(); } protected String getDataValue(Object data) { return ((Data) data).getValue(); } private final QName dataTypeAttributeName = new QName("", "datatype"); private boolean verifyDataType(Object data, String value, Locator locator, VerifierContext context) { boolean failed = false; QName name = dataTypeAttributeName; if (!verifyNonEmptyOrPadded(data, name, value, locator, context)) failed = true; else if (!verifyDataType(data, name, value, locator, context)) failed = true; if (failed) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "Invalid {0} value ''{1}''.", name, value)); } return !failed; } private boolean verifyDataType(Object data, QName name, String value, Locator locator, VerifierContext context) { if (isStandardDataType(value)) return true; else if (isPrivateDataType(value)) return true; else { Reporter reporter = context.getReporter(); reporter.logInfo(reporter.message(locator, "*KEY*", "Non-standard {0} must start with 'x-' prefix, got ''{1}''.", value)); return false; } } protected boolean isStandardDataType(String dataType) { return dataType.equals(DATA_TYPE_608); } private boolean isPrivateDataType(String dataType) { return dataType.indexOf("x-") == 0; } protected boolean verifyAncestry(Object content, Locator locator, VerifierContext context) { boolean failed = false; Node node = context.getXMLNode(content); if (node == null) { if (content instanceof Element) node = (Element) content; } if (node != null) { QName name = context.getBindingElementName(content); List<List<QName>> ancestors = getModel().getElementPermissibleAncestors(name); if (ancestors != null) { if (!Nodes.hasAncestors(node, ancestors)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "Element ''{0}'' must have ancestors {1}.", name, ancestors)); failed = true; } } } return !failed; } private boolean verifyBase64Content(Object content, String value, Locator locator, VerifierContext context) { boolean failed = false; QName name = context.getBindingElementName(content); try { Base64.decode(value); } catch (IllegalArgumentException e) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "SMPTE element ''{0}'' content does not conform to Base64 encoding: {1}", name, e.getMessage())); failed = true; } return !failed; } @Override public boolean verifyOtherAttributes(Object content, Locator locator, VerifierContext context) { if (!super.verifyOtherAttributes(content, locator, context)) return false; else return verifySMPTEOtherAttributes(content, locator, context); } protected boolean verifySMPTEOtherAttributes(Object content, Locator locator, VerifierContext context) { boolean failed = false; NamedNodeMap attributes = context.getXMLNode(content).getAttributes(); for (int i = 0, n = attributes.getLength(); i < n; ++i) { boolean failedAttribute = false; Node item = attributes.item(i); if (!(item instanceof Attr)) continue; Attr attribute = (Attr) item; String nsUri = attribute.getNamespaceURI(); String localName = attribute.getLocalName(); if (localName == null) localName = attribute.getName(); if (localName.indexOf("xmlns") == 0) continue; QName name = new QName(nsUri != null ? nsUri : "", localName); Model model = getModel(); if (model.isNamespace(name.getNamespaceURI())) { if (name.getNamespaceURI().indexOf(NAMESPACE_PREFIX) == 0) { Reporter reporter = context.getReporter(); String value = attribute.getValue(); if (!model.isGlobalAttribute(name)) { reporter.logError(reporter.message(locator, "*KEY*", "Unknown attribute in SMPTE namespace ''{0}'' not permitted on ''{1}''.", name, context.getBindingElementName(content))); failedAttribute = true; } else if (!model.isGlobalAttributePermitted(name, context.getBindingElementName(content))) { reporter.logError(reporter.message(locator, "*KEY*", "SMPTE attribute ''{0}'' not permitted on ''{1}''.", name, context.getBindingElementName(content))); failedAttribute = true; } else if (!verifyNonEmptyOrPadded(content, name, value, locator, context)) { reporter.logError(reporter.message(locator, "*KEY*", "Invalid {0} value ''{1}''.", name, value)); failedAttribute = true; } else if (!verifySMPTEAttribute(content, locator, context, name, value)) { reporter.logError(reporter.message(locator, "*KEY*", "Invalid or prohibited {0} attribute or attribute value ''{1}''.", name, value)); failedAttribute = true; } } } if (failedAttribute) failed = failedAttribute; } return !failed; } protected boolean verifySMPTEAttribute(Object content, Locator locator, VerifierContext context, QName name, String value) { boolean failed = false; if (isBackgroundImageAttribute(name)) { if (!verifySMPTEBackgroundImage(content, name, value, locator, context)) failed = true; } else if (isBackgroundImageHVAttribute(name)) { if (!verifySMPTEBackgroundImageHV(content, name, value, locator, context)) failed = true; } return !failed; } protected boolean verifyNonEmptyOrPadded(Object content, QName name, String value, Locator locator, VerifierContext context) { Reporter reporter = context.getReporter(); if (value.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Empty {0} not permitted, got ''{1}''.", name, value)); return false; } else if (Strings.isAllXMLSpace(value)) { reporter.logInfo(reporter.message(locator, "*KEY*", "The value of {0} is entirely XML space characters, got ''{1}''.", name, value)); return false; } else if (!value.equals(value.trim())) { reporter.logInfo(reporter.message(locator, "*KEY*", "XML space padding not permitted on {0}, got ''{1}''.", name, value)); return false; } else return true; } private static final QName backgroundImageAttributeName = new QName(NAMESPACE_2010, "backgroundImage"); protected QName getBackgroundImageAttributeName() { return backgroundImageAttributeName; } private boolean isBackgroundImageAttribute(QName name) { return name.equals(getBackgroundImageAttributeName()); } protected boolean verifySMPTEBackgroundImage(Object content, QName name, Object valueObject, Locator locator, VerifierContext context) { boolean failed = false; assert valueObject instanceof String; String value = (String) valueObject; Reporter reporter = context.getReporter(); if (URIs.isLocalFragment(value)) { String id = URIs.getFragment(value); assert id != null; Node node = context.getXMLNode(content); if (node == null) { if (content instanceof Element) node = (Element) content; } if (node != null) { Document document = node.getOwnerDocument(); if (document != null) { Element targetElement = document.getElementById(id); if (targetElement != null) { Object target = context.getBindingElement(targetElement); if (target != null) { QName targetName = context.getBindingElementName(target); if (!isImageElement(targetName)) { Location location = new Location(content, context.getBindingElementName(content), name, locator); IdReferences.badReference(target, location, context, name, getImageElementName()); failed = true; } } } else { reporter.logInfo(reporter.message(locator, "*KEY*", "SMPTE attribute {0} references local image element ''{1}'', but no corresponding element is present.", name, value)); failed = true; } } } } else { if (reporter.isWarningEnabled("references-external-image")) { Message message = reporter.message(locator, "*KEY*", "SMPTE attribute {0} references external image at ''{1}''.", name, value);; if (reporter.logWarning(message)) { reporter.logError(message); failed = true; } } } return !failed; } private static final QName imageElementName = new QName(NAMESPACE_2010, ELT_IMAGE); protected QName getImageElementName() { return imageElementName; } private boolean isImageElement(QName name) { return name.equals(getImageElementName()); } private boolean isBackgroundImageHVAttribute(QName name) { String ln = name.getLocalPart(); return inSMPTEPrimaryNamespace(name) && (ln.equals(ATTR_BACKGROUND_IMAGE_HORIZONTAL) || ln.equals(ATTR_BACKGROUND_IMAGE_VERTICAL)); } protected boolean verifySMPTEBackgroundImageHV(Object content, QName name, Object valueObject, Locator locator, VerifierContext context) { assert valueObject instanceof String; String value = (String) valueObject; Integer[] minMax = new Integer[] { 1, 1 }; Object[] treatments = new Object[] { NegativeTreatment.Error, MixedUnitsTreatment.Error }; Location location = new Location(content, context.getBindingElementName(content), name, locator); if (Keywords.isKeyword(value)) { return isBackgroundImageHVKeyword(name, value); } else if (Lengths.isLengths(value, location, context, minMax, treatments, null)) { return true; } else { Lengths.badLengths(value, location, context, minMax, treatments); return false; } } private boolean isBackgroundImageHVKeyword(QName name, String value) { String ln = name.getLocalPart(); if (ln.equals(ATTR_BACKGROUND_IMAGE_HORIZONTAL)) { if (value.equals("left")) return true; else if (value.equals("center")) return true; else if (value.equals("right")) return true; else return false; } else if (ln.equals(ATTR_BACKGROUND_IMAGE_VERTICAL)) { if (value.equals("top")) return true; else if (value.equals("center")) return true; else if (value.equals("bottom")) return true; else return false; } else { return false; } } }