/*
* 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.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
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.Length;
import com.skynav.ttv.model.value.impl.LengthImpl;
import com.skynav.ttv.util.Location;
import com.skynav.ttv.util.Reporter;
import com.skynav.ttv.verifier.VerifierContext;
import com.skynav.ttv.verifier.util.MixedUnitsTreatment;
import com.skynav.ttv.verifier.util.NegativeTreatment;
public class Lengths {
public static boolean maybeLength(String value) {
value = value.trim();
if (value.length() == 0)
return false;
char c1 = value.charAt(0);
char c2 = (value.length() > 1) ? value.charAt(1) : 0;
char c3 = (value.length() > 2) ? value.charAt(2) : 0;
if ((c1 == '+') || (c1 == '-')) {
if (Characters.isDigit(c2))
return true;
else if ((c2 == '.') && Characters.isDigit(c3))
return true;
else
return false;
} else if (Characters.isDigit(c1)) {
return true;
} else if ((c1 == '.') && Characters.isDigit(c2)) {
return true;
} else {
return false;
}
}
private static final Pattern lengthPattern = Pattern.compile("([\\+\\-]?(?:\\d*.\\d+|\\d+))(\\w+|%)?");
public static boolean isLength(String value, Location location, VerifierContext context, Object[] treatments, Length[] outputLength) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
Matcher m = lengthPattern.matcher(value);
if (m.matches()) {
assert m.groupCount() > 0;
String number = m.group(1);
if (number.charAt(0) == '+')
number = number.substring(1);
double numberValue;
if (!Strings.containsDecimalSeparator(number)) {
try {
numberValue = new BigInteger(number).doubleValue();
} catch (NumberFormatException e) {
return false;
}
} else {
try {
numberValue = new BigDecimal(number).doubleValue();
} catch (NumberFormatException e) {
return false;
}
}
if (treatments != null) {
if (numberValue < 0) {
NegativeTreatment negativeTreatment = (NegativeTreatment) treatments[0];
if (negativeTreatment == NegativeTreatment.Error)
return false;
else if (reporter != null) {
if (negativeTreatment == NegativeTreatment.Warning) {
if (reporter.logWarning(reporter.message(locator, "*KEY*",
"Negative <length> expression {0} should not be used.", Numbers.normalize(numberValue)))) {
treatments[0] = NegativeTreatment.Allow; // suppress second warning
return false;
}
} else if (negativeTreatment == NegativeTreatment.Info) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Negative <length> expression {0} used.", Numbers.normalize(numberValue)));
}
}
}
}
Length.Unit unitsValue = null;
if (m.groupCount() > 1) {
String units = m.group(2);
if (units != null) {
try {
unitsValue = Length.Unit.valueOfShorthand(units);
int unitVersion = unitsValue.fromVersion();
int ttmlVersion = context.getModel().getTTMLVersion();
if (unitVersion > ttmlVersion) {
return false;
} else if (!isUnitsPermitted(unitsValue, location, context))
return false;
} catch (IllegalArgumentException e) {
return false;
}
}
}
if (unitsValue == null) {
if (isUnitsRequired(location, context)) {
return false;
} else {
unitsValue = Length.Unit.Pixel;
}
}
if (outputLength != null)
outputLength[0] = new LengthImpl(numberValue, unitsValue);
if (locator != null)
updateUsage(context, locator, unitsValue);
return true;
} else
return false;
}
private static void updateUsage(VerifierContext context, Locator locator, Length.Unit unit) {
String key = "usage" + unit.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(locator);
}
public static void badLength(String value, Location location, VerifierContext context, Object[] treatments) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
boolean negative = false;
double numberValue = 0;
Length.Unit units = null;
do {
int valueIndex = 0;
int valueLength = value.length();
BigDecimal integralPart = null;
BigDecimal fractionalPart = null;
char c;
// whitespace before optional sign
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 <length> expression, XML space padding not permitted before number."));
}
// optional sign before non-negative-number
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (c == '+')
++valueIndex;
else if (c == '-') {
negative = true;
++valueIndex;
}
// whitespace before non-negative-number
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 <length> expression, XML space padding not permitted between sign and non-negative-number."));
}
// non-negative-number (integral part)
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (Characters.isDigit(c)) {
StringBuffer sb = new StringBuffer();
while (Characters.isDigit(c)) {
sb.append(c);
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
if (sb.length() > 0) {
try {
integralPart = new BigDecimal(new BigInteger(sb.toString()));
} catch (NumberFormatException e) {
}
}
}
// non-negative-number (fractional part)
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (c == '.') {
c = value.charAt(++valueIndex);
if (Characters.isDigit(c)) {
StringBuffer sb = new StringBuffer();
while (Characters.isDigit(c)) {
sb.append(c);
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
if (sb.length() > 0) {
try {
fractionalPart = new BigDecimal(new BigInteger(sb.toString()),sb.length());
} catch (NumberFormatException e) {
}
}
}
}
// number
if (integralPart == null) {
if (fractionalPart == null) {
reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <length> expression, missing number after optional sign."));
} else {
numberValue = fractionalPart.doubleValue();
}
} else {
if (fractionalPart == null) {
numberValue = integralPart.doubleValue();
} else {
numberValue = integralPart.add(fractionalPart).doubleValue();
}
}
// whitespace before units
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 <length> expression, XML space padding not permitted between number and units."));
}
// units
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (c == '%') {
units = Length.Unit.Percentage;
++valueIndex;
} else if (Characters.isLetter(c)) {
StringBuffer sb = new StringBuffer();
while (Characters.isLetter(c)) {
sb.append(c);
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
String unitsAsString = sb.toString();
try {
units = Length.Unit.valueOfShorthand(unitsAsString);
int unitVersion = units.fromVersion();
int ttmlVersion = context.getModel().getTTMLVersion();
if (unitVersion > ttmlVersion) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <length> expression, units ''{0}'' not supported in TTML version {1}, requires TTML version {2}.",
unitsAsString, ttmlVersion, unitVersion));
}
} catch (IllegalArgumentException e) {
try {
units = Length.Unit.valueOfShorthandIgnoringCase(unitsAsString);
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <length> expression, units is not expressed with correct case, got ''{0}'', expected ''{1}''.",
unitsAsString, units.shorthand()));
} catch (IllegalArgumentException ee) {
reporter.logInfo(reporter.message(locator, "*KEY*", "Bad <length> expression, unknown units ''{0}''.", unitsAsString));
}
}
}
// whitespace after units
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 <length> expression, XML space padding not permitted after units."));
}
// garbage after (units S*)
if (valueIndex < valueLength) {
StringBuffer sb = new StringBuffer();
while (valueIndex < valueLength) {
sb.append(value.charAt(valueIndex++));
}
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <length> expression, unrecognized characters not permitted after units, got ''{0}''.", sb.toString()));
}
} while (false);
if (negative)
numberValue = -numberValue;
if (treatments != null) {
NegativeTreatment negativeTreatment = (NegativeTreatment) treatments[0];
if ((numberValue < 0) && (negativeTreatment == NegativeTreatment.Error)) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <length> expression, negative value {0} not permitted.", Numbers.normalize(numberValue)));
}
}
if (units == null) {
if (isUnitsRequired(location, context))
unitsRequired(location, context);
} else {
if (!isUnitsPermitted(units, location, context))
unitsNotPermitted(units, location, context);
}
}
private static boolean isUnitsPermitted(Length.Unit units, Location location, VerifierContext context) {
return context.getModel().getStyleVerifier().isLengthUnitsPermitted(location.getElementName(), location.getAttributeName(), units);
}
private static void unitsNotPermitted(Length.Unit units, Location location, VerifierContext context) {
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message(location.getLocator(),
"*KEY*", "Bad <length> expression, units value {0} not permitted on {1}.", units.shorthand(), location.getAttributeName()));
}
private static boolean isUnitsRequired(Location location, VerifierContext context) {
return context.getModel().getStyleVerifier().isLengthUnitsRequired(location.getElementName(), location.getAttributeName());
}
private static void unitsRequired(Location location, VerifierContext context) {
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message(location.getLocator(),
"*KEY*", "Bad <length> expression, missing units, expected one of {0}.", Length.Unit.shorthands()));
}
public static boolean isLengths(String value, Location location, VerifierContext context, Integer[] minMax, Object[] treatments, List<Length> outputLengths) {
Reporter reporter = (context != null) ? context.getReporter() : null;
Locator locator = location.getLocator();
List<Length> lengths = new java.util.ArrayList<Length>();
String [] lengthComponents = value.split("[ \t\r\n]+");
int numComponents = lengthComponents.length;
for (String component : lengthComponents) {
Length[] length = new Length[1];
if (isLength(component, location, context, treatments, length))
lengths.add(length[0]);
else
return false;
}
if (minMax != null) {
if (numComponents < minMax[0])
return false;
else if (numComponents > minMax[1])
return false;
}
if (treatments != null) {
if (!Units.sameUnits(lengths)) {
assert treatments.length > 1;
MixedUnitsTreatment mixedUnitsTreatment = (MixedUnitsTreatment) treatments[1];
Set<Length.Unit> units = Units.units(lengths);
if (mixedUnitsTreatment == MixedUnitsTreatment.Error)
return false;
else if (reporter != null) {
if (mixedUnitsTreatment == MixedUnitsTreatment.Warning) {
if (reporter.logWarning(reporter.message(locator, "*KEY*",
"Mixed units {0} should not be used in <length> expressions.", Length.Unit.shorthands(units)))) {
treatments[1] = MixedUnitsTreatment.Allow; // suppress second warning
return false;
}
} else if (mixedUnitsTreatment == MixedUnitsTreatment.Info) {
reporter.logInfo(reporter.message(locator, "*KEY*", "Mixed units {0} used in <length> expressions.", Length.Unit.shorthands(units)));
}
}
}
}
if (outputLengths != null) {
outputLengths.clear();
outputLengths.addAll(lengths);
}
return true;
}
public static void badLengths(String value, Location location, VerifierContext context, Integer[] minMax, Object[] treatments) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
List<Length> lengths = new java.util.ArrayList<Length>();
String [] lengthComponents = value.split("[ \t\r\n]+");
int numComponents = lengthComponents.length;
Object[] treatmentsInner = (treatments != null) ? new Object[] { treatments[0], treatments[1] } : null;
for (String component : lengthComponents) {
Length[] length = new Length[1];
if (isLength(component, location, context, treatmentsInner, length))
lengths.add(length[0]);
else
badLength(component, location, context, treatmentsInner);
}
if (numComponents < minMax[0]) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Missing <length> expression, got {0}, but expected at least {1} <length> expressions.", numComponents, minMax[0]));
} else if (numComponents > minMax[1]) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Extra <length> expression, got {0}, but expected no more than {1} <length> expressions.", numComponents, minMax[1]));
}
if (treatments != null) {
MixedUnitsTreatment mixedUnitsTreatment = (MixedUnitsTreatment) treatments[1];
if (!Units.sameUnits(lengths) && (mixedUnitsTreatment == MixedUnitsTreatment.Error)) {
reporter.logInfo(reporter.message(locator, "*KEY*", "Mixed units {0} not permitted.", Length.Unit.shorthands(Units.units(lengths))));
}
}
}
}