/*
* Copyright (c) 2012 Chris Ellison, Mike Deats, Liron Yahdav, Ryan Neal,
* Brandon Sutherlin, Scott Griffin
*
* This software is released under the MIT license
* (http://www.opensource.org/licenses/mit-license.php)
*
* Created on Feb 12, 2012
*/
package edu.cmu.sv.arinc838.validation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamReader;
import edu.cmu.sv.arinc838.dao.FileDefinitionDao;
import edu.cmu.sv.arinc838.dao.IntegrityDefinitionDao.IntegrityType;
import edu.cmu.sv.arinc838.dao.SoftwareDefinitionFileDao;
import edu.cmu.sv.arinc838.dao.SoftwareDescriptionDao;
import edu.cmu.sv.arinc838.util.Converter;
/**
* <p>
* This class encapsulates the low-level validation of the different types
* required to build the XDF and BDF files. The methods use one of two paradigms
* for validating input.
* </p>
* <p>
* For simple validation (e.g. {@link #validateUint32(long)}), the methods
* return the input value unchanged if the validation passes or throw an
* {@link IllegalArgumentException} if the validation fails.
* </p>
* <p>
* The reason for this design as opposed to returning a true/false is to allow
* using this method in-line to validate the values without the need for an
* "if/else" check. For example:
* </p>
*
* <pre>
* public void setFileSize(long fileSize) {
* this.fileSize = DataValidator.validateUint32(fileSize);
* }
* </pre>
*
* vs.
*
* <pre>
* public void setFileSize(long fileSize) {
* if(DataValidator.validateUint32(fileSize)) {
* this.fileSize = fileSize
* }
* else {
* throw new IllegalArgumentException("Some error message");
* }
* }
* </pre>
* <p>
* It should be noted that in-line validation of this nature, while convenient,
* violates DO-178B requirements that validation code should be separated from
* the data it is attempting to validate. This implementation of ARINC 838
* follows the DO-178B guidelines in this regard.
* </p>
* <p>
* For more complex validation that may have multiple errors (e.g.
* {@link #validateDataFileName(String)}), the methods return a {@link List} of
* {@link Exception}s for each error encountered. Some severe errors may cause
* the validation to end early, e.g. null checks, and thus only a single error
* would be returned. These methods always return an empty list if no errors
* were found.
* </p>
*
* @author Mike Deats
*
*
*/
public class DataValidator {
/**
* The XML version required. Value is {@value}
*/
public static final String XML_VERSION = "1.0";
/**
* The Character encoding of the XML. Value is {@value}
*/
public static final String XML_ENCODING = "utf-8";
/**
* The maximum length of a STR64k. Value is {@value}
*/
public static final int STR64K_MAX_LENGTH = 65535;
/**
* The maximum length (in bytes) of a HEXBIN64k. Value is {@value}
*/
public static final int HEXBIN64K_MAX_LENGTH = 32768;
public static final String[] INVALID_DATA_FILE_EXTENSIONS = { "bdf", "crc", "dir", "hdr", "ldr", "lci", "lcl",
"lcs", "lna", "lnd", "lnl", "lno", "lnr", "lns", "lub", "luh", "lui", "lum", "lur", "lus", "xdf" };
/**
* A mapping between required attributes to values in the XML header
*/
private static final Map<String, String> ATTRIB_VALUE_MAP = new HashMap<String, String>();
private static final Map<String, String> NS_PREFIX_URI_MAP = new HashMap<String, String>();
/**
* Populate the maps
*/
static {
ATTRIB_VALUE_MAP.put("schemaLocation", "http://www.arinc.com");
NS_PREFIX_URI_MAP.put("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
NS_PREFIX_URI_MAP.put("sdf", "http://www.arinc.com/arinc838");
}
/**
* Validates that the given value is an unsigned 32-bit integer.
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public long validateUint32(long value) {
if ((value >= 0) && (value <= (long) Math.pow(2, 32))) {
return value;
} else {
throw new IllegalArgumentException("The value '" + value + "' is not an unsigned, 32-bit integer");
}
}
/**
* Validates that the input is a STR64k for use in the binary. This is
* defined as a string that is a maximum of {@link STR64K_MAX_LENGTH}
* characters.
*
* @param value
* The input value
* @return A {@link List} of {@link Exception}s that detail the errors found
*/
public List<Exception> validateStr64kBinary(String value) {
ArrayList<Exception> errors = new ArrayList<Exception>();
if (value == null) {
errors.add(new IllegalArgumentException(
"The input value cannot be null"));
} else {
String checked = XmlFormatter.unescapeXmlSpecialChars(value);
try {
checked = checkForNonASCII(checked);
} catch (Exception e) {
errors.add(e);
}
if (checked.length() > STR64K_MAX_LENGTH) {
errors.add(new IllegalArgumentException(
"The input value length of "
+ checked.length()
+ " exceeds the maximum allowed characters of 65535"));
}
}
return errors;
}
private String checkForNonASCII(String value) {
Pattern p = Pattern.compile("[^\\p{ASCII}]");
Matcher m = p.matcher(value);
if (m.find()) {
int index = m.start();
char illegal = value.charAt(index);
throw new IllegalArgumentException("Non-ASCII character '"
+ illegal + "' found at index " + index);
}
return value;
}
/**
* Validates that the input is a STR64k for use in the XML. This is defined
* as a string that
* <ol>
* <li>Has all <, >, and & characters escaped as <, >, and &</li>
* <li>Is a maximum of {@link STR64K_MAX_LENGTH} characters (not including
* escape characters)</li>
* </ol>
*
* @param value
* The input value
* @return A {@link List} of {@link Exception}s that detail the errors found
*
*/
public List<Exception> validateStr64kXml(String value) {
ArrayList<Exception> errors = new ArrayList<Exception>();
if (value == null) {
errors.add(new IllegalAccessException("Str64k cannot be null"));
return errors;
}
errors.addAll(validateStr64kBinary(value));
try {
checkForEscapedXMLChars(value);
} catch (IllegalArgumentException e) {
errors.add(e);
}
return errors;
}
/**
* Validates that the list has a least 1 element
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public List<?> validateList1(List<?> value) {
if (value == null) {
throw new IllegalArgumentException("A LIST1 cannot be null");
}
if (value.size() < 1) {
throw new IllegalArgumentException("A LIST1 must have a size >= 1.");
}
return value;
}
/**
* Checks if the special XML characters <, >, and & are properly escaped
*
* @param value
* The string to check for properly escaped characters
*
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public String checkForEscapedXMLChars(String value) {
int idx = value.indexOf('<');
if (idx != -1) {
throw new IllegalArgumentException("Found unescaped '<' char at index " + idx);
}
idx = value.indexOf('>');
if (idx != -1) {
throw new IllegalArgumentException("Found unescaped '>' char at index " + idx);
}
idx = value.indexOf('&');
if (idx != -1 && !value.matches(".*((&)|(<)|(>)).*")) {
throw new IllegalArgumentException("Found unescaped '&' char at index " + idx);
}
return value;
}
/**
* Validates the file format version value. Must be a byte[4], and have the
* value of {@link SoftwareDefinitionFileDao#DEFAULT_FILE_FORMAT_VERSION}
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public byte[] validateFileFormatVersion(byte[] version) {
if (!Arrays.equals(version,
SoftwareDefinitionFileDao.DEFAULT_FILE_FORMAT_VERSION)) {
throw new IllegalArgumentException(
"File format version was set to 0x"
+ Converter.bytesToHex(version)
+ ", expected 0x"
+ Converter
.bytesToHex(SoftwareDefinitionFileDao.DEFAULT_FILE_FORMAT_VERSION));
}
return version;
}
/**
* Validates the integrity type represents the CRC16, 32, or 64.
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public long validateIntegrityType(long type) {
if (IntegrityType.fromLong(type) == null) {
throw new IllegalArgumentException("Integrity type was invalid. Got " + type + ", expected "
+ IntegrityType.asString());
}
return type;
}
/**
* Validates that the integrity value is a valid byte array that is either
* 2, 4, or 8 bytes long
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public byte[] validateIntegrityValue(byte[] value) {
if (value == null) {
throw new IllegalArgumentException("Integrity value cannot be null");
}
int size = value.length;
if (size != 2 && size != 4 && size != 8) {
throw new IllegalArgumentException("Incorrect number of bytes for integrity value. Got " + size
+ ", expected 2, 4, or 8");
}
return value;
}
/**
* Validates that the input is a valid software part number. The format is
* MMMCC-SSSS-SSSS where
* <ol>
* <li>MMM = The manufacturer code</li>
* <li>CC = The check characters, calculated by XORing the binary
* representation of the other characters, not including the - delimiters</li>
* <li>SSSS-SSSS = The part number. Valid characters are any alphanumeric
* character except I, O, Q, and Z.</li>
* </ol>
*
* Note that even though this validation is more complex than most, each
* validation step must pass before moving to the next. Thus this method
* will not return a list of errors.
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public String validateSoftwarePartNumber(String value) {
if (value == null) {
throw new IllegalArgumentException("Software part number cannot be null");
}
// Just check the basic format first: MMMCC-SSSS-SSSS
if (!value.matches("[A-Z0-9]{5}-[A-Z0-9]{4}-[A-Z0-9]{4}")) {
throw new IllegalArgumentException("Software part number format was invalid. Got " + value
+ ", expected format to be " + SoftwareDescriptionDao.SOFTWARE_PART_NUMBER_FORMAT);
}
checkForIllegalCharsInPartNumber(value);
validateCheckCharacters(value);
return value;
}
/**
* <p>
* Takes a syntactically valid (put partial) software part number string and
* returns a fully valid software part number including the calculated check
* characters. The check characters must replaced with place holder
* characters, otherwise an IllegalArgumentException will be thrown. For
* example, the string ABC??-1234-5678 is a valid input but the string
* ABC-1234-5678 is not.
* </p>
* See {@link #validateSoftwarePartNumber(String)} for more information on a
* valid software part number string.
*
* @param value
* The input value
* @return The validated input value
* @throws IllegalArgumentException
* if the input value does not validate.
*/
public String generateSoftwarePartNumber(String value) {
if (value == null) {
throw new IllegalArgumentException("Software part number cannot be null");
}
checkForIllegalCharsInPartNumber(value);
String check = generateCheckCharacters(value);
String fullPart = value.substring(0, 3) + check + value.substring(5);
return validateSoftwarePartNumber(fullPart);
}
private String checkForIllegalCharsInPartNumber(String value) {
String partNumber = null;
try {
partNumber = value.substring(value.indexOf('-') + 1);
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalArgumentException("Software part number format was invalid. Got " + value
+ ", expected format to be " + SoftwareDescriptionDao.SOFTWARE_PART_NUMBER_FORMAT);
}
// check for illegal characters in the SSSS-SSSS (part number) section
Pattern p = Pattern.compile("[iIoOqQzZ]");
Matcher m = p.matcher(partNumber);
if (m.find()) {
int index = m.start();
char illegal = partNumber.charAt(index);
throw new IllegalArgumentException("Software part number contains illegal character '" + illegal
+ "' at index " + index);
}
return value;
}
private String validateCheckCharacters(String partNumber) {
String check = partNumber.substring(3, 5);
String checked = generateCheckCharacters(partNumber);
if (!check.equals(checked)) {
throw new IllegalArgumentException(
"Software part number check characters did not validate. Got "
+ checked + ", expected " + check + ".");
}
return check;
}
/**
* <pre>
* Step 1: Establish the characters for the PN before the check characters are known:
* ACM??-1234-5678 (?? denoting unresolved CC values, not included in the
* calculation)
* Step 2: Exclude delimiters and the unresolved CC values, resulting in: ACM12345678
* Step 3: Convert the ASCII characters to binary
* Step 4: XOR all the binary characters
* Step 5: Express the resulting value in upper case hexadecimal characters:
* 0x47 => “47”
* Step 6: Construct the final PN, including delimiters:
* ACM47-1234-5678
* </pre>
*
*/
private String generateCheckCharacters(String partNumber) {
String data = partNumber.substring(0, 3) + partNumber.substring(6, 10) + partNumber.substring(11);
int result = 0;
for (char c : data.toCharArray()) {
result = result ^ c;
}
return Integer.toHexString(result).toUpperCase();
}
/**
* Validates that the byte array is a valid HEXBIN32, which has a maximum
* length of 4 bytes
*
* @param value
* The HEXBIN32 value to validate
*
* @return The validated value
* @throws IllegalArgumentException
* if the value does not validate
*/
public byte[] validateHexbin32(byte[] value) {
if (value == null) {
throw new IllegalArgumentException("Hexbin 32 type cannot be null");
} else if (value.length != 4) {
throw new IllegalArgumentException("Hexbin 32 type must be 4 bytes");
}
return value;
}
/**
* Validates that the byte array is a valid HEXBIN64k, which has a maximum
* length of {@link #HEXBIN64K_MAX_LENGTH} bytes
*
* @param value
* The HEXBIN64k value to validate
* @return The validated value
* @throws IllegalArgumentException
* if the value does not validate
*/
public byte[] validateHexbin64k(byte[] value) {
if (value == null) {
throw new IllegalArgumentException("Hexbin 64k type cannot be null");
} else if (value.length > HEXBIN64K_MAX_LENGTH) {
throw new IllegalArgumentException("Hexbin 64k type must be =< " + HEXBIN64K_MAX_LENGTH + " bytes");
}
return value;
}
/**
* Validates the filename of a data file. Data file names must meet the
* following criteria:
* <ol>
* <li>Must be less than 256 characters, include extension and '.' delimiter
* </li>
* <li>Must have an extension following the '.' delimiter, e.g. '.bin'</li>
* <li>The extension cannot be one of the reserved extensions listed in
* {@link #INVALID_DATA_FILE_EXTENSIONS}</li>
* <li>Cannot contain illegal characters ", ', `, *, <, >, :, ;, #, ?, /, \,
* |, ~, !, @, $, %, ^, &, +, =, 'comma', or whitespace
* </ol>
*
* @param fileName
* The data file name to validate
* @return A {@link List} of {@link Exception}s that detail the errors found
*
*/
public List<Exception> validateDataFileName(String fileName) {
ArrayList<Exception> errors = new ArrayList<Exception>();
if (fileName == null) {
errors.add(new IllegalArgumentException("File name cannot be null"));
return errors;
}
if (fileName.length() > 255) {
errors.add(new IllegalArgumentException("File name must be <= 255 characters. The length is "
+ fileName.length()));
}
// check extension
String name = fileName;
try {
name = fileName.substring(0, fileName.lastIndexOf("."));
String extension = fileName
.substring(fileName.lastIndexOf(".") + 1);
if (Arrays.asList(INVALID_DATA_FILE_EXTENSIONS).contains(
extension.toLowerCase())) {
errors.add(new IllegalArgumentException("File name '"
+ fileName + "' contained an illegal extension '"
+ extension + "'."));
}
} catch (StringIndexOutOfBoundsException e) {
errors.add(new IllegalArgumentException("File name '" + fileName
+ "' must have an extension, e.g. '.bin'"));
}
// check file names for bad characters
Pattern p = Pattern.compile("[\"'`*<>:;#?/\\\\|~!@$%^&+=,\\s]");
Matcher m = p.matcher(name);
if (m.find()) {
int index = m.start();
char illegal = name.charAt(index);
errors.add(new IllegalArgumentException("File name '" + fileName
+ "' contained an illegal character '" + illegal
+ "' at index " + index + "."));
}
return errors;
}
/**
* Validates that each data file within an LSP is unique, regardless of
* case. For example, the names abc.doc and ABC.doc are not allowed within
* the same LSP.
*
* @param fileDefs
* List of {@link FileDefinitionDao file definitions} to check
* for uniqueness
* @return A {@link List} of {@link Exception}s that detail the errors found
*/
public List<Exception> validateDataFileNamesAreUnique(List<FileDefinitionDao> fileDefs) {
ArrayList<Exception> errors = new ArrayList<Exception>();
// map of lower case file names to list of actual file names
Map<String, List<String>> fileNames = new HashMap<String, List<String>>();
for (FileDefinitionDao fileDef : fileDefs) {
List<String> values = fileNames.get(fileDef.getFileName()
.toLowerCase());
if (values == null) {
values = new ArrayList<String>();
fileNames.put(fileDef.getFileName().toLowerCase(), values);
}
values.add(fileDef.getFileName());
}
for (String lowerCaseFileNames : fileNames.keySet()) {
String fileNameList = fileNames.get(lowerCaseFileNames).toString()
.replaceAll("^\\[|\\]$", "");
if (fileNames.get(lowerCaseFileNames).size() > 1) {
errors.add(new IllegalArgumentException(
"Duplicate data file names: " + fileNameList + "."));
}
}
return errors;
}
/**
* verifies that the attributes specified on the opening element are what is
* specified in the spec. This will ignore trailing/leading whitespace and
* capitalization.
*
* @param xsr
* @return
*/
public List<Exception> validateXmlHeaderAttributes(XMLStreamReader xsr) {
List<Exception> errors = new ArrayList<Exception>();
Map<String, String> localCopy = new HashMap<String, String>(ATTRIB_VALUE_MAP);
int ncCount = xsr.getAttributeCount();
for (int i = 0; i < ncCount; ++i) {
String name = xsr.getAttributeLocalName(i);
String value = xsr.getAttributeValue(i);
value = value.trim();
if (localCopy.containsKey(name)) {
String expected = localCopy.remove(name);
if (!value.equalsIgnoreCase(expected)) {
Exception e = new IllegalArgumentException("Attribute: " + name + " is wrong. Expected: '"
+ expected + "' found: '" + value + "'");
errors.add(e);
}
} else {
Exception e = new IllegalArgumentException("Attribute: '" + name + "' is unexpected.");
errors.add(e);
}
}
if (!localCopy.isEmpty()) {
String missingList = "";
for (String item : localCopy.keySet()) {
missingList += " " + item;
}
Exception e = new IllegalArgumentException("Attributes missing: " + missingList);
errors.add(e);
}
if(xsr.standaloneSet()) {
Exception e = new IllegalArgumentException("Standalone attribute should not be set.");
errors.add(e);
}
if (!DataValidator.XML_ENCODING.equalsIgnoreCase(xsr.getCharacterEncodingScheme())) {
Exception e = new IllegalArgumentException("The XML Encoding is wrong." +
"Expected: " + DataValidator.XML_ENCODING +
" found: " + xsr.getCharacterEncodingScheme());
errors.add(e);
}
if (!DataValidator.XML_VERSION.equalsIgnoreCase(xsr.getVersion())) {
Exception e = new IllegalArgumentException("The XML version is wrong." +
"Expected: " + DataValidator.XML_VERSION +
" found: " + xsr.getVersion());
errors.add(e);
}
return errors;
}
/**
* Verifes that only the namespaces specified in the spec are present. It
* will ignore whitespace and capitalization
*
* @param xsr
* @return
*/
public List<Exception> validateXmlHeaderNamespaces(XMLStreamReader xsr) {
List<Exception> errors = new ArrayList<Exception>();
Map<String, String> localCopy = new HashMap<String, String>(NS_PREFIX_URI_MAP);
int ncCount = xsr.getNamespaceCount();
for (int i = 0; i < ncCount; ++i) {
String name = xsr.getNamespacePrefix(i);
String value = xsr.getNamespaceURI(i);
value = value.trim();
if (localCopy.containsKey(name)) {
String expected = localCopy.remove(name);
if (!value.equalsIgnoreCase(expected)) {
Exception e = new IllegalArgumentException("Namespace: '" + name + "' is wrong. Expected: '"
+ expected + "' found: '" + value + "'");
errors.add(e);
}
} else {
Exception e = new IllegalArgumentException("Namespace Prefix: '" + name + "' is unexpected.");
errors.add(e);
}
}
if (!localCopy.isEmpty()) {
String missingList = "";
for (String item : localCopy.keySet()) {
missingList += " " + item;
}
Exception e = new IllegalArgumentException("Namespaces missing: " + missingList);
errors.add(e);
}
return errors;
}
}