/*
* Copyright 2013-2016 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.ttml;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import org.w3c.dom.Attr;
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.ttml1.tt.TimedText;
import com.skynav.ttv.model.ttml1.ttd.ClockMode;
import com.skynav.ttv.model.ttml1.ttd.DropMode;
import com.skynav.ttv.model.ttml1.ttd.MarkerMode;
import com.skynav.ttv.model.ttml1.ttd.TimeBase;
import com.skynav.ttv.model.ttml1.ttp.Extensions;
import com.skynav.ttv.model.ttml1.ttp.Features;
import com.skynav.ttv.util.Annotations;
import com.skynav.ttv.util.ComparableQName;
import com.skynav.ttv.util.Enums;
import com.skynav.ttv.util.Location;
import com.skynav.ttv.util.Reporter;
import com.skynav.ttv.verifier.AbstractVerifier;
import com.skynav.ttv.verifier.ParameterValueVerifier;
import com.skynav.ttv.verifier.ParameterVerifier;
import com.skynav.ttv.verifier.VerifierContext;
import com.skynav.ttv.verifier.ttml.parameter.BaseVerifier;
import com.skynav.ttv.verifier.ttml.parameter.CellResolutionVerifier;
import com.skynav.ttv.verifier.ttml.parameter.ClockModeVerifier;
import com.skynav.ttv.verifier.ttml.parameter.DropModeVerifier;
import com.skynav.ttv.verifier.ttml.parameter.FrameRateMultiplierVerifier;
import com.skynav.ttv.verifier.ttml.parameter.FrameRateVerifier;
import com.skynav.ttv.verifier.ttml.parameter.MarkerModeVerifier;
import com.skynav.ttv.verifier.ttml.parameter.PixelAspectRatioVerifier;
import com.skynav.ttv.verifier.ttml.parameter.ProfileVerifier;
import com.skynav.ttv.verifier.ttml.parameter.SubFrameRateVerifier;
import com.skynav.ttv.verifier.ttml.parameter.TickRateVerifier;
import com.skynav.ttv.verifier.ttml.parameter.TimeBaseVerifier;
import com.skynav.ttv.verifier.util.Strings;
import com.skynav.xml.helpers.XML;
import static com.skynav.ttv.model.ttml.TTML1.Constants.*;
public class TTML1ParameterVerifier extends AbstractVerifier implements ParameterVerifier {
public static final String NAMESPACE = NAMESPACE_TT_PARAMETER;
public static final QName cellResolutionAttributeName = new QName(NAMESPACE, "cellResolution");
public static final QName clockModeAttributeName = new QName(NAMESPACE, "clockMode");
public static final QName dropModeAttributeName = new QName(NAMESPACE, "dropMode");
public static final QName frameRateAttributeName = new QName(NAMESPACE, "frameRate");
public static final QName frameRateMultiplierAttributeName = new QName(NAMESPACE, "frameRateMultiplier");
public static final QName markerModeAttributeName = new QName(NAMESPACE, "markerMode");
public static final QName pixelAspectRatioAttributeName = new QName(NAMESPACE, "pixelAspectRatio");
public static final QName profileAttributeName = new QName(NAMESPACE, "profile");
public static final QName subFrameRateAttributeName = new QName(NAMESPACE, "subFrameRate");
public static final QName tickRateAttributeName = new QName(NAMESPACE, "tickRate");
public static final QName timeBaseAttributeName = new QName(NAMESPACE, "timeBase");
public static final QName useAttributeName = new QName("", "use");
public static final QName xmlBaseAttributeName = XML.getBaseAttributeName();
// internal only parameters
public static final QName defaultedParametersAttributeName =
new QName(Annotations.getNamespace(), "defaultedParameters", Annotations.getNamespacePrefix());
private static final Object[][] parameterAccessorMap = new Object[][] {
{
cellResolutionAttributeName, // attribute name
"CellResolution", // accessor method name suffix
String.class, // value type
CellResolutionVerifier.class, // specialized verifier
Boolean.FALSE, // padding permitted
"32 15", // default value
},
{
clockModeAttributeName,
"ClockMode",
ClockMode.class,
ClockModeVerifier.class,
Boolean.FALSE,
ClockMode.UTC,
},
{
dropModeAttributeName,
"DropMode",
DropMode.class,
DropModeVerifier.class,
Boolean.FALSE,
DropMode.NON_DROP,
},
{
frameRateAttributeName,
"FrameRate",
BigInteger.class,
FrameRateVerifier.class,
Boolean.TRUE,
BigInteger.valueOf(30),
},
{
frameRateMultiplierAttributeName,
"FrameRateMultiplier",
String.class,
FrameRateMultiplierVerifier.class,
Boolean.FALSE,
"1 1",
},
{
markerModeAttributeName,
"MarkerMode",
MarkerMode.class,
MarkerModeVerifier.class,
Boolean.FALSE,
MarkerMode.DISCONTINUOUS,
},
{
pixelAspectRatioAttributeName,
"PixelAspectRatio",
String.class,
PixelAspectRatioVerifier.class,
Boolean.FALSE,
"1 1",
},
{
profileAttributeName,
"Profile",
String.class,
ProfileVerifier.class,
Boolean.FALSE,
null,
},
{
subFrameRateAttributeName,
"SubFrameRate",
BigInteger.class,
SubFrameRateVerifier.class,
Boolean.FALSE,
BigInteger.valueOf(1),
},
{
tickRateAttributeName,
"TickRate",
BigInteger.class,
TickRateVerifier.class,
Boolean.FALSE,
BigInteger.valueOf(1),
},
{
timeBaseAttributeName,
"TimeBase",
TimeBase.class,
TimeBaseVerifier.class,
Boolean.FALSE,
TimeBase.MEDIA,
},
// 'use' attribute applies only to ttp:profile element
{
useAttributeName,
"Use",
String.class,
ProfileVerifier.class,
Boolean.FALSE,
null,
},
// 'xml:base' attribute applies only to ttp:features and ttp:extensions elements
{
xmlBaseAttributeName,
"Base",
String.class,
BaseVerifier.class,
Boolean.FALSE,
null,
},
};
private Map<QName, ParameterAccessor> accessors;
public TTML1ParameterVerifier(Model model) {
super(model);
populate();
}
public QName getParameterAttributeName(String parameterName) {
// assumes that parameter name is same as local part of qualified attribute name, which
// is presently true in TTML1
for (QName name : accessors.keySet()) {
if (parameterName.equals(name.getLocalPart()))
return name;
}
return null;
}
public boolean verify(Object content, Locator locator, VerifierContext context, ItemType type) {
setState(content, context);
if (type == ItemType.Attributes)
return verifyAttributeItems(content, locator, context);
else if (type == ItemType.Other)
return verifyOtherAttributes(content, locator, context);
else
throw new IllegalArgumentException();
}
protected boolean verifyAttributeItems(Object content, Locator locator, VerifierContext context) {
boolean failed = false;
for (ParameterAccessor pa : accessors.values()) {
if (!pa.verify(getModel(), content, locator, context))
failed = true;
}
return !failed;
}
protected boolean verifyOtherAttributes(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) {
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);
if (name.getNamespaceURI().equals(NAMESPACE)) {
Reporter reporter = context.getReporter();
if (!isParameterAttribute(name)) {
reporter.logError(reporter.message(locator, "*KEY*", "Unknown attribute in TT Parameter namespace ''{0}'' not permitted on ''{1}''.",
name, context.getBindingElementName(content)));
failed = true;
} else if (!permitsParameterAttribute(content, name)) {
reporter.logError(reporter.message(locator, "*KEY*", "TT Parameter attribute ''{0}'' not permitted on ''{1}''.",
name, context.getBindingElementName(content)));
failed = true;
}
}
}
return !failed;
}
protected boolean permitsParameterAttribute(Object content, QName name) {
if (content instanceof TimedText)
return true;
else
return false;
}
protected boolean isParameterAttribute(QName name) {
return name.getNamespaceURI().equals(NAMESPACE) && accessors.containsKey(name);
}
private void populate() {
this.accessors = makeAccessors();
}
private Map<QName, ParameterAccessor> makeAccessors() {
Map<QName, ParameterAccessor> accessors = new java.util.HashMap<QName, ParameterAccessor>();
populateAccessors(accessors);
return accessors;
}
protected void populateAccessors(Map<QName, ParameterAccessor> accessors) {
populateAccessors(accessors, parameterAccessorMap);
}
protected void populateAccessors(Map<QName, ParameterAccessor> accessors, Object[][] accessorMap) {
for (Object[] parameterAccessorEntry : accessorMap) {
assert parameterAccessorEntry.length >= 6;
QName parameterName = (QName) parameterAccessorEntry[0];
String accessorName = (String) parameterAccessorEntry[1];
Class<?> valueClass = (Class<?>) parameterAccessorEntry[2];
Class<?> verifierClass = (Class<?>) parameterAccessorEntry[3];
boolean paddingPermitted = ((Boolean) parameterAccessorEntry[4]).booleanValue();
Object defaultValue = parameterAccessorEntry[5];
accessors.put(parameterName, new ParameterAccessor(parameterName, accessorName, valueClass, verifierClass, paddingPermitted, defaultValue));
}
}
protected boolean setParameterDefaultValue(Object content, ParameterAccessor pa, Object defaultValue) {
if (content instanceof TimedText) {
if (defaultValue != null) {
setParameterValue(content, pa.setterName, pa.valueClass, defaultValue);
return true;
}
} else if ((content instanceof Features) || (content instanceof Extensions)) {
if (pa.parameterName.equals(XML.getBaseAttributeName())) {
Model model = getModel();
if (content instanceof Features)
defaultValue = model.getFeatureNamespaceUri().toString();
else if (content instanceof Extensions)
defaultValue = model.getExtensionNamespaceUri().toString();
if (defaultValue != null) {
setParameterValue(content, pa.setterName, pa.valueClass, defaultValue);
return true;
}
}
}
return false;
}
protected void setParameterValue(Object content, String setterName, Class<?> valueClass, Object value) {
try {
Class<?> contentClass = content.getClass();
Method m = contentClass.getMethod(setterName, new Class<?>[]{ valueClass });
m.invoke(content, new Object[]{ value });
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
}
}
protected class ParameterAccessor {
QName parameterName;
String getterName;
String setterName;
Class<?> valueClass;
ParameterValueVerifier verifier;
boolean paddingPermitted;
Object defaultValue;
public ParameterAccessor(QName parameterName, String accessorName, Class<?> valueClass, Class<?> verifierClass, boolean paddingPermitted, Object defaultValue) {
populate(parameterName, accessorName, valueClass, verifierClass, paddingPermitted, defaultValue);
}
public boolean verify(Model model, Object content, Locator locator, VerifierContext context) {
boolean success = true;
Object value = getParameterValue(content);
if (value != null) {
Location location = new Location(content, context.getBindingElementName(content), parameterName, locator);
if (value instanceof String)
success = verify((String) value, location, context);
else
success = verifier.verify(value, location, context);
} else if (setParameterDefaultValue(content))
recordDefaultedParameter(model, content, parameterName, locator, context);
if (!success) {
Reporter reporter = context.getReporter();
reporter.logError(reporter.message(locator,
"*KEY*", "Invalid {0} value ''{1}''.", parameterName, value));
}
return success;
}
private boolean verify(String value, Location location, VerifierContext context) {
boolean success = false;
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
if (value.length() == 0) {
reporter.logInfo(reporter.message(locator,
"*KEY*", "Empty {0} not permitted, got ''{1}''.", parameterName, value));
} else if (Strings.isAllXMLSpace(value)) {
reporter.logInfo(reporter.message(locator,
"*KEY*", "The value of {0} is entirely XML space characters, got ''{1}''.", parameterName, value));
} else if (!paddingPermitted && !value.equals(value.trim())) {
reporter.logInfo(reporter.message(locator,
"*KEY*", "XML space padding not permitted on {0}, got ''{1}''.", parameterName, value));
} else
success = verifier.verify(value, location, context);
return success;
}
private ParameterValueVerifier createParameterValueVerifier(Class<?> verifierClass) {
try {
return (ParameterValueVerifier) verifierClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private void populate(QName parameterName, String accessorName, Class<?> valueClass, Class<?> verifierClass, boolean paddingPermitted, Object defaultValue) {
this.parameterName = parameterName;
this.getterName = "get" + accessorName;
this.setterName = "set" + accessorName;
this.valueClass = valueClass;
this.verifier = createParameterValueVerifier(verifierClass);
this.paddingPermitted = paddingPermitted;
this.defaultValue = defaultValue;
}
private Object getParameterValue(Object content) {
try {
Class<?> contentClass = content.getClass();
Method m = contentClass.getMethod(getterName, new Class<?>[]{});
return convertType(m.invoke(content, new Object[]{}), valueClass);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
return null;
} catch (SecurityException e) {
throw new RuntimeException(e);
}
}
private boolean setParameterDefaultValue(Object content) {
return TTML1ParameterVerifier.this.setParameterDefaultValue(content, this, defaultValue);
}
private Object convertType(Object value, Class<?> targetClass) {
if (value == null)
return null;
else if (targetClass.isInstance(value))
return value;
else if (value.getClass() == String.class) {
if (targetClass == Float.class) {
try {
return Float.valueOf((String) value);
} catch (NumberFormatException e) {
return null;
}
} else
return null;
} else if (value.getClass() == BigInteger.class) {
if (targetClass == String.class)
return ((BigInteger) value).toString();
else
return null;
} else if (value instanceof Enum<?>) {
if (targetClass == String.class)
return Enums.getValue((Enum<?>) value);
else
return null;
} else
return null;
}
}
private static void recordDefaultedParameter(Model model, Object content, QName parameterName, Locator locator, VerifierContext context) {
Map<QName,String> otherAttributes = Annotations.getOtherAttributes(content);
Set<ComparableQName> defaulted = ComparableQName.parseNames(otherAttributes.get(defaultedParametersAttributeName));
ComparableQName pn = new ComparableQName(parameterName);
if (!defaulted.contains(pn)) {
defaulted.add(pn);
otherAttributes.put(defaultedParametersAttributeName, ComparableQName.toString(defaulted));
}
}
public static final boolean isParameterDefaulted(Object content, QName name) {
Map<QName,String> otherAttributes = Annotations.getOtherAttributes(content);
Set<ComparableQName> defaulted = ComparableQName.parseNames(otherAttributes.get(defaultedParametersAttributeName));
return defaulted.contains(name);
}
public static final boolean isFrameRateDefaulted(Object content) {
return isParameterDefaulted(content, new ComparableQName(frameRateAttributeName));
}
public static final boolean isTickRateDefaulted(Object content) {
return isParameterDefaulted(content, new ComparableQName(tickRateAttributeName));
}
}