/*
* 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.ttx.transformer.isd;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import com.skynav.ttv.app.InvalidOptionUsageException;
import com.skynav.ttv.app.MissingOptionArgumentException;
import com.skynav.ttv.app.OptionSpecification;
import com.skynav.ttv.app.UnknownOptionException;
import com.skynav.ttv.model.Model;
import com.skynav.ttv.model.value.TimeParameters;
import com.skynav.ttv.util.Annotations;
import com.skynav.ttv.util.ComparableQName;
import com.skynav.ttv.util.Condition;
import com.skynav.ttv.util.IOUtil;
import com.skynav.ttv.util.Namespaces;
import com.skynav.ttv.util.PostVisitor;
import com.skynav.ttv.util.PreVisitor;
import com.skynav.ttv.util.Reporter;
import com.skynav.ttv.util.StyleSet;
import com.skynav.ttv.util.StyleSpecification;
import com.skynav.ttv.util.Traverse;
import com.skynav.ttv.util.Visitor;
import com.skynav.ttv.verifier.ttml.timing.TimingVerificationParameters;
import com.skynav.ttx.transformer.AbstractTransformer;
import com.skynav.ttx.transformer.TransformerContext;
import com.skynav.ttx.transformer.TransformerException;
import com.skynav.ttx.util.DirectedGraph;
import com.skynav.ttx.util.TimeCoordinate;
import com.skynav.ttx.util.TimeInterval;
import com.skynav.ttx.util.TopologicalSort;
import com.skynav.xml.helpers.XML;
public class ISD {
public static final String TRANSFORMER_NAME = "isd";
private static final String DEFAULT_OUTPUT_ENCODING = AbstractTransformer.DEFAULT_OUTPUT_ENCODING;
private static final Charset defaultOutputEncoding;
private static final String defaultOutputFileNamePattern = "isdi{0,number,000000}.xml";
static {
Charset de;
try {
de = Charset.forName(DEFAULT_OUTPUT_ENCODING);
} catch (RuntimeException e) {
de = Charset.defaultCharset();
}
defaultOutputEncoding = de;
}
// option and usage info
private static final String[][] longOptionSpecifications = new String[][] {
{ "isd-output-clean", "", "clean (remove) all files in output directory prior to writing ISD output" },
{ "isd-output-directory", "DIRECTORY","specify path to directory where ISD output is to be written" },
{ "isd-output-encoding", "ENCODING", "specify character encoding of ISD output (default: " + defaultOutputEncoding.name() + ")" },
{ "isd-output-indent", "", "indent ISD output (default: no indent)" },
{ "isd-output-pattern", "PATTERN", "specify ISD output file name pattern (default: 'isd00000')" },
};
private static final Collection<OptionSpecification> longOptions;
static {
Set<OptionSpecification> s = new java.util.TreeSet<OptionSpecification>();
for (String[] spec : longOptionSpecifications) {
s.add(new OptionSpecification(spec[0], spec[1], spec[2]));
}
longOptions = Collections.unmodifiableSet(s);
}
public static ISDHelper getHelper(TransformerContext context) {
return (ISDHelper) context.getResourceState(ResourceState.isdHelper.name());
}
@SuppressWarnings("unchecked")
public static Map<Object,Object> getParents(TransformerContext context) {
return (Map<Object,Object>) context.getResourceState(ResourceState.isdParents.name());
}
@SuppressWarnings("unchecked")
public static Map<Object,TimingState> getTimingStates(TransformerContext context) {
return (Map<Object,TimingState>) context.getResourceState(ResourceState.isdTimingStates.name());
}
public static class ISDTransformer extends AbstractTransformer {
// options state
private boolean outputDirectoryClean;
private String outputDirectoryPath;
private String outputEncodingName;
private boolean outputIndent;
private String outputPattern;
// derived option state
private File outputDirectory;
private Charset outputEncoding;
private MessageFormat outputPatternFormatter;
public ISDTransformer(TransformerContext context) {
super(context);
}
public void resetAllState(boolean restart) {
resetResourceState(restart);
resetOptionsState(restart);
resetGlobalState(restart);
}
private void resetResourceState(boolean restart) {
}
private void resetOptionsState(boolean restart) {
outputDirectoryClean = false;
outputDirectoryPath = null;
outputEncodingName = null;
outputIndent = false;
outputPattern = null;
outputDirectory = null;
outputEncoding = null;
outputPatternFormatter = null;
}
private void resetGlobalState(boolean restart) {
}
public String getName() {
return TRANSFORMER_NAME;
}
@Override
public Collection<OptionSpecification> getShortOptionSpecs() {
return null;
}
@Override
public Collection<OptionSpecification> getLongOptionSpecs() {
return longOptions;
}
@Override
public int parseLongOption(List<String> args, int index) {
String arg = args.get(index);
int numArgs = args.size();
String option = arg;
assert option.length() > 2;
option = option.substring(2);
if (option.equals("isd-output-clean")) {
outputDirectoryClean = true;
} else if (option.equals("isd-output-directory")) {
if (index + 1 > numArgs)
throw new MissingOptionArgumentException("--" + option);
outputDirectoryPath = args.get(++index);
} else if (option.equals("isd-output-encoding")) {
if (index + 1 > numArgs)
throw new MissingOptionArgumentException("--" + option);
outputEncodingName = args.get(++index);
} else if (option.equals("isd-output-indent")) {
outputIndent = true;
} else if (option.equals("isd-output-pattern")) {
if (index + 1 > numArgs)
throw new MissingOptionArgumentException("--" + option);
outputPattern = args.get(++index);
} else
index = index - 1;
return index + 1;
}
@Override
public int parseShortOption(List<String> args, int index) {
String arg = args.get(index);
String option = arg;
assert option.length() == 2;
option = option.substring(1);
throw new UnknownOptionException("-" + option);
}
@Override
public void processDerivedOptions() {
// output directory
File outputDirectory;
if (outputDirectoryPath != null) {
outputDirectory = new File(outputDirectoryPath);
if (!outputDirectory.exists())
throw new InvalidOptionUsageException("isd-output-directory", "directory does not exist: " + outputDirectoryPath);
else if (!outputDirectory.isDirectory())
throw new InvalidOptionUsageException("isd-output-directory", "not a directory: " + outputDirectoryPath);
} else
outputDirectory = new File(".");
this.outputDirectory = outputDirectory;
// output encoding
Charset outputEncoding;
if (outputEncodingName != null) {
try {
outputEncoding = Charset.forName(outputEncodingName);
} catch (Exception e) {
outputEncoding = null;
}
if (outputEncoding == null)
throw new InvalidOptionUsageException("isd-output-encoding", "unknown encoding: " + outputEncodingName);
} else
outputEncoding = null;
if (outputEncoding == null)
outputEncoding = defaultOutputEncoding;
this.outputEncoding = outputEncoding;
// output pattern
String outputPattern = this.outputPattern;
if (outputPattern == null)
outputPattern = defaultOutputFileNamePattern;
this.outputPattern = outputPattern;
this.outputPatternFormatter = new MessageFormat(outputPattern, Locale.US);
}
@Override
public void transform(List<String> args, Object root, OutputStream out) {
assert root != null;
assert context != null;
populateContext(context);
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message("*KEY*", "Transforming result using ''{0}'' transformer...", getName()));
// extract significant time intervals
Set<TimeInterval> intervals = extractISDIntervals(root, context);
if (reporter.getDebugLevel() > 0) {
StringBuffer sb = new StringBuffer();
for (TimeInterval interval : intervals) {
if (sb.length() > 0)
sb.append(',');
sb.append(interval);
}
reporter.logDebug(reporter.message("*KEY*", "Resolved active intervals: {0}.", "{" + sb + "}"));
}
writeISDSequence(root, context, intervals, out);
}
private void populateContext(TransformerContext context) {
context.setResourceState(ResourceState.isdGenerationIndices.name(), new int[GenerationIndex.values().length]);
context.setResourceState(ResourceState.isdHelper.name(), ISDHelper.makeInstance(context.getModel()));
context.setResourceState(ResourceState.isdParents.name(), new java.util.HashMap<Object, Object>());
context.setResourceState(ResourceState.isdTimingStates.name(), new java.util.HashMap<Object, TimingState>());
context.setResourceState(ResourceState.isdUsesForcedVisibility.name(), Boolean.FALSE);
context.setResourceState(ResourceState.isdUsesTTML2Feature.name(), Boolean.FALSE);
}
private Set<TimeInterval> extractISDIntervals(Object root, TransformerContext context) {
Reporter reporter = context.getReporter();
getHelper(context).generateAnonymousSpans(root, context);
recordParents(root, context);
resolveTiming(root, context);
Set<TimeCoordinate> coordinates = new java.util.TreeSet<TimeCoordinate>();
for (TimeInterval interval : extractActiveIntervals(root, context)) {
coordinates.add(interval.getBegin());
coordinates.add(interval.getEnd());
}
if (reporter.getDebugLevel() > 0) {
StringBuffer sb = new StringBuffer();
for (TimeCoordinate coordinate : coordinates) {
if (sb.length() > 0)
sb.append(',');
sb.append(coordinate);
}
reporter.logDebug(reporter.message("*KEY*", "Resolved active coordinates: {0}.", "{" + sb + "}"));
}
Set<TimeInterval> intervals = new java.util.TreeSet<TimeInterval>();
TimeCoordinate lastCoordinate = null;
for (TimeCoordinate coordinate : coordinates) {
if (lastCoordinate != null)
intervals.add(new TimeInterval(lastCoordinate, coordinate));
lastCoordinate = coordinate;
}
return intervals;
}
private void recordParents(Object root, final TransformerContext context) {
try {
getHelper(context).traverse(root, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
Map<Object,Object> parents = getParents(context);
if (!parents.containsKey(content))
parents.put(content, parent);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private void resolveTiming(Object root, TransformerContext context) {
resolveExplicit(root, context);
resolveImplicit(root, context);
resolveActive(root, context);
}
private void resolveExplicit(Object root, final TransformerContext context) {
try {
final TimeParameters timeParameters = TimingVerificationParameters.makeInstance(root, context.getExternalParameters()).getTimeParameters();
getHelper(context).traverse(root, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
if (getHelper(context).isTimed(content)) {
TimingState ts = getTimingState(content, context, timeParameters);
ts.resolveExplicit();
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private void resolveImplicit(Object root, final TransformerContext context) {
try {
final TimeParameters timeParameters = TimingVerificationParameters.makeInstance(root, context.getExternalParameters()).getTimeParameters();
getHelper(context).traverse(root, new PostVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
if (getHelper(context).isTimed(content)) {
TimingState ts = getTimingState(content, context, timeParameters);
ts.resolveImplicit();
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private void resolveActive(Object root, final TransformerContext context) {
try {
final TimeParameters timeParameters = TimingVerificationParameters.makeInstance(root, context.getExternalParameters()).getTimeParameters();
getHelper(context).traverse(root, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
if (getHelper(context).isTimed(content)) {
TimingState ts = getTimingState(content, context, timeParameters);
ts.resolveActive();
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private java.util.Set<TimeInterval> extractActiveIntervals(Object root, final TransformerContext context) {
final TimeParameters timeParameters = TimingVerificationParameters.makeInstance(root, context.getExternalParameters()).getTimeParameters();
final java.util.Set<TimeInterval> intervals = new java.util.TreeSet<TimeInterval>();
try {
getHelper(context).traverse(root, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
if (getHelper(context).isTimed(content)) {
TimingState ts = getTimingState(content,context, timeParameters);
ts.extractActiveInterval(intervals);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return intervals;
}
private TimingState getTimingState(Object content, TransformerContext context, TimeParameters timeParameters) {
if (content == null)
return null;
Map<Object,TimingState> timingStates = getTimingStates(context);
if (!timingStates.containsKey(content))
timingStates.put(content, new TimingState(context, content, timeParameters));
return getTimingStates(context).get(content);
}
private void writeISDSequence(Object root, TransformerContext context, Set<TimeInterval> intervals, OutputStream out) {
Reporter reporter = context.getReporter();
boolean suppressOutput = (Boolean) context.getResourceState(TransformerContext.ResourceState.ttxSuppressOutputSerialization.name());
if (!suppressOutput && outputDirectoryClean)
cleanOutputDirectory(outputDirectory, context);
try {
Model model = context.getModel();
JAXBContext jc = JAXBContext.newInstance(model.getJAXBContextPath());
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
Marshaller m = jc.createMarshaller();
m.marshal(context.getBindingElement(context.getXMLNode(root)), doc);
List<Object> isdSequence = new java.util.ArrayList<Object>();
for (TimeInterval interval : intervals) {
Document docCopy = copyDocument(doc, db);
markIdAttributes(docCopy, context);
pruneIntervals(docCopy, context, interval);
pruneRegions(docCopy, context);
pruneTimingAndRegionAttributes(docCopy, context);
generateISDWrapper(docCopy, interval, context);
pruneISDExclusions(docCopy, context);
enactISDPostTransforms(docCopy, context);
Namespaces.normalize(docCopy, model.getNormalizedPrefixes());
if (hasUsableContent(docCopy, context)) {
Object isd = writeISD(docCopy, isdSequence.size() + 1, suppressOutput, context);
if (isd != null)
isdSequence.add(isd);
}
}
if (suppressOutput) {
reporter.logInfo(reporter.message("*KEY*", "Suppressing ''{0}'' transformer output serialization.", getName()));
context.setResourceState(TransformerContext.ResourceState.ttxOutput.name(), isdSequence);
}
} catch (JAXBException e) {
reporter.logError(e);
} catch (ParserConfigurationException e) {
reporter.logError(e);
}
}
private static Document copyDocument(Document doc, DocumentBuilder db) {
Document docCopy = db.newDocument();
Node rootCopy = doc.getDocumentElement().cloneNode(true);
docCopy.appendChild(docCopy.adoptNode(rootCopy));
return docCopy;
}
private static void markIdAttributes(Document doc, TransformerContext context) {
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (elt.hasAttributeNS(XML.xmlNamespace, "id"))
elt.setIdAttributeNS(XML.xmlNamespace, "id", true);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static void pruneIntervals(Document doc, TransformerContext context, final TimeInterval interval) {
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isTimedElement(elt)) {
TimeInterval eltInterval = getActiveInterval(elt);
if (eltInterval.isEmpty() || !eltInterval.intersects(interval)) {
assert parent instanceof Element;
Element eltParent = (Element) parent;
pruneElement(elt, eltParent);
}
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static TimeInterval getActiveInterval(Element elt) {
String begin;
if (elt.hasAttributeNS(TTMLHelper.NAMESPACE_ISD, "begin"))
begin = elt.getAttributeNS(TTMLHelper.NAMESPACE_ISD, "begin");
else
begin = null;
String end;
if (elt.hasAttributeNS(TTMLHelper.NAMESPACE_ISD, "end"))
end = elt.getAttributeNS(TTMLHelper.NAMESPACE_ISD, "end");
else
end = null;
return new TimeInterval(begin, end);
}
private static void pruneElement(Element elt, Element parent) {
assert elt != null;
if (parent != null) {
assert elt.getParentNode() == parent;
parent.removeChild(elt);
}
}
private static void pruneRegions(Document doc, TransformerContext context) {
List<Element> regions = getRegionElements(doc, context);
regions = maybeImplyDefaultRegion(doc, context, regions);
for (Element region : regions) {
try {
Element body = copyBodyElement(doc, context);
if (body != null) {
pruneUnselectedContent(body, context, region);
if (hasUsableContent(body, context))
region.appendChild(body);
}
} catch (DOMException e) {
context.getReporter().logError(e);
}
}
removeBodyElement(doc, context);
}
private static boolean hasUsableContent(Element elt, TransformerContext context) {
final ISDHelper helper = getHelper(context);
final boolean returnUsable[] = new boolean[1];
try {
Traverse.traverseElements(elt, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (helper.hasUsableContent(elt)) {
returnUsable[0] = true;
return false;
} else
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return returnUsable[0];
}
private static void pruneUnselectedContent(final Element body, TransformerContext context, final Element region) {
try {
Traverse.traverseElements(body, null, new PostVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (!isSelectedContent(elt, region)) {
if (parent != null) {
assert parent instanceof Element;
Element eltParent = (Element) parent;
pruneElement(elt, eltParent);
} else if (elt != body) {
assert false;
}
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static boolean isSelectedContent(Element elt, Element region) {
String id = getAssociatedRegionIdentifier(elt);
if (id != null) {
String idRegion = TTMLHelper.getXmlIdentifier(region);
if (idRegion != null)
return id.equals(idRegion);
else
return false;
} else
return TTMLHelper.isAnonymousRegionElement(region);
}
private static String getAssociatedRegionIdentifier(Element elt) {
String id;
id = TTMLHelper.getRegionIdentifier(elt);
if (id != null)
return id;
// use descendant before ancestor since we are doing post-traversal visit
id = getNearestDescendantRegionIdentifier(elt);
if (id != null)
return id;
// use ancestor after descendant since we are doing post-traversal visit
id = getNearestAncestorRegionIdentifier(elt);
if (id != null)
return id;
return null;
}
private static String getNearestAncestorRegionIdentifier(Element elt) {
for (Node a = elt.getParentNode(); a != null; a = a.getParentNode()) {
if (a instanceof Element) {
String id = TTMLHelper.getRegionIdentifier((Element) a);
if (id != null)
return id;
} else
break;
}
return null;
}
private static String getNearestDescendantRegionIdentifier(Element elt) {
Queue<Element> descendants = new java.util.LinkedList<Element>();
descendants.add(elt);
while (!descendants.isEmpty()) {
Element e = descendants.remove();
for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n instanceof Element) {
Element c = (Element) n;
String id = TTMLHelper.getRegionIdentifier(c);
if (id != null)
return id;
descendants.add(c);
}
}
}
return null;
}
private static List<Element> getRegionElements(Document doc, TransformerContext context) {
final List<Element> regions = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isOutOfLineRegionElement(elt))
regions.add(elt);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return regions;
}
private static Element maybeImplyHeadElement(Document document, TransformerContext context) {
Element head = getHeadElement(document, context);
if (head == null) {
Element defaultHead = document.createElementNS(TTMLHelper.NAMESPACE_TT, "head");
Element body = getBodyElement(document, context);
try {
Element root = document.getDocumentElement();
if (body != null)
root.insertBefore(defaultHead, body);
else
root.appendChild(defaultHead);
head = defaultHead;
} catch (DOMException e) {
context.getReporter().logError(e);
}
}
return head;
}
private static Element maybeImplyLayoutElement(Document document, TransformerContext context) {
Element layout = getLayoutElement(document, context);
if (layout == null) {
Element defaultLayout = document.createElementNS(TTMLHelper.NAMESPACE_TT, "layout");
Element head = maybeImplyHeadElement(document, context);
assert head != null;
try {
head.appendChild(defaultLayout);
layout = defaultLayout;
} catch (DOMException e) {
context.getReporter().logError(e);
}
}
return layout;
}
private static List<Element> maybeImplyDefaultRegion(Document document, TransformerContext context, List<Element> regions) {
if (regions.isEmpty()) {
try {
Element defaultRegion = document.createElementNS(TTMLHelper.NAMESPACE_TT, "region");
defaultRegion.setAttributeNS(XML.xmlNamespace, "id", getHelper(context).generateAnonymousRegionId(context));
Element layout = maybeImplyLayoutElement(document, context);
assert layout != null;
layout.appendChild(defaultRegion);
regions.add(defaultRegion);
} catch (DOMException e) {
context.getReporter().logError(e);
}
}
return regions;
}
private static Element copyBodyElement(Document document, TransformerContext context) {
Element body = getBodyElement(document, context);
if (body != null)
return (Element) body.cloneNode(true);
else
return null;
}
private static Element removeBodyElement(Document document, TransformerContext context) {
try {
Element body = getBodyElement(document, context);
if (body != null)
return (Element) body.getParentNode().removeChild(body);
} catch (DOMException e) {
context.getReporter().logError(e);
}
return null;
}
private static Element getHeadElement(Document document, TransformerContext context) {
final Element[] retHead = new Element[1];
try {
Traverse.traverseElements(document, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isHeadElement(elt)) {
retHead[0] = elt;
return false;
} else
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return retHead[0];
}
private static Element getBodyElement(Document document, TransformerContext context) {
final Element[] retBody = new Element[1];
try {
Traverse.traverseElements(document, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
Element eltParent = (Element) parent;
if (TTMLHelper.isBodyElement(elt) && TTMLHelper.isRootElement(eltParent)) {
retBody[0] = elt;
return false;
} else
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return retBody[0];
}
private static Element getLayoutElement(Document document, TransformerContext context) {
final Element[] retLayout = new Element[1];
try {
Traverse.traverseElements(document, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isLayoutElement(elt)) {
retLayout[0] = elt;
return false;
} else
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return retLayout[0];
}
private static void pruneTimingAndRegionAttributes(Document document, TransformerContext context) {
try {
Traverse.traverseElements(document, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isTimedElement(elt)) {
pruneTimingAttributes(elt);
pruneRegionAttributes(elt);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static void pruneTimingAttributes(Element elt) {
elt.removeAttributeNS(null, "begin");
elt.removeAttributeNS(null, "end");
elt.removeAttributeNS(null, "dur");
elt.removeAttributeNS(null, "timeContainer");
}
private static void pruneRegionAttributes(Element elt) {
elt.removeAttributeNS(null, "region");
}
private static void generateISDWrapper(Document document, TimeInterval interval, TransformerContext context) {
Element root = document.getDocumentElement();
Element isd = document.createElementNS(TTMLHelper.NAMESPACE_ISD, "isd");
isd.setAttributeNS(null, "begin", interval.getBegin().toString());
isd.setAttributeNS(null, "end", interval.getEnd().toString());
copyWrapperAttributes(isd, root);
copyMiscellaneousAttributes(isd, root);
Map<Element, StyleSet> computedStyleSets = generateISDComputedStyleSets(isd, root, context);
generateISDRegions(isd, root, computedStyleSets, context);
unwrapRedundantAnonymousSpans(isd, root, context);
document.removeChild(root);
document.appendChild(isd);
}
private static void copyWrapperAttributes(Element isd, Element root) {
NamedNodeMap attrs = root.getAttributes();
for (int i = 0, n = attrs.getLength(); i < n; ++i) {
Node node = attrs.item(i);
if (node instanceof Attr) {
Attr a = (Attr) node;
String nsUri = a.getNamespaceURI();
if (nsUri == null) {
continue;
} else if (nsUri.equals(TTMLHelper.NAMESPACE_TT_PARAMETER)) {
if (inISDParameterAttributeSet(a) && !isDefaultParameterValue(a)) {
isd.setAttributeNS(nsUri, a.getLocalName(), a.getValue());
}
} else if (nsUri.equals(XML.xmlNamespace)) {
String ln = a.getLocalName();
if (ln.equals("lang") || ln.equals("space"))
isd.setAttributeNS(nsUri, ln, a.getValue());
}
}
}
}
private static void copyMiscellaneousAttributes(Element isd, Element root) {
if (TTML1Helper.hasExplicitExtent(root))
isd.setAttributeNS(null, "extent", TTML1Helper.getExplicitExtent(root));
}
private static boolean inISDParameterAttributeSet(Attr attr) {
String localName = attr.getLocalName();
String value = attr.getValue();
if ((value == null) || value.isEmpty())
return false;
else if (localName.equals("cellResolution"))
return true;
else if (localName.equals("displayAspectRatio")) // TTML2
return true;
else if (localName.equals("frameRate"))
return true;
else if (localName.equals("frameRateMultiplier"))
return true;
else if (localName.equals("pixelAspectRatio"))
return true;
else if (localName.equals("subFrameRate"))
return true;
else if (localName.equals("tickRate"))
return true;
else if (localName.equals("version")) // TTML2
return true;
else
return false;
}
private static Map<Element,StyleSet> generateISDComputedStyleSets(final Element isd, Element root, TransformerContext context) {
try {
final Map<Element, StyleSet> computedStyleSets = resolveComputedStyles(root, context);
final Set<String> styleIds = new java.util.HashSet<String>();
final Document document = isd.getOwnerDocument();
Traverse.traverseElements(root, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (computedStyleSets.containsKey(elt)) {
StyleSet css = computedStyleSets.get(elt);
String id = css.getId();
if (!id.isEmpty()) {
elt.setAttributeNS(TTMLHelper.NAMESPACE_ISD, "css", id);
if (!styleIds.contains(id)) {
Element isdStyle = document.createElementNS(TTMLHelper.NAMESPACE_ISD, "css");
generateAttributes(css, isdStyle);
isd.appendChild(isdStyle);
styleIds.add(id);
}
}
}
return true;
}
});
return computedStyleSets;
} catch (Exception e) {
context.getReporter().logError(e);
return null;
}
}
private static void generateAttributes(StyleSet ss, Element elt) {
String id = ss.getId();
if ((id != null) && !id.isEmpty()) {
for (StyleSpecification s : ss.getStyles().values()) {
ComparableQName n = s.getName();
elt.setAttributeNS(n.getNamespaceURI(), n.getLocalPart(), s.getValue());
}
elt.setAttributeNS(XML.xmlNamespace, "id", id);
}
}
private static Map<Element, StyleSet> resolveComputedStyles(Element root, TransformerContext context) {
// compute initial value overrides
StyleSet overrides = computeInitialStyleOverrides(root.getOwnerDocument(), context);
// resolve specified style sets
Map<Element, StyleSet> specifiedStyleSets = resolveSpecifiedStyles(root, context, overrides);
// derive {CSS(E)} from {SSS(E)}
Map<Element, StyleSet> computedStyleSets = new java.util.HashMap<Element, StyleSet>();
for (Map.Entry<Element, StyleSet> e : specifiedStyleSets.entrySet()) {
Element elt = e.getKey();
if (TTMLHelper.isRegionElement(elt)) {
computedStyleSets.put(elt, applicableStyles(e.getValue(), elt, context));
} else if (TTMLHelper.isContentElement(elt)) {
computedStyleSets.put(elt, applicableStyles(e.getValue(), elt, context));
}
}
// maybe elide initial values
if (!(Boolean) context.getResourceState(TransformerContext.ResourceState.ttxDontElideInitials.name())) {
for (Map.Entry<Element, StyleSet> e : computedStyleSets.entrySet())
elideInitialValues(e.getValue(), e.getKey(), context);
}
// compute set of unique CSSs
Set<StyleSet> uniqueComputedStyleSets = new java.util.TreeSet<StyleSet>(StyleSet.getValuesComparator());
for (StyleSet css : computedStyleSets.values()) {
if (!css.isEmpty())
uniqueComputedStyleSets.add(css);
}
// obtain ordered list of unique CSSs
List<StyleSet> uniqueStyles = new java.util.ArrayList<StyleSet>(uniqueComputedStyleSets);
// assign identifiers to unique CSSs
int uniqueStyleIndex = 0;
for (StyleSet css : uniqueStyles)
css.setId("s" + uniqueStyleIndex++);
// remap CSS map entries to unique CSSs
for (Map.Entry<Element,StyleSet> e : computedStyleSets.entrySet()) {
StyleSet css = e.getValue();
int index = uniqueStyles.indexOf(css);
if (index >= 0) {
StyleSet cssUnique = uniqueStyles.get(index);
if (css != cssUnique) // N.B. must not use equals() here
e.setValue(cssUnique);
}
}
return computedStyleSets;
}
private static StyleSet computeInitialStyleOverrides(Document doc, TransformerContext context) {
StyleSet overrides = new StyleSet();
for (Element initial : getInitialElements(doc, context)) {
overrides.merge(getInlineStyles(initial, context), context.getConditionEvaluatorState());
}
return overrides;
}
private static List<Element> getInitialElements(Document doc, TransformerContext context) {
final List<Element> initials = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isInitialElement(elt))
initials.add(elt);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return initials;
}
private static Map<Element, StyleSet> resolveSpecifiedStyles(Element root, TransformerContext context, StyleSet overrides) {
Map<Element, StyleSet> specifiedStyleSets = new java.util.HashMap<Element, StyleSet>();
specifiedStyleSets = resolveSpecifiedStyles(getStyleElements(root, context), specifiedStyleSets, context, overrides);
specifiedStyleSets = resolveSpecifiedStyles(getAnimationElements(root, context), specifiedStyleSets, context, overrides);
specifiedStyleSets = resolveSpecifiedStyles(getRegionOrContentElements(root, context), specifiedStyleSets, context, overrides);
return specifiedStyleSets;
}
private static Collection<Element> getStyleElements(Element root, TransformerContext context) {
final Collection<Element> elts = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(root, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isStyleElement(elt)) {
elts.add(elt);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return topoSortByStyleReferences(elts, context);
}
private static Collection<Element> topoSortByStyleReferences(Collection<Element> elts, TransformerContext context) {
DirectedGraph<Element> graph = new DirectedGraph<Element>();
for (Element elt : elts)
graph.addNode(elt);
for (Element elt : elts) {
for (Element refStyle : getReferencedStyleElements(elt)) {
graph.addEdge(elt, refStyle);
}
}
try {
List<Element> eltsSorted = TopologicalSort.sort(graph);
// topo sort ensures all forward refs, but we want all reverse refs, so reverse sorted results
Collections.reverse(eltsSorted);
return eltsSorted;
} catch (IllegalArgumentException e) {
Reporter reporter = context.getReporter();
reporter.logError(reporter.message("*KEY*", "Cycle in style chain, unable to resolve styles."));
return new java.util.ArrayList<Element>();
}
}
private static Collection<Element> getAnimationElements(Element root, TransformerContext context) {
final Collection<Element> elts = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(root, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isAnimationElement(elt)) {
elts.add(elt);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return elts;
}
private static Collection<Element> getRegionOrContentElements(Element root, TransformerContext context) {
final Collection<Element> elts = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(root, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isRegionOrContentElement(elt))
elts.add(elt);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
return elts;
}
private static Map<Element, StyleSet> resolveSpecifiedStyles(Collection<Element> elts, Map<Element, StyleSet> specifiedStyleSets, TransformerContext context, StyleSet overrides) {
for (Element elt : elts) {
assert !specifiedStyleSets.containsKey(elt);
specifiedStyleSets.put(elt, computeSpecifiedStyleSet(elt, specifiedStyleSets, context, overrides));
}
return specifiedStyleSets;
}
private static StyleSet applicableStyles(StyleSet ss, Element elt, TransformerContext context) {
boolean containsInapplicableStyle = false;
QName eltName = new QName(elt.getNamespaceURI(), elt.getLocalName());
for (StyleSpecification s : ss.getStyles().values()) {
if (!doesStyleApply(eltName, s.getName(), context)) {
containsInapplicableStyle = true;
break;
}
}
if (!containsInapplicableStyle)
return ss;
else {
StyleSet ssNew = new StyleSet(ss.getGeneration());
for (StyleSpecification s : ss.getStyles().values()) {
if (doesStyleApply(eltName, s.getName(), context)) {
ssNew.merge(s);
}
}
return ssNew;
}
}
private static boolean doesStyleApply(Element elt, QName styleName, TransformerContext context) {
QName eltName = new QName(elt.getNamespaceURI(), elt.getLocalName());
return doesStyleApply(eltName, styleName, context);
}
private static boolean doesStyleApply(QName eltName, QName styleName, TransformerContext context) {
return context.getModel().doesStyleApply(eltName, styleName);
}
private static void elideInitialValues(StyleSet ss, Element elt, TransformerContext context) {
QName eltName = new QName(elt.getNamespaceURI(), elt.getLocalName());
List<StyleSpecification> elisions = new java.util.ArrayList<StyleSpecification>();
for (StyleSpecification s : ss.getStyles().values()) {
StyleSpecification initial = getInitialStyle(eltName, s.getName(), context, null);
if (initial != null) {
String value = initial.getValue();
if ((value != null) && value.equals(s.getValue()))
elisions.add(s);
}
}
for (StyleSpecification s : elisions) {
ss.remove(s.getName());
}
}
private static StyleSpecification getInitialStyle(Element elt, QName styleName, TransformerContext context, StyleSet overrides) {
QName eltName = new QName(elt.getNamespaceURI(), elt.getLocalName());
return getInitialStyle(eltName, styleName, context, overrides);
}
private static StyleSpecification getInitialStyle(QName eltName, QName styleName, TransformerContext context, StyleSet overrides) {
String value = context.getModel().getInitialStyleValue(eltName, styleName);
if (value != null) {
if (overrides != null) {
StyleSpecification override = overrides.get(styleName);
if (override != null)
value = override.getValue();
}
return new StyleSpecification(new ComparableQName(styleName), value);
} else
return null;
}
private static StyleSet computeSpecifiedStyleSet(Element elt, Map<Element, StyleSet> specifiedStyleSets, TransformerContext context, StyleSet overrides) {
// See TTML2, Section 8.4.4.2
// 1. initialization
StyleSet sss = new StyleSet(getHelper(context).generateStyleSetIndex(context));
Condition.EvaluatorState ces = context.getConditionEvaluatorState();
// 2. referential and chained referential styling
for (StyleSet ss : getSpecifiedStyleSets(getReferencedStyleElements(elt), specifiedStyleSets))
sss.merge(ss, ces);
// 3. nested styling
for (StyleSet ss : getSpecifiedStyleSets(getChildStyleElements(elt), specifiedStyleSets))
sss.merge(ss, ces);
// 4. inline styling
sss.merge(getInlineStyles(elt, context), ces);
// 5. animation styling
if (!TTMLHelper.isAnimationElement(elt)) {
for (StyleSet ss : getSpecifiedStyleSets(getChildAnimationElements(elt), specifiedStyleSets))
sss.merge(ss, ces);
}
// 6. implicit inheritance and initial value fallback
if (!TTMLHelper.isAnimationElement(elt) && !TTMLHelper.isStyleElement(elt)) {
for (QName name : getDefinedStyleNames(context)) {
if (!sss.containsKey(name)) {
StyleSpecification s;
if (!isInheritableStyle(elt, name, context) || TTMLHelper.isRootStylingElement(elt))
s = getInitialStyle(elt, name, context, overrides);
else if (specialStyleInheritance(elt, name, sss, context))
s = getSpecialInheritedStyle(elt, name, sss, specifiedStyleSets, context);
else
s = getNearestAncestorStyle(elt, name, specifiedStyleSets);
if ((s != null) && (doesStyleApply(elt, name, context) || TTMLHelper.isRootStylingElement(elt)))
sss.merge(s);
}
}
}
return sss;
}
private static Collection<Element> getReferencedStyleElements(Element elt) {
Collection<Element> elts = new java.util.ArrayList<Element>();
if (elt.hasAttribute("style")) {
String style = elt.getAttribute("style");
if (!style.isEmpty()) {
Document document = elt.getOwnerDocument();
String[] idrefs = style.split("\\s+");
for (String idref : idrefs) {
Element refStyle = document.getElementById(idref);
if (refStyle != null)
elts.add(refStyle);
}
}
}
return elts;
}
private static Collection<Element> getChildStyleElements(Element elt) {
Collection<Element> elts = new java.util.ArrayList<Element>();
for (Node n = elt.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n instanceof Element) {
Element c = (Element) n;
if (TTMLHelper.isStyleElement(c))
elts.add(c);
}
}
return elts;
}
private static Collection<Element> getChildAnimationElements(Element elt) {
Collection<Element> elts = new java.util.ArrayList<Element>();
for (Node n = elt.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n instanceof Element) {
Element c = (Element) n;
if (TTMLHelper.isAnimationElement(c))
elts.add(c);
}
}
return elts;
}
private static Collection<StyleSet> getSpecifiedStyleSets(Collection<Element> elts, Map<Element, StyleSet> specifiedStyleSets) {
Collection<StyleSet> styleSets = new java.util.ArrayList<StyleSet>();
for (Element elt : elts) {
if (specifiedStyleSets.containsKey(elt))
styleSets.add(specifiedStyleSets.get(elt));
}
return styleSets;
}
private static StyleSet getInlineStyles(Element elt, TransformerContext context) {
StyleSet styles = new StyleSet();
NamedNodeMap attrs = elt.getAttributes();
for (int i = 0, n = attrs.getLength(); i < n; ++i) {
Node node = attrs.item(i);
if (node instanceof Attr) {
Attr a = (Attr) node;
String nsUri = a.getNamespaceURI();
if (nsUri != null) {
QName styleName = new ComparableQName(a.getNamespaceURI(), a.getLocalName());
if (isDefinedStyle(styleName, context))
styles.merge(new StyleSpecification(styleName, a.getValue()));
}
}
}
return styles;
}
private static Collection<QName> getDefinedStyleNames(TransformerContext context) {
return context.getModel().getDefinedStyleNames();
}
private static boolean isDefinedStyle(QName styleName, TransformerContext context) {
return context.getModel().isDefinedStyle(styleName);
}
private static boolean isInheritableStyle(Element elt, QName styleName, TransformerContext context) {
return isInheritableStyle(new QName(elt.getNamespaceURI(), elt.getLocalName()), styleName, context);
}
private static boolean isInheritableStyle(QName eltName, QName styleName, TransformerContext context) {
return context.getModel().isInheritableStyle(eltName, styleName);
}
private static boolean specialStyleInheritance(Element elt, QName styleName, StyleSet sss, TransformerContext context) {
return getHelper(context).specialStyleInheritance(elt, styleName, sss, context);
}
private static StyleSpecification getSpecialInheritedStyle(Element elt, QName styleName, StyleSet sss, Map<Element, StyleSet> specifiedStyleSets, TransformerContext context) {
return getHelper(context).getSpecialInheritedStyle(elt, styleName, sss, specifiedStyleSets, context);
}
private static StyleSpecification getNearestAncestorStyle(Element elt, QName styleName, Map<Element, StyleSet> specifiedStyleSets) {
for (Node a = elt.getParentNode(); a != null; a = a.getParentNode()) {
if (a instanceof Element) {
Element p = (Element) a;
StyleSet ss = specifiedStyleSets.get(p);
if (ss != null) {
if (ss.containsKey(styleName))
return ss.get(styleName);
}
}
}
return null;
}
private static void generateISDRegions(final Element isd, Element root, final Map<Element, StyleSet> computedStyleSets, final TransformerContext context) {
try {
Traverse.traverseElements(root, null, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isRegionElement(elt)) {
generateISDRegion(isd, elt, (computedStyleSets != null) ? computedStyleSets.get(elt) : null, context);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static void generateISDRegion(Element isd, Element region, StyleSet ss, TransformerContext context) {
Element body = detachBody(region);
if ((body != null) || TTML1Helper.hasShowBackgroundAlways(region, ss)) {
Document document = isd.getOwnerDocument();
Element isdRegion = document.createElementNS(TTMLHelper.NAMESPACE_ISD, "region");
copyRegionAttributes(isdRegion, region, context);
if (body != null)
isdRegion.appendChild(body);
isd.appendChild(isdRegion);
}
}
private static void copyRegionAttributes(Element isdRegion, Element region, TransformerContext context) {
boolean retainLocations = (Boolean) context.getResourceState(TransformerContext.ResourceState.ttxRetainLocations.name());
NamedNodeMap attrs = region.getAttributes();
List<Attr> attrsNew = new java.util.ArrayList<Attr>();
for (int i = 0, n = attrs.getLength(); i < n; ++i) {
Node node = attrs.item(i);
Attr aNew = null;
if (node instanceof Attr) {
Attr a = (Attr) node;
String nsUri = a.getNamespaceURI();
String localName = a.getLocalName();
if (nsUri != null) {
if (nsUri.equals(XML.xmlNamespace)) {
if (localName.equals("id"))
aNew = a;
} else if (nsUri.equals(TTMLHelper.NAMESPACE_ISD)) {
if (localName.equals("css"))
aNew = a;
} else if (nsUri.equals(Annotations.getNamespace())) {
if (retainLocations) {
if (localName.equals("loc"))
aNew = a;
}
}
}
}
if (aNew != null)
attrsNew.add(aNew);
}
for (Attr a : attrsNew)
isdRegion.setAttributeNS(a.getNamespaceURI(), a.getLocalName(), a.getValue());
}
private static Element detachBody(Element region) {
Element body = findChildElement(region, TTMLHelper.NAMESPACE_TT, "body");
if (body != null) {
assert body.getParentNode() == region;
region.removeChild(body);
return body;
} else
return null;
}
private static void unwrapRedundantAnonymousSpans(final Element isd, Element root, TransformerContext context) {
try {
Traverse.traverseElements(isd, null, new PostVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (TTMLHelper.isAnonymousSpanElement(elt)) {
if (parent instanceof Element) {
if (hasSameComputedStyleSet(elt, (Element) parent))
unwrapAnonymousSpan(elt, (Element) parent);
}
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static boolean hasSameComputedStyleSet(Element e1, Element e2) {
if (!e1.hasAttributeNS(TTMLHelper.NAMESPACE_ISD, "css"))
return false;
else if (!e2.hasAttributeNS(TTMLHelper.NAMESPACE_ISD, "css"))
return false;
else {
String s1 = e1.getAttributeNS(TTMLHelper.NAMESPACE_ISD, "css");
String s2 = e2.getAttributeNS(TTMLHelper.NAMESPACE_ISD, "css");
return s1.equals(s2);
}
}
private static void unwrapAnonymousSpan(Element elt, Element parent) {
Node fc = elt.getFirstChild();
assert fc != null;
Node lc = elt.getLastChild();
assert lc == fc;
if (fc instanceof Text) {
Text text = (Text) elt.removeChild(fc);
assert text != null;
parent.replaceChild(text, elt);
}
}
private static Element findChildElement(Element elt, String namespace, String name) {
for (Node n = elt.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n instanceof Element) {
Element c = (Element) n;
String nsUri = c.getNamespaceURI();
String localName = c.getLocalName();
if ((namespace == null) ^ (nsUri == null))
continue;
else if (!localName.equals(name))
continue;
else
return c;
}
}
return null;
}
private void pruneISDExclusions(Document document, final TransformerContext context) {
try {
Traverse.traverseElements(document, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (!maybeExcludeElement(elt, context))
maybeExcludeAttributes(elt, context);
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
private static boolean maybeExcludeElement(Element elt, TransformerContext context) {
boolean retainMetadata = (Boolean) context.getResourceState(TransformerContext.ResourceState.ttxRetainMetadata.name());
String nsUri = elt.getNamespaceURI();
boolean exclude = false;
if (nsUri.equals(TTMLHelper.NAMESPACE_TT_METADATA)) {
if (!retainMetadata)
exclude = true;
}
if (exclude) {
excludeElement(elt);
return true;
} else
return false;
}
private static void excludeElement(Element elt) {
Node parent = elt.getParentNode();
if (parent != null) {
parent.removeChild(elt);
}
}
private static boolean maybeExcludeAttributes(Element elt, TransformerContext context) {
boolean retainLocations = (Boolean) context.getResourceState(TransformerContext.ResourceState.ttxRetainLocations.name());
String nsUriElt = elt.getNamespaceURI();
NamedNodeMap attrs = elt.getAttributes();
List<Attr> exclusions = new java.util.ArrayList<Attr>();
for (int i = 0, n = attrs.getLength(); i < n; ++i) {
Node node = attrs.item(i);
if (node instanceof Attr) {
Attr a = (Attr) node;
String nsUri = a.getNamespaceURI();
String localName = a.getLocalName();
String value = a.getValue();
if (nsUri == null) {
if (localName.equals("style"))
exclusions.add(a);
else
continue;
} else if (nsUri.equals(TTMLHelper.NAMESPACE_TT_METADATA)) {
exclusions.add(a);
} else if (nsUri.equals(TTMLHelper.NAMESPACE_TT_PARAMETER)) {
if (isDefaultParameterValue(a))
exclusions.add(a);
} else if (nsUri.equals(TTMLHelper.NAMESPACE_TT_STYLE)) {
if ((nsUriElt == null) || !nsUriElt.equals(TTMLHelper.NAMESPACE_ISD))
exclusions.add(a);
} else if (nsUri.equals(TTMLHelper.NAMESPACE_ISD)) {
if (!localName.equals("css"))
exclusions.add(a);
} else if (nsUri.equals(Annotations.getNamespace())) {
if (!retainLocations)
exclusions.add(a);
} else if (nsUri.equals(XML.xmlnsNamespace)) {
if (value != null) {
if (value.equals(TTMLHelper.NAMESPACE_ISD)) {
if ((nsUriElt != null) && nsUriElt.equals(TTMLHelper.NAMESPACE_TT))
exclusions.add(a);
} else if (value.equals(TTMLHelper.NAMESPACE_TT))
exclusions.add(a);
else if (value.equals(TTMLHelper.NAMESPACE_TT_METADATA))
exclusions.add(a);
else if (value.equals(Annotations.getNamespace()))
exclusions.add(a);
}
}
}
}
for (Attr a : exclusions)
elt.removeAttributeNode(a);
return exclusions.size() > 0;
}
// TBD - use defaulting data from TTML1ParameterVerifier.parameterAccessorMap
private static boolean isDefaultParameterValue(Attr attr) {
String localName = attr.getLocalName();
String value = attr.getValue();
if ((value == null) || value.isEmpty())
return false;
else if (localName.equals("cellResolution"))
return value.equals("32 15");
else if (localName.equals("clockMode"))
return value.equals("utc");
else if (localName.equals("dropMode"))
return value.equals("nonDrop");
else if (localName.equals("frameRateMultiplier"))
return value.equals("1 1");
else if (localName.equals("markerMode"))
return value.equals("discontinuous");
else if (localName.equals("pixelAspectRatio"))
return value.equals("1 1");
else if (localName.equals("subFrameRate"))
return value.equals("1");
else if (localName.equals("tickRate"))
return value.equals("1");
else if (localName.equals("timeBase"))
return value.equals("media");
else
return false;
}
private void enactISDPostTransforms(Document document, TransformerContext context) {
getHelper(context).enactISDPostTransforms(document, context);
}
private static boolean hasUsableContent(Document document, TransformerContext context) {
Element root = document.getDocumentElement();
return (root != null) && hasUsableContent(root, context);
}
private static void cleanOutputDirectory(File directory, TransformerContext context) {
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message("*KEY*", "Cleaning ISD artifacts from output directory ''{0}''...", directory.getPath()));
File[] files = directory.listFiles();
if (files != null) {
for (File f : files) {
String name = f.getName();
if (name.indexOf("isd") != 0)
continue;
else if (name.indexOf(".xml") != (name.length() - 4))
continue;
else if (!f.delete())
throw new TransformerException("unable to clean output directory: can't delete: '" + name + "'");
}
}
}
private Object writeISD(Document document, int sequenceIndex, boolean suppressOutput, TransformerContext context) {
if (suppressOutput)
return writeISDAsByteArray(document, sequenceIndex, context);
else
return writeISDAsFile(document, sequenceIndex, context);
}
private Object writeISDAsFile(Document document, int sequenceIndex, TransformerContext context) {
Reporter reporter = context.getReporter();
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
String outputFileName = outputPatternFormatter.format(new Object[]{Integer.valueOf(sequenceIndex)});
File outputFile = new File(outputDirectory, outputFileName);
fos = new FileOutputStream(outputFile);
bos = new BufferedOutputStream(fos);
writeISD(document, bos, context);
reporter.logInfo(reporter.message("*KEY*", "Wrote ISD ''{0}''.", outputFile.getAbsolutePath()));
return outputFile;
} catch (FileNotFoundException e) {
reporter.logError(e);
return null;
} finally {
IOUtil.closeSafely(bos);
IOUtil.closeSafely(fos);
}
}
private Object writeISDAsByteArray(Document document, int sequenceIndex, TransformerContext context) {
Reporter reporter = context.getReporter();
ByteArrayOutputStream bas = null;
BufferedOutputStream bos = null;
try {
bas = new ByteArrayOutputStream();
bos = new BufferedOutputStream(bas);
writeISD(document, bos, context);
return bas.toByteArray();
} catch (Exception e) {
reporter.logError(e);
return null;
} finally {
IOUtil.closeSafely(bos);
IOUtil.closeSafely(bas);
}
}
private void writeISD(Document document, OutputStream os, TransformerContext context) throws TransformerException {
BufferedWriter bw = null;
try {
TransformerFactory tf = TransformerFactory.newInstance();
DOMSource source = new DOMSource(document);
bw = new BufferedWriter(new OutputStreamWriter(os, outputEncoding));
StreamResult result = new StreamResult(bw);
javax.xml.transform.Transformer t = tf.newTransformer();
t.setOutputProperty(OutputKeys.INDENT, outputIndent ? "yes" : "no");
t.transform(source, result);
} catch (TransformerConfigurationException e) {
throw new RuntimeException(e);
} catch (javax.xml.transform.TransformerException e) {
throw new RuntimeException(e);
} finally {
if (bw != null) {
try { bw.close(); } catch (IOException e) {}
}
}
}
}
}