/* * 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.util; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.xml.sax.Locator; import com.skynav.ttv.model.value.ClockTime; import com.skynav.ttv.model.value.OffsetTime; import com.skynav.ttv.model.value.Time; import com.skynav.ttv.model.value.TimeBase; import com.skynav.ttv.model.value.TimeParameters; import com.skynav.ttv.model.value.impl.ClockTimeImpl; import com.skynav.ttv.model.value.impl.OffsetTimeImpl; import com.skynav.ttv.util.Location; import com.skynav.ttv.util.Reporter; import com.skynav.ttv.verifier.VerifierContext; public class Timing { public static boolean isCoordinate(String value, Location location, VerifierContext context, TimeParameters timeParameters, Time[] outputTime) { if (isClockTime(value, location, context, timeParameters, outputTime)) return true; else if (isOffsetTime(value, location, context, timeParameters, outputTime)) return true; else return false; } public static void badCoordinate(String value, Location location, VerifierContext context, TimeParameters timeParameters) { if (value.indexOf(':') >= 0) badClockTime(value, location, context, timeParameters); else badOffsetTime(value, location, context, timeParameters); } public static boolean isDuration(String value, Location location, VerifierContext context, TimeParameters timeParameters, Time[] outputTime) { return isCoordinate(value, location, context, timeParameters, outputTime); } public static void badDuration(String value, Location location, VerifierContext context, TimeParameters timeParameters) { badCoordinate(value, location, context, timeParameters); } private static final Pattern clockTimePattern = Pattern.compile("(\\d{2,3}):(\\d{2}):(\\d{2})(\\.\\d+|:\\d{2,}(?:\\.\\d+)?)?"); public static boolean isClockTime(String value, Location location, VerifierContext context, TimeParameters timeParameters, Time[] outputTime) { Matcher m = clockTimePattern.matcher(value); if (m.matches()) { assert m.groupCount() >= 3; String hours = m.group(1); String minutes = m.group(2); String seconds = m.group(3); String frames = null; String subFrames = null; if (m.groupCount() > 3) { String remainder = m.group(4); if (remainder != null) { if (remainder.indexOf(':') == 0) { String[] parts = remainder.substring(1).split("\\.", 3); if (parts.length > 0) frames = parts[0]; if (parts.length > 1) subFrames = parts[1]; } else seconds += remainder; } } if ((timeParameters.getTimeBase() == TimeBase.CLOCK) && ((frames != null) || (subFrames != null))) return false; ClockTime t = new ClockTimeImpl(hours, minutes, seconds, frames, subFrames); if (t.getMinutes() > 59) return false; if (t.getSeconds() > 60.0) return false; if (t.getFrames() >= timeParameters.getFrameRate()) return false; if (t.getSubFrames() >= timeParameters.getSubFrameRate()) return false; if (outputTime != null) outputTime[0] = t; if (location != null) { if (frames != null) updateUsage(context, location, OffsetTime.Metric.Frames); } return true; } else return false; } public static void badClockTime(String value, Location location, VerifierContext context, TimeParameters timeParameters) { Reporter reporter = context.getReporter(); Locator locator = location.getLocator(); assert value.indexOf(':') >= 0; String[] parts = value.split("\\:", 5); int numParts = parts.length; if (numParts > 0) { String hh = parts[0]; if (hh.length() == 0) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, hours part is empty in clock time.")); else if (!Strings.isDigits(hh)) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, hours part ''{0}'' contains non-digit character in clock time.", hh)); else if (hh.length() < 2) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, hours part must contain two or more digits in clock time.")); } else { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, empty expression.")); } if (numParts > 1) { String mm = parts[1]; if (mm.length() == 0) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, minutes part is empty in clock time.")); else if (!Strings.isDigits(mm)) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, minutes part ''{0}'' contains non-digit character in clock time.", mm)); else if (mm.length() < 2) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, minutes part is missing digit(s), must contain two digits in clock time.")); else if (mm.length() > 2) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, minutes part contains extra digit(s), must contain two digits in clock time.")); else if (Integer.parseInt(mm) >= 60) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, minutes ''{0}'' must be less than 60.", mm)); } else { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing minutes and seconds parts in clock time.")); } if (numParts > 2) { String ss = parts[2]; if (ss.length() == 0) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part is empty in clock time.")); else if (Strings.containsDecimalSeparator(ss)) { String[] subParts = ss.split("\\.", 3); if (subParts.length > 0) { String w = subParts[0]; if (w.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part whole sub-part is empty in clock time.")); } else if (!Strings.isDigits(w)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part whole sub-part ''{0}'' contains non-digit character in clock time.", w)); } else if (w.length() < 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part is missing digit(s), must contain two digits in clock time.")); } else if (w.length() > 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part contains extra digit(s), must contain two digits in clock time.")); } } if (subParts.length > 1) { String f = subParts[1]; if (f.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part fraction sub-part is empty in clock time.")); } else if (!Strings.isDigits(f)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part fraction sub-part ''{0}'' contains non-digit character in clock time.", f)); } } if (subParts.length == 2) { String w = subParts[0]; String f = subParts[1]; if (Strings.isDigits(w) && Strings.isDigits(f)) { if (Double.parseDouble(ss) > 60.0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds ''{0}'' must be less than 60.0.", ss)); } } } else if (subParts.length > 2) { StringBuffer sb = new StringBuffer(); for (int i = 2, n = subParts.length; i < n; ++i) { sb.append('.'); sb.append(subParts[i]); } reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part contains extra sub-parts ''{0}''.", sb.toString())); } } else if (!Strings.isDigits(ss)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part ''{0}'' contains unexpected character (not digit or decimal separator) in clock time.", ss)); } else if (ss.length() < 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part is missing digit(s), must contain two digits in clock time.")); } else if (ss.length() > 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds part contains extra digit(s), must contain two digits in clock time.")); } else if (Integer.parseInt(ss) > 60) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, seconds ''{0}'' must be less than 60.0.", ss)); } } else { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing seconds part in clock time.")); } if (numParts > 3) { String ff = parts[3]; int frames = 0; int subFrames = 0; if (ff.length() == 0) reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part is empty in clock time.")); else if (Strings.containsDecimalSeparator(ff)) { String[] subParts = ff.split("\\.", 3); if (subParts.length > 0) { String w = subParts[0]; if (w.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part whole sub-part is empty in clock time.")); } else if (!Strings.isDigits(w)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part whole sub-part ''{0}'' contains non-digit character in clock time.", w)); } else if (w.length() < 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part whole sub-part is missing digit(s), must contain two or more digits in clock time.")); } } if (subParts.length > 1) { String f = subParts[1]; if (f.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part sub-frames sub-part is empty in clock time.")); } else if (!Strings.isDigits(f)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part sub-frames sub-part ''{0}'' contains non-digit character in clock time.", f)); } } if (subParts.length == 2) { String w = subParts[0]; String f = subParts[1]; if (Strings.isDigits(w) && Strings.isDigits(f)) { frames = Integer.parseInt(w); subFrames = Integer.parseInt(f); } } else if (subParts.length > 2) { StringBuffer sb = new StringBuffer(); for (int i = 2, n = subParts.length; i < n; ++i) { sb.append('.'); sb.append(subParts[i]); } reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part contains extra sub-parts ''{0}''.", sb.toString())); } } else if (!Strings.isDigits(ff)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part ''{0}'' contains unexpected character (not digit or decimal separator) in clock time.", ff)); } else if (ff.length() < 2) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part is missing digit(s), must contain two or more digits in clock time.")); } else { frames = Integer.parseInt(ff); } if (ff.length() > 0) { if (timeParameters.getTimeBase() == TimeBase.CLOCK) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames part not permitted when using 'clock' time base.")); } double frameRate = timeParameters.getFrameRate(); if (frames >= frameRate) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames ''{0}'' must be less than frame rate {1}.", frames, frameRate)); } double subFrameRate = timeParameters.getSubFrameRate(); if (subFrames >= subFrameRate) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, sub-frames ''{0}'' must be less than sub-frame rate {1}.", subFrames, subFrameRate)); } } } if (numParts > 4) { String uu = parts[4]; if (uu.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, unexpected empty part after seconds or frames part in clock time.")); } else { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, unexpected part '':{0}'' after seconds or frames part in clock time.", uu)); } } } private static final Pattern offsetTimePattern = Pattern.compile("(\\d+(?:\\.\\d+)?)(h|m|s|ms|f|t)"); public static boolean isOffsetTime(String value, Location location, VerifierContext context, TimeParameters timeParameters, Time[] outputTime) { Matcher m = offsetTimePattern.matcher(value); if (m.matches()) { assert m.groupCount() == 2; String offset = m.group(1); String metric = m.group(2); OffsetTime t = new OffsetTimeImpl(offset, metric); if ((timeParameters.getTimeBase() == TimeBase.CLOCK) && (t.getMetric() == OffsetTime.Metric.Frames)) return false; if (outputTime != null) outputTime[0] = t; if (location != null) updateUsage(context, location, t.getMetric()); return true; } else return false; } private static void updateUsage(VerifierContext context, Location location, OffsetTime.Metric metric) { String key = "usage" + metric.name(); @SuppressWarnings("unchecked") Set<Locator> usage = (Set<Locator>) context.getResourceState(key); if (usage == null) { usage = new java.util.HashSet<Locator>(); context.setResourceState(key, usage); } usage.add(location.getLocator()); } public static void badOffsetTime(String value, Location location, VerifierContext context, TimeParameters timeParameters) { Reporter reporter = context.getReporter(); Locator locator = location.getLocator(); int valueIndex = 0; int valueLength = value.length(); char c; do { // whitespace before time count if (valueIndex == valueLength) break; c = value.charAt(valueIndex); if (Characters.isXMLSpace(c)) { while (Characters.isXMLSpace(c)) { if (++valueIndex >= valueLength) break; c = value.charAt(valueIndex); } reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, XML space padding not permitted before offset time.")); } // time count (digit+) if (valueIndex == valueLength) break; c = value.charAt(valueIndex); if (Characters.isDigit(c)) { while (Characters.isDigit(c)) { if (++valueIndex >= valueLength) break; c = value.charAt(valueIndex); } } // optional fraction (decimal separator) if (valueIndex == valueLength) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing metric in integral offset time.")); break; } if ((c != '.') && !Characters.isLetter(c)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, time count must contain digits followed by optional fraction then metric, got ''{0}'' in offset time.", c)); break; } // optional fraction (digits) if (c == '.') { if (++valueIndex == valueLength) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing fraction part and metric.")); break; } c = value.charAt(valueIndex); if (Characters.isDigit(c)) { while (Characters.isDigit(c)) { if (++valueIndex >= valueLength) break; c = value.charAt(valueIndex); } } else { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing fraction part after decimal separator, must contain one or more digits.")); break; } } // metric if (valueIndex == valueLength) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, missing metric in non-integral offset time.")); break; } StringBuffer sb = new StringBuffer(); c = value.charAt(valueIndex); while (Characters.isLetter(c)) { sb.append(c); if (++valueIndex >= valueLength) break; c = value.charAt(valueIndex); } if (sb.length() == 0) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, unexpected character ''{0}'', expected metric in offset time.", c)); break; } else { String metric = sb.toString(); try { OffsetTime.Metric m = OffsetTime.Metric.valueOfShorthand(metric); if ((timeParameters.getTimeBase() == TimeBase.CLOCK) && (m == OffsetTime.Metric.Frames)) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, frames metric not permitted when using 'clock' time base.")); } } catch (IllegalArgumentException e) { try { OffsetTime.Metric.valueOfShorthand(metric.toLowerCase()); reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, metric ''{0}'' must be lower case in offset time.", metric)); } catch (IllegalArgumentException ee) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, unknown metric ''{0}'' in offset time.", metric)); } break; } } // whitespace after metric if (valueIndex == valueLength) break; c = value.charAt(valueIndex); if (Characters.isXMLSpace(c)) { while (Characters.isXMLSpace(c)) { if (++valueIndex >= valueLength) break; c = value.charAt(valueIndex); } if (valueIndex == valueLength) { reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, XML space padding not permitted after offset time.")); } } // unrecognized non-whitespace characters after offset time if (valueIndex != valueLength) { String remainder = value.substring(valueIndex); reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <timeExpression>, unrecognized characters ''{0}'' after offset time.", remainder)); } } while (false); } }