diff --git a/src/main/java/emissary/core/IBaseDataObjectXmlCodecs.java b/src/main/java/emissary/core/IBaseDataObjectXmlCodecs.java new file mode 100644 index 0000000000..247da35ceb --- /dev/null +++ b/src/main/java/emissary/core/IBaseDataObjectXmlCodecs.java @@ -0,0 +1,654 @@ +package emissary.core; + +import emissary.core.channels.InMemoryChannelFactory; +import emissary.core.channels.SeekableByteChannelFactory; +import emissary.core.channels.SeekableByteChannelHelper; +import emissary.util.ByteUtil; +import emissary.util.xml.AbstractJDOMUtil; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap.SimpleEntry; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.XMLConstants; + +import static emissary.core.IBaseDataObjectXmlHelper.BIRTH_ORDER_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.BROKEN_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.NUM_CHILDREN_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.NUM_SIBLINGS_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.OUTPUTABLE_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.PARAMETER_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.PRIORITY_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.PROCESSING_ERROR_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.VALUE_ELEMENT_NAME; +import static emissary.core.IBaseDataObjectXmlHelper.VIEW_ELEMENT_NAME; + +/** + * This class contains the interfaces and implementations used to convert an IBDO->XML and XML->IBDO. + */ +public final class IBaseDataObjectXmlCodecs { + /** + * Logger instance + */ + private static final Logger LOGGER = LoggerFactory.getLogger(IBaseDataObjectXmlCodecs.class); + /** + * The XML attribute name for Base64. + */ + public static final String BASE64_ATTRIBUTE_NAME = "base64"; + /** + * The XML attribute name for SHA256. + */ + public static final String SHA256_ATTRIBUTE_NAME = "sha256"; + /** + * New line string to use for normalised XML + */ + public static final String BASE64_NEW_LINE_STRING = new String(new byte[] {'\n'}); + /** + * Max width of Base64 char block. + */ + public static final int BASE64_LINE_WIDTH = 76; + /** + * The Base64 encoder. + * + * Uses same width as default, but overrides new line separator to use normalized XML separator. + * + * See http://www.jdom.org/docs/apidocs/org/jdom2/output/Format.html#setLineSeparator(java.lang.String) + */ + public static final Base64.Encoder BASE64_ENCODER = Base64.getMimeEncoder(BASE64_LINE_WIDTH, new byte[] {'\n'}); + /** + * The Base64 decoder. + */ + private static final Base64.Decoder BASE64_DECODER = Base64.getMimeDecoder(); + /** + * The XML attribute name for Encoding. + */ + public static final String ENCODING_ATTRIBUTE_NAME = "encoding"; + /** + * The XML element name for Name. + */ + public static final String NAME_ELEMENT_NAME = "name"; + /** + * A map of element names of IBaseDataObject methods that get/set primitives and their default values. + */ + public static final Map PRIMITVE_NAME_DEFAULT_MAP = Collections + .unmodifiableMap(new ConcurrentHashMap<>(Stream.of( + new SimpleEntry<>(BIRTH_ORDER_ELEMENT_NAME, new BaseDataObject().getBirthOrder()), + new SimpleEntry<>(BROKEN_ELEMENT_NAME, new BaseDataObject().isBroken()), + new SimpleEntry<>(NUM_CHILDREN_ELEMENT_NAME, new BaseDataObject().getNumChildren()), + new SimpleEntry<>(NUM_SIBLINGS_ELEMENT_NAME, new BaseDataObject().getNumSiblings()), + new SimpleEntry<>(OUTPUTABLE_ELEMENT_NAME, new BaseDataObject().isOutputable()), + new SimpleEntry<>(PRIORITY_ELEMENT_NAME, new BaseDataObject().getPriority())) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))); + /** + * The XML namespace for "xml". + */ + public static final Namespace XML_NAMESPACE = Namespace.getNamespace(XMLConstants.XML_NS_PREFIX, + XMLConstants.XML_NS_URI); + + /** + * Interface for decoding an element value. + */ + public interface ElementDecoder { + /** + * Decodes and XML element value and sets it on the specified IBDO method. + * + * @param elements the list of elements to be decoded. + * @param ibdo the ibdo to set the values on. + * @param ibdoMethodName the ibdo method name to use to set the values. + * @throws Exception thrown if anything goes wrong. + */ + void decode(List elements, IBaseDataObject ibdo, String ibdoMethodName) throws Exception; + } + + /** + * Interface for encoding an element value. + */ + public interface ElementEncoder { + /** + * Encodes a list of values into an element that is attached to the parent element with the specified child element + * name. + * + * @param values to be encoded. + * @param parentElement that the child element is to be attached to. + * @param childElementName the name of the child element. + */ + void encode(List values, Element parentElement, String childElementName); + } + + /** + * Class that contains the element decoders. + */ + public static class ElementDecoders { + /** + * Decoder for boolean elements. + */ + public final ElementDecoder booleanDecoder; + /** + * Decoder for byte[] elements. + */ + public final ElementDecoder byteArrayDecoder; + /** + * Decoder for integer elements. + */ + public final ElementDecoder integerDecoder; + /** + * Decoder for SeekableByteChannel elements. + */ + public final ElementDecoder seekableByteChannelFactoryDecoder; + /** + * Decoder for Map<String,byte[]> elements. + */ + public final ElementDecoder stringByteArrayDecoder; + /** + * Decoder for String elements. + */ + public final ElementDecoder stringDecoder; + /** + * Decoder for Map<String,Collection<Object>> elements. + */ + public final ElementDecoder stringObjectDecoder; + + /** + * Constructs a container for the XML element decoders. + * + * @param booleanDecoder decoder for boolean elements + * @param byteArrayDecoder decoder for byte[] elements + * @param integerDecoder decoder for integer elements + * @param seekableByteChannelFactoryDecoder decoder for SeekableByteChannelElements. + * @param stringByteArrayDecoder decoder for Map<String,byte[]> elements + * @param stringDecoder decoder for String elements + * @param stringObjectDecoder decoder for Map<String,Collection<Object>> elements + */ + public ElementDecoders( + final ElementDecoder booleanDecoder, + final ElementDecoder byteArrayDecoder, + final ElementDecoder integerDecoder, + final ElementDecoder seekableByteChannelFactoryDecoder, + final ElementDecoder stringByteArrayDecoder, + final ElementDecoder stringDecoder, + final ElementDecoder stringObjectDecoder) { + Validate.notNull(booleanDecoder, "Required: booleanDecoder not null!"); + Validate.notNull(byteArrayDecoder, "Required: byteArrayDecoder not null!"); + Validate.notNull(integerDecoder, "Required: integerDecoder not null!"); + Validate.notNull(seekableByteChannelFactoryDecoder, "Required: seekableByteChannelFactoryDecoder not null!"); + Validate.notNull(stringByteArrayDecoder, "Required: stringByteArrayDecoder not null!"); + Validate.notNull(stringDecoder, "Required: stringDecoder not null!"); + Validate.notNull(stringObjectDecoder, "Required: stringObjectDecoder not null!"); + + this.booleanDecoder = booleanDecoder; + this.byteArrayDecoder = byteArrayDecoder; + this.integerDecoder = integerDecoder; + this.seekableByteChannelFactoryDecoder = seekableByteChannelFactoryDecoder; + this.stringByteArrayDecoder = stringByteArrayDecoder; + this.stringDecoder = stringDecoder; + this.stringObjectDecoder = stringObjectDecoder; + } + } + + /** + * Class that contains the element encoders. + */ + public static class ElementEncoders { + /** + * Encoder for boolean elements. + */ + public final ElementEncoder booleanEncoder; + /** + * Encoder for byte[] elements. + */ + public final ElementEncoder byteArrayEncoder; + /** + * Encoder for integer elements. + */ + public final ElementEncoder integerEncoder; + /** + * Encoder for SeekableByteChannel elements. + */ + public final ElementEncoder seekableByteChannelFactoryEncoder; + /** + * Encoder for Map<String,byte[]> elements. + */ + public final ElementEncoder> stringByteArrayEncoder; + /** + * Encoder for String elements. + */ + public final ElementEncoder stringEncoder; + /** + * Encoder for Map<String, Collection<Object>> elements. + */ + public final ElementEncoder>> stringObjectEncoder; + + /** + * Constructs a container for the XML element encoders. + * + * @param booleanEncoder encoder for boolean elements + * @param byteArrayEncoder encoder for byte[] elements + * @param integerEncoder encoder for integer elements + * @param seekableByteChannelFactoryEncoder encoder for SeekableByteChannel elements + * @param stringByteArrayEncoder encoder for Map<String,byte[]> elements + * @param stringEncoder encoder for String elements. + * @param stringObjectEncoder encoder for Map<String, Collection<Object>> elements + */ + public ElementEncoders( + final ElementEncoder booleanEncoder, + final ElementEncoder byteArrayEncoder, + final ElementEncoder integerEncoder, + final ElementEncoder seekableByteChannelFactoryEncoder, + final ElementEncoder> stringByteArrayEncoder, + final ElementEncoder stringEncoder, + final ElementEncoder>> stringObjectEncoder) { + Validate.notNull(booleanEncoder, "Required: booleanEncoder not null!"); + Validate.notNull(byteArrayEncoder, "Required: byteArrayEncoder not null!"); + Validate.notNull(integerEncoder, "Required: integerEncoder not null!"); + Validate.notNull(seekableByteChannelFactoryEncoder, "Required: seekableByteChannelFactoryEncoder not null!"); + Validate.notNull(stringByteArrayEncoder, "Required: stringByteArrayEncoder not null!"); + Validate.notNull(stringEncoder, "Required: stringEncoder not null!"); + Validate.notNull(stringObjectEncoder, "Required: stringObjectEncoder not null!"); + + this.booleanEncoder = booleanEncoder; + this.byteArrayEncoder = byteArrayEncoder; + this.integerEncoder = integerEncoder; + this.seekableByteChannelFactoryEncoder = seekableByteChannelFactoryEncoder; + this.stringByteArrayEncoder = stringByteArrayEncoder; + this.stringEncoder = stringEncoder; + this.stringObjectEncoder = stringObjectEncoder; + } + } + + /** + * Implementation of an XML element decoder that has a boolean value. + */ + public static final ElementDecoder DEFAULT_BOOLEAN_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, boolean.class); + + for (final Element element : elements) { + method.invoke(ibdo, Boolean.valueOf(element.getValue())); + } + }; + + /** + * Implementation of an XML element decoder that has a SeekableByteChannel value. + */ + public static final ElementDecoder DEFAULT_SEEKABLE_BYTE_CHANNEL_FACTORY_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, SeekableByteChannelFactory.class); + + for (final Element element : elements) { + final String elementValue = element.getValue(); + final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + + method.invoke(ibdo, InMemoryChannelFactory.create(extractBytes(encoding, elementValue))); + } + }; + + /** + * Implementation of an XML element decoder that has a byte array value. + */ + public static final ElementDecoder DEFAULT_BYTE_ARRAY_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, byte[].class); + + for (final Element element : elements) { + final String elementValue = element.getValue(); + final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + + method.invoke(ibdo, extractBytes(encoding, elementValue)); + } + }; + + /** + * Implementation of an XML element decoder that has an integer value. + */ + public static final ElementDecoder DEFAULT_INTEGER_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, int.class); + + for (final Element element : elements) { + method.invoke(ibdo, Integer.decode(element.getValue())); + } + }; + + /** + * Implementation of an XML element decoder that has a string value. + */ + public static final ElementDecoder DEFAULT_STRING_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, String.class); + + for (final Element element : elements) { + final String elementValue = element.getValue(); + final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + + method.invoke(ibdo, new String(extractBytes(encoding, elementValue), StandardCharsets.UTF_8)); + } + }; + + /** + * Implementation of an XML element decoder that has a mapped value where the key is a string and the value is a byte + * array. + */ + public static final ElementDecoder DEFAULT_STRING_BYTE_ARRAY_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, String.class, byte[].class); + + for (final Element element : elements) { + final Element nameElement = element.getChild(NAME_ELEMENT_NAME); + final String name = nameElement.getValue(); + final String nameEncoding = nameElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + final String nameDecoded = new String(extractBytes(nameEncoding, name), StandardCharsets.UTF_8); + final Element valueElement = element.getChild(VALUE_ELEMENT_NAME); + final String value = valueElement.getValue(); + final String valueEncoding = valueElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + final byte[] valueDecoded = extractBytes(valueEncoding, value); + + method.invoke(ibdo, nameDecoded, valueDecoded); + } + }; + + /** + * Implementation of an XML element decoder that has a mapped value where the key is a string and the value is an + * object. + */ + public static final ElementDecoder DEFAULT_STRING_OBJECT_DECODER = (elements, ibdo, ibdoMethodName) -> { + final Method method = getIbdoMethod(ibdoMethodName, String.class, Object.class); + + for (final Element element : elements) { + final Element nameElement = element.getChild(NAME_ELEMENT_NAME); + final String name = nameElement.getValue(); + final String nameEncoding = nameElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + final String nameDecoded = new String(extractBytes(nameEncoding, name), StandardCharsets.UTF_8); + final Element valueElement = element.getChild(VALUE_ELEMENT_NAME); + final String value = valueElement.getValue(); + final String valueEncoding = valueElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + final String valueDecoded = new String(extractBytes(valueEncoding, value)); + + method.invoke(ibdo, nameDecoded, valueDecoded); + } + }; + + /** + * An implementation of an XML element encoder for SeekableByteChannel's that produces a base64 value. + */ + public static final ElementEncoder DEFAULT_SEEKABLE_BYTE_CHANNEL_FACTORY_ENCODER = + new SeekableByteChannelFactoryEncoder(); + + private static class SeekableByteChannelFactoryEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (final SeekableByteChannelFactory value : values) { + if (value != null) { + try { + final byte[] bytes = SeekableByteChannelHelper.getByteArrayFromChannel(value, + BaseDataObject.MAX_BYTE_ARRAY_SIZE); + + parentElement.addContent(preserve(protectedElementBase64(childElementName, bytes))); + } catch (final IOException e) { + LOGGER.error("Could not get bytes from SeekableByteChannel!", e); + } + } + } + } + } + + /** + * An implementation of an XML element encoder for SeekableByteChannel's that produces a SHA256 hash value. + */ + public static final ElementEncoder SHA256_SEEKABLE_BYTE_CHANNEL_FACTORY_ENCODER = + new HashSeekableByteChannelFactoryEncoder(); + + private static class HashSeekableByteChannelFactoryEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (final SeekableByteChannelFactory value : values) { + if (value != null) { + try { + final byte[] bytes = SeekableByteChannelHelper.getByteArrayFromChannel(value, + BaseDataObject.MAX_BYTE_ARRAY_SIZE); + + parentElement.addContent(preserve(protectedElementHash(childElementName, bytes))); + } catch (final IOException e) { + LOGGER.error("Could not get bytes from SeekableByteChannel!", e); + } + } + } + } + + private static Element protectedElementHash(final String name, final byte[] bytes) { + final Element element = new Element(name); + + if (ByteUtil.hasNonPrintableValues(bytes)) { + element.setAttribute(ENCODING_ATTRIBUTE_NAME, SHA256_ATTRIBUTE_NAME); + element.addContent(ByteUtil.sha256Bytes(bytes)); + } else { + element.addContent(new String(bytes, StandardCharsets.ISO_8859_1)); + } + + return element; + } + } + + /** + * An implementation of an XML element encoder for integers. + */ + public static final ElementEncoder DEFAULT_INTEGER_ENCODER = new IntegerEncoder(); + + private static class IntegerEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (final int value : values) { + if (((Integer) PRIMITVE_NAME_DEFAULT_MAP.get(childElementName)) != value) { + parentElement.addContent(AbstractJDOMUtil.simpleElement(childElementName, value)); + } + } + } + } + + /** + * An implementation of an XML element encoder for Strings. + */ + public static final ElementEncoder DEFAULT_STRING_ENCODER = new StringEncoder(); + + private static class StringEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (int i = values.size() - 1; i >= 0; i--) { + String value = values.get(i); + + if (PROCESSING_ERROR_ELEMENT_NAME.equals(childElementName) && StringUtils.isNotEmpty(value)) { + value = value.substring(0, value.length() - 1); + } + + if (value != null) { + parentElement.addContent(preserve(protectedElement(childElementName, value))); + } + } + } + } + + /** + * An implementation of an XML element encoder for byte[]. + */ + public static final ElementEncoder DEFAULT_BYTE_ARRAY_ENCODER = new ByteArrayEncoder(); + + private static class ByteArrayEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (final byte[] value : values) { + if (value != null) { + parentElement.addContent(preserve(protectedElementBase64(childElementName, value))); + } + } + } + } + + /** + * An implementation of an XML element encoder for booleans. + */ + public static final ElementEncoder DEFAULT_BOOLEAN_ENCODER = new BooleanEncoder(); + + private static class BooleanEncoder implements ElementEncoder { + @Override + public void encode(final List values, final Element parentElement, final String childElementName) { + for (final boolean value : values) { + if ((Boolean) PRIMITVE_NAME_DEFAULT_MAP.get(childElementName) != value) { + parentElement.addContent(AbstractJDOMUtil.simpleElement(childElementName, value)); + } + } + } + } + + /** + * An implementation of an XML element encoder for Map<String, Collection<Object>>. + */ + public static final ElementEncoder>> DEFAULT_STRING_OBJECT_ENCODER = new StringObjectEncoder(); + + private static class StringObjectEncoder implements ElementEncoder>> { + @Override + public void encode(final List>> values, final Element parentElement, final String childElementName) { + for (final Map> value : values) { + for (final Entry> parameter : value.entrySet()) { + for (final Object item : parameter.getValue()) { + final Element metaElement = new Element(PARAMETER_ELEMENT_NAME); + + parentElement.addContent(metaElement); + metaElement.addContent(preserve(protectedElement(NAME_ELEMENT_NAME, parameter.getKey()))); + metaElement.addContent(preserve(protectedElement(VALUE_ELEMENT_NAME, item.toString()))); + } + } + } + } + } + + /** + * An implementation of an XML element encoder for Map<String, byte[]>. + */ + public static final ElementEncoder> DEFAULT_STRING_BYTE_ARRAY_ENCODER = new StringByteArrayEncoder(); + + private static class StringByteArrayEncoder implements ElementEncoder> { + @Override + public void encode(final List> values, final Element parentElement, final String childElementName) { + for (final Map value : values) { + for (final Entry view : value.entrySet()) { + final Element metaElement = new Element(VIEW_ELEMENT_NAME); + + parentElement.addContent(metaElement); + metaElement.addContent(preserve(protectedElement(NAME_ELEMENT_NAME, view.getKey()))); + metaElement.addContent(preserve(protectedElementBase64(VALUE_ELEMENT_NAME, view.getValue()))); + } + } + } + } + + /** + * The default set of XML element decoders. + */ + public static final ElementDecoders DEFAULT_ELEMENT_DECODERS = new ElementDecoders( + DEFAULT_BOOLEAN_DECODER, + DEFAULT_BYTE_ARRAY_DECODER, + DEFAULT_INTEGER_DECODER, + DEFAULT_SEEKABLE_BYTE_CHANNEL_FACTORY_DECODER, + DEFAULT_STRING_BYTE_ARRAY_DECODER, + DEFAULT_STRING_DECODER, + DEFAULT_STRING_OBJECT_DECODER); + + /** + * The default set of XML element encoders. + */ + public static final ElementEncoders DEFAULT_ELEMENT_ENCODERS = new ElementEncoders( + DEFAULT_BOOLEAN_ENCODER, + DEFAULT_BYTE_ARRAY_ENCODER, + DEFAULT_INTEGER_ENCODER, + DEFAULT_SEEKABLE_BYTE_CHANNEL_FACTORY_ENCODER, + DEFAULT_STRING_BYTE_ARRAY_ENCODER, + DEFAULT_STRING_ENCODER, + DEFAULT_STRING_OBJECT_ENCODER); + + /** + * The set of XML element encoders that will sha256 hash the specified element types. + */ + public static final ElementEncoders SHA256_ELEMENT_ENCODERS = new ElementEncoders( + DEFAULT_BOOLEAN_ENCODER, + DEFAULT_BYTE_ARRAY_ENCODER, + DEFAULT_INTEGER_ENCODER, + SHA256_SEEKABLE_BYTE_CHANNEL_FACTORY_ENCODER, + DEFAULT_STRING_BYTE_ARRAY_ENCODER, + DEFAULT_STRING_ENCODER, + DEFAULT_STRING_OBJECT_ENCODER); + + private IBaseDataObjectXmlCodecs() {} + + /** + * Return UTF8 bytes from an XML value, decoding base64 if required + * + * @param encoding e.g. 'base64', otherwise it returns the bytes as they are presented + * @param elementValue containing the data + * @return the data from elementValue, decoded from base64 if required + */ + public static byte[] extractBytes(final String encoding, final String elementValue) { + if (BASE64_ATTRIBUTE_NAME.equalsIgnoreCase(encoding)) { + final String newElementValue = elementValue.replace("\n", ""); + final byte[] bytes = newElementValue.getBytes(StandardCharsets.UTF_8); + return BASE64_DECODER.decode(bytes); + } + + return elementValue.getBytes(StandardCharsets.UTF_8); + } + + private static Element preserve(final Element element) { + element.setAttribute("space", "preserve", XML_NAMESPACE); + + return element; + } + + private static Element protectedElement(final String name, final String string) { + return protectedElementBase64(name, string.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Creates a 'protected' element which can be encoded with base64 if it contains unsafe characters + * + * See method source for specific definition of 'unsafe'. + * + * @param name of the element + * @param bytes to wrap, if they contain unsafe characters + * @return the created element + */ + private static Element protectedElementBase64(final String name, final byte[] bytes) { + final Element element = new Element(name); + + if (ByteUtil.hasNonPrintableValues(bytes)) { + String base64String = BASE64_NEW_LINE_STRING + + BASE64_ENCODER.encodeToString(bytes) + + BASE64_NEW_LINE_STRING; + + element.setAttribute(ENCODING_ATTRIBUTE_NAME, BASE64_ATTRIBUTE_NAME); + element.addContent(base64String); + } else { + element.addContent(new String(bytes, StandardCharsets.ISO_8859_1)); + } + + return element; + } + + /** + * Gets the requested method object from the IBaseDataObject class. + * + * @throws SecurityException + * @throws NoSuchMethodException + */ + private static Method getIbdoMethod(final String name, final Class... parameterTypes) + throws NoSuchMethodException, SecurityException { + return IBaseDataObject.class.getDeclaredMethod(name, parameterTypes); + } +} diff --git a/src/main/java/emissary/core/IBaseDataObjectXmlHelper.java b/src/main/java/emissary/core/IBaseDataObjectXmlHelper.java index 7b7444c924..08def8f3c8 100644 --- a/src/main/java/emissary/core/IBaseDataObjectXmlHelper.java +++ b/src/main/java/emissary/core/IBaseDataObjectXmlHelper.java @@ -1,33 +1,19 @@ package emissary.core; -import emissary.core.channels.InMemoryChannelFactory; +import emissary.core.IBaseDataObjectXmlCodecs.ElementDecoders; +import emissary.core.IBaseDataObjectXmlCodecs.ElementEncoders; import emissary.core.channels.SeekableByteChannelFactory; -import emissary.core.channels.SeekableByteChannelHelper; import emissary.kff.KffDataObjectHandler; import emissary.util.xml.AbstractJDOMUtil; import org.apache.commons.lang3.Validate; import org.jdom2.Document; import org.jdom2.Element; -import org.jdom2.Namespace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.util.AbstractMap.SimpleEntry; -import java.util.Base64; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.xml.XMLConstants; /** * This class helps convert IBaseDataObjects to and from XML. @@ -45,34 +31,6 @@ public final class IBaseDataObjectXmlHelper { * The XML element prefix for attachments. */ public static final String ATTACHMENT_ELEMENT_PREFIX = "att"; - /** - * The XML attribute name for Base64. - */ - public static final String BASE64_ATTRIBUTE_NAME = "base64"; - /** - * New line byte array to use for normalised XML - */ - private static final byte[] BASE64_NEW_LINE_BYTE = {'\n'}; - /** - * New line string to use for normalised XML - */ - private static final String BASE64_NEW_LINE_STRING = new String(BASE64_NEW_LINE_BYTE); - /** - * Max width of Base64 char block. - */ - private static final int BASE64_LINE_WIDTH = 76; - /** - * The Base64 decoder. - */ - private static final Base64.Decoder BASE64_DECODER = Base64.getMimeDecoder(); - /** - * The Base64 encoder. - * - * Uses same width as default, but overrides new line separator to use normalised XML separator. - * - * @see http://www.jdom.org/docs/apidocs/org/jdom2/output/Format.html#setLineSeparator(java.lang.String) - */ - private static final Base64.Encoder BASE64_ENCODER = Base64.getMimeEncoder(BASE64_LINE_WIDTH, BASE64_NEW_LINE_BYTE); /** * The XML element name for Birth Order. */ @@ -113,10 +71,6 @@ public final class IBaseDataObjectXmlHelper { * The IBaseDataObject set method name for Data. */ public static final String DATA_SET_METHOD_NAME = "setChannelFactory"; - /** - * The XML attribute name for Encoding. - */ - public static final String ENCODING_ATTRIBUTE_NAME = "encoding"; /** * The XML element prefix for Extracted Records. */ @@ -169,10 +123,6 @@ public final class IBaseDataObjectXmlHelper { * The IBaseDataObject set method name for Id. */ public static final String ID_SET_METHOD_NAME = "setId"; - /** - * The XML element name for Name. - */ - public static final String NAME_ELEMENT_NAME = "name"; /** * The XML element name for Num Siblings. */ @@ -257,233 +207,9 @@ public final class IBaseDataObjectXmlHelper { * The XML element name for Setup. */ public static final String SETUP_ELEMENT_NAME = "setup"; - /** - * The XML namespace for "xml". - */ - public static final Namespace XML_NAMESPACE = Namespace.getNamespace(XMLConstants.XML_NS_PREFIX, - XMLConstants.XML_NS_URI); - /** - * A map of element names of IBaseDataObject methods that get/set primitives and their default values. - */ - public static final Map PRIMITVE_NAME_DEFAULT_MAP = Collections - .unmodifiableMap(new ConcurrentHashMap<>(Stream.of( - new SimpleEntry<>(BIRTH_ORDER_ELEMENT_NAME, new BaseDataObject().getBirthOrder()), - new SimpleEntry<>(BROKEN_ELEMENT_NAME, new BaseDataObject().isBroken()), - new SimpleEntry<>(NUM_CHILDREN_ELEMENT_NAME, new BaseDataObject().getNumChildren()), - new SimpleEntry<>(NUM_SIBLINGS_ELEMENT_NAME, new BaseDataObject().getNumSiblings()), - new SimpleEntry<>(OUTPUTABLE_ELEMENT_NAME, new BaseDataObject().isOutputable()), - new SimpleEntry<>(PRIORITY_ELEMENT_NAME, new BaseDataObject().getPriority())) - .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))); - - /** - * Interface for decoding an element value. - */ - private interface ElementDecoder { - /** - * Decodes an XML element. - * - * @param element to decode. - * @return the decoded element value. - */ - Object decode(Element element); - - /** - * Returns the class of the key for a mapped value or null for a non-mapped value. - * - * @return the class of the key for a mapped value or null for a non-mapped value. - */ - Class getKeyClass(); - - /** - * Returns the class of the value, whether mapped or non-mapped. - * - * @return the class of the value, whether mapped or non-mapped. - */ - Class getValueClass(); - } - - /** - * Implementation of an XML element decoder that has a boolean value. - */ - private static ElementDecoder booleanDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - return Boolean.valueOf(element.getValue()); - } - - @Override - public Class getKeyClass() { - return null; - } - - @Override - public Class getValueClass() { - return boolean.class; - } - }; - - /** - * Implementation of an XML element decoder that has a SeekableByteChannel value. - */ - private static ElementDecoder seekableByteChannelFactoryDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - final String elementValue = element.getValue(); - final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - - return InMemoryChannelFactory.create(extractBytes(encoding, elementValue)); - } - - @Override - public Class getKeyClass() { - return null; - } - - @Override - public Class getValueClass() { - return SeekableByteChannelFactory.class; - } - }; - - /** - * Implementation of an XML element decoder that has a byte array value. - */ - private static ElementDecoder byteArrayDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - final String elementValue = element.getValue(); - final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - - return extractBytes(encoding, elementValue); - } - - @Override - public Class getKeyClass() { - return null; - } - - @Override - public Class getValueClass() { - return byte[].class; - } - }; - - /** - * Implementation of an XML element decoder that has an integer value. - */ - private static ElementDecoder integerDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - try { - return Integer.decode(element.getValue()); - } catch (final NumberFormatException e) { - return null; - } - } - - @Override - public Class getKeyClass() { - return null; - } - - @Override - public Class getValueClass() { - return int.class; - } - }; - - /** - * Implementation of an XML element decoder that has a string value. - */ - private static ElementDecoder stringDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - final String elementValue = element.getValue(); - final String encoding = element.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - - return new String(extractBytes(encoding, elementValue), StandardCharsets.UTF_8); - } - - @Override - public Class getKeyClass() { - return null; - } - - @Override - public Class getValueClass() { - return String.class; - } - }; - - /** - * Implementation of an XML element decoder that has a mapped value where the key is a string and the value is a byte - * array. - */ - private static ElementDecoder stringByteArrayDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - final Element childElement = element.getChild(VALUE_ELEMENT_NAME); - final String elementValue = childElement.getValue(); - final String encoding = childElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - - return extractBytes(encoding, elementValue); - } - - @Override - public Class getKeyClass() { - return String.class; - } - - @Override - public Class getValueClass() { - return byte[].class; - } - }; - - /** - * Implementation of an XML element decoder that has a mapped value where the key is a string and the value is an - * object. - */ - private static ElementDecoder stringObjectDecoder = new ElementDecoder() { - @Override - public Object decode(final Element element) { - final Element childElement = element.getChild(VALUE_ELEMENT_NAME); - final String elementValue = childElement.getValue(); - final String encoding = childElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - - return new String(extractBytes(encoding, elementValue), StandardCharsets.UTF_8); - } - - @Override - public Class getKeyClass() { - return String.class; - } - - @Override - public Class getValueClass() { - return Object.class; - } - }; private IBaseDataObjectXmlHelper() {} - /** - * Return UTF8 bytes from an XML value, decoding base64 if required - * - * @param encoding e.g. 'base64', otherwise it returns the bytes as they are presented - * @param elementValue containing the data - * @return the data from elementValue, decoded from base64 if required - */ - private static byte[] extractBytes(final String encoding, final String elementValue) { - if (BASE64_ATTRIBUTE_NAME.equalsIgnoreCase(encoding)) { - final String newElementValue = elementValue.replace("\n", ""); - final byte[] bytes = newElementValue.getBytes(StandardCharsets.UTF_8); - return BASE64_DECODER.decode(bytes); - } else { - return elementValue.getBytes(StandardCharsets.UTF_8); - } - } - /** * Setup a typical BDO * @@ -518,27 +244,29 @@ public static IBaseDataObject createStandardInitialIbdo(final SeekableByteChanne * * @param document containing the IBaseDataObject and children descriptions. * @param children the list where the children will be added. + * @param decoders used to decode XML into ibdo. * @return the IBaseDataObject. */ - public static IBaseDataObject ibdoFromXml(final Document document, final List children) { + public static IBaseDataObject ibdoFromXml(final Document document, final List children, final ElementDecoders decoders) { Validate.notNull(document, "Required document != null!"); Validate.notNull(children, "Required children != null!"); + Validate.notNull(decoders, "Required: decoders not null!"); final Element root = document.getRootElement(); final Element answersElement = root.getChild(ANSWERS_ELEMENT_NAME); final IBaseDataObject parentIbdo = new BaseDataObject(); final List answerChildren = answersElement.getChildren(); - ibdoFromXmlMainElements(answersElement, parentIbdo); + ibdoFromXmlMainElements(answersElement, parentIbdo, decoders); for (final Element answerChild : answerChildren) { final IBaseDataObject childIbdo = new BaseDataObject(); final String childName = answerChild.getName(); if (childName.startsWith(EXTRACTED_RECORD_ELEMENT_PREFIX)) { - parentIbdo.addExtractedRecord(ibdoFromXmlMainElements(answerChild, childIbdo)); + parentIbdo.addExtractedRecord(ibdoFromXmlMainElements(answerChild, childIbdo, decoders)); } else if (childName.startsWith(ATTACHMENT_ELEMENT_PREFIX)) { - children.add(ibdoFromXmlMainElements(answerChild, childIbdo)); + children.add(ibdoFromXmlMainElements(answerChild, childIbdo, decoders)); } } @@ -550,124 +278,71 @@ public static IBaseDataObject ibdoFromXml(final Document document, final List keyClass, final Class valueClass, - final IBaseDataObject ibdo, final String ibdoMethodName, final Object parameter, final Element element) { - try { - if (keyClass == null) { - final Method method = IBaseDataObject.class.getDeclaredMethod(ibdoMethodName, valueClass); - - method.invoke(ibdo, parameter); - } else { - final String name = (String) stringDecoder.decode(element.getChild(NAME_ELEMENT_NAME)); - final Method method = IBaseDataObject.class.getDeclaredMethod(ibdoMethodName, keyClass, valueClass); - - method.invoke(ibdo, name, parameter); - } - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - LOGGER.warn("Unable to call ibdo method {}!", ibdoMethodName, e); - } - } - /** * Creates an XML string from a parent IBaseDataObject and a list of children IBaseDataObjects. * * @param parent the parent IBaseDataObject * @param children the children IBaseDataObjects. * @param initialIbdo the initial IBaseDataObject. + * @param encoders used to encode ibdo into XML. * @return the XML string. */ public static String xmlFromIbdo(final IBaseDataObject parent, final List children, - final IBaseDataObject initialIbdo) { + final IBaseDataObject initialIbdo, final ElementEncoders encoders) { Validate.notNull(parent, "Required: parent != null!"); Validate.notNull(children, "Required: children != null!"); Validate.notNull(initialIbdo, "Required: initialIbdo != null!"); + Validate.notNull(encoders, "Required: encoders not null!"); final Element rootElement = new Element(RESULT_ELEMENT_NAME); final Element setupElement = new Element(SETUP_ELEMENT_NAME); rootElement.addContent(setupElement); - xmlFromIbdoMainElements(initialIbdo, setupElement); + xmlFromIbdoMainElements(initialIbdo, setupElement, encoders); final Element answersElement = new Element(ANSWERS_ELEMENT_NAME); rootElement.addContent(answersElement); - xmlFromIbdoMainElements(parent, answersElement); + xmlFromIbdoMainElements(parent, answersElement, encoders); final List extractedRecords = parent.getExtractedRecords(); if (extractedRecords != null) { @@ -675,7 +350,7 @@ public static String xmlFromIbdo(final IBaseDataObject parent, final List> parameter : ibdo.getParameters().entrySet()) { - for (final Object item : parameter.getValue()) { - final Element metaElement = new Element(PARAMETER_ELEMENT_NAME); - - element.addContent(metaElement); - metaElement.addContent(preserve(protectedElement(NAME_ELEMENT_NAME, parameter.getKey()))); - metaElement.addContent(preserve(protectedElement(VALUE_ELEMENT_NAME, item.toString()))); - } - } - - for (final Entry view : ibdo.getAlternateViews().entrySet()) { - final Element metaElement = new Element(VIEW_ELEMENT_NAME); - - element.addContent(metaElement); - metaElement.addContent(preserve(protectedElement(NAME_ELEMENT_NAME, view.getKey()))); - metaElement.addContent(preserve(protectedElement(VALUE_ELEMENT_NAME, view.getValue()))); - } - } - - private static void addNonNullContent(final Element parent, final String elementName, final String string) { - if (string != null) { - parent.addContent(preserve(protectedElement(elementName, string))); - } - } - - private static void addNonNullContent(final Element parent, final String elementName, final byte[] bytes) { - if (bytes != null) { - parent.addContent(preserve(protectedElement(elementName, bytes))); - } - } - - private static void addNonNullContent(final Element parent, final String elementName, - final SeekableByteChannelFactory seekableByteChannelFactory) { - if (seekableByteChannelFactory != null) { - try { - final byte[] bytes = SeekableByteChannelHelper.getByteArrayFromChannel(seekableByteChannelFactory, - BaseDataObject.MAX_BYTE_ARRAY_SIZE); - - addNonNullContent(parent, elementName, bytes); - } catch (final IOException e) { - LOGGER.error("Could not get bytes from SeekableByteChannel!", e); - } - } - } - - private static void addNonDefaultContent(final Element parent, final String elementName, final boolean bool) { - if (((Boolean) PRIMITVE_NAME_DEFAULT_MAP.get(elementName)).booleanValue() != bool) { - parent.addContent(AbstractJDOMUtil.simpleElement(elementName, bool)); - } - } - - private static void addNonDefaultContent(final Element parent, final String elementName, final int integer) { - if (((Integer) PRIMITVE_NAME_DEFAULT_MAP.get(elementName)).intValue() != integer) { - parent.addContent(AbstractJDOMUtil.simpleElement(elementName, integer)); - } - } - - private static Element preserve(final Element element) { - element.setAttribute("space", "preserve", XML_NAMESPACE); - - return element; - } - - private static Element protectedElement(final String name, final String string) { - return protectedElement(name, string.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Creates a 'protected' element which can be encoded with base64 if it contains unsafe characters - * - * See method source for specific definition of 'unsafe'. - * - * @param name of the element - * @param bytes to wrap, if they contain unsafe characters - * @return the created element - */ - private static Element protectedElement(final String name, final byte[] bytes) { - final Element element = new Element(name); - - boolean badCharacters = false; - for (int i = 0; i < bytes.length; i++) { - if (bytes[i] < 9 || bytes[i] > 13 && bytes[i] < 32) { - badCharacters = true; - break; - } - } - if (badCharacters) { - final StringBuilder base64String = new StringBuilder(); - base64String.append(BASE64_NEW_LINE_STRING); - base64String.append(BASE64_ENCODER.encodeToString(bytes)); - base64String.append(BASE64_NEW_LINE_STRING); - - element.setAttribute(ENCODING_ATTRIBUTE_NAME, BASE64_ATTRIBUTE_NAME); - element.addContent(base64String.toString()); - } else { - element.addContent(new String(bytes, StandardCharsets.ISO_8859_1)); - } - - return element; + * @param encoders used to encode ibdo into XML. + */ + public static void xmlFromIbdoMainElements(final IBaseDataObject ibdo, final Element element, final ElementEncoders encoders) { + Validate.notNull(ibdo, "Required: ibdo not null!"); + Validate.notNull(element, "Required: element not null!"); + Validate.notNull(encoders, "Required: encoders not null!"); + + encoders.seekableByteChannelFactoryEncoder.encode(Collections.singletonList(ibdo.getChannelFactory()), element, DATA_ELEMENT_NAME); + encoders.integerEncoder.encode(Collections.singletonList(ibdo.getBirthOrder()), element, BIRTH_ORDER_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getBroken()), element, BROKEN_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getClassification()), element, CLASSIFICATION_ELEMENT_NAME); + encoders.stringEncoder.encode(ibdo.getAllCurrentForms(), element, CURRENT_FORM_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getFilename()), element, FILENAME_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getFontEncoding()), element, FONT_ENCODING_ELEMENT_NAME); + encoders.byteArrayEncoder.encode(Collections.singletonList(ibdo.footer()), element, FOOTER_ELEMENT_NAME); + encoders.byteArrayEncoder.encode(Collections.singletonList(ibdo.header()), element, HEADER_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getHeaderEncoding()), element, HEADER_ENCODING_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getId()), element, ID_ELEMENT_NAME); + encoders.integerEncoder.encode(Collections.singletonList(ibdo.getNumChildren()), element, NUM_CHILDREN_ELEMENT_NAME); + encoders.integerEncoder.encode(Collections.singletonList(ibdo.getNumSiblings()), element, NUM_SIBLINGS_ELEMENT_NAME); + encoders.booleanEncoder.encode(Collections.singletonList(ibdo.isOutputable()), element, OUTPUTABLE_ELEMENT_NAME); + encoders.integerEncoder.encode(Collections.singletonList(ibdo.getPriority()), element, PRIORITY_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getProcessingError()), element, PROCESSING_ERROR_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getTransactionId()), element, TRANSACTION_ID_ELEMENT_NAME); + encoders.stringEncoder.encode(Collections.singletonList(ibdo.getWorkBundleId()), element, WORK_BUNDLE_ID_ELEMENT_NAME); + encoders.stringObjectEncoder.encode(Collections.singletonList(ibdo.getParameters()), element, PARAMETER_ELEMENT_NAME); + encoders.stringByteArrayEncoder.encode(Collections.singletonList(ibdo.getAlternateViews()), element, VIEW_ELEMENT_NAME); } } diff --git a/src/main/java/emissary/util/ByteUtil.java b/src/main/java/emissary/util/ByteUtil.java index 88a592d83b..81c98d3c57 100755 --- a/src/main/java/emissary/util/ByteUtil.java +++ b/src/main/java/emissary/util/ByteUtil.java @@ -1,5 +1,7 @@ package emissary.util; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; @@ -273,6 +275,51 @@ public static String grabLine(byte[] data, int pos) { return ret; } + /** + * Scans a byte array looking for non-printable values. + * + * @param bytes the bytes to be scanned. + * @return whether or not there were non-printable values. + */ + public static boolean hasNonPrintableValues(final byte[] bytes) { + boolean badCharacters = false; + + for (byte aByte : bytes) { + if (aByte < 9 || aByte > 13 && aByte < 32) { + badCharacters = true; + break; + } + } + + return badCharacters; + } + + /** + * Creates a hex string of a sha256 hash for a byte[]. + * + * @param bytes to be hashed + * @return the hex string of a sha256 hash of the bytes. + */ + public static String sha256Bytes(final byte[] bytes) { + try { + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + final byte[] hash = md.digest(bytes); + + final StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + final String hex = Integer.toHexString(0xff & b); + + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + /** This class is not meant to be instantiated. */ private ByteUtil() {} } diff --git a/src/test/java/emissary/core/IBaseDataObjectHelperTest.java b/src/test/java/emissary/core/IBaseDataObjectHelperTest.java index d4cc8b05ec..132bdaca7d 100644 --- a/src/test/java/emissary/core/IBaseDataObjectHelperTest.java +++ b/src/test/java/emissary/core/IBaseDataObjectHelperTest.java @@ -3,6 +3,7 @@ import emissary.core.channels.InMemoryChannelFactory; import emissary.kff.KffDataObjectHandler; import emissary.parser.SessionParser; +import emissary.test.core.junit5.UnitTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -class IBaseDataObjectHelperTest { +class IBaseDataObjectHelperTest extends UnitTest { private IBaseDataObject ibdo1; private IBaseDataObject ibdo2; @@ -54,11 +55,11 @@ void setup() { ibdo2 = new BaseDataObject(); } - private void verifyClone(final String methodName, final IBaseDataObject origObj, final Boolean isSame, final Boolean isEquals, + private static void verifyClone(final String methodName, final IBaseDataObject origObj, final Boolean isSame, final Boolean isEquals, final Boolean switchWithFullClone) { try { final Method method = IBaseDataObject.class.getMethod(methodName); - final boolean isArrayType = method.getReturnType().getName().equals("[B"); + final boolean isArrayType = "[B".equals(method.getReturnType().getName()); final IBaseDataObject cloneFalseObj = IBaseDataObjectHelper.clone(origObj, false); final IBaseDataObject cloneTrueObj = IBaseDataObjectHelper.clone(origObj, true); verifyCloneAssertions(method, origObj, cloneFalseObj, isSame, isEquals); @@ -76,8 +77,8 @@ private void verifyClone(final String methodName, final IBaseDataObject origObj, } } - private void verifyCloneAssertions(final Method method, final Object obj1, final Object obj2, final Boolean isSame, final Boolean isEquals) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + private static void verifyCloneAssertions(final Method method, final Object obj1, final Object obj2, final Boolean isSame, final Boolean isEquals) + throws IllegalAccessException, InvocationTargetException { final Object o1 = method.invoke(obj1); final Object o2 = method.invoke(obj2); @@ -91,7 +92,7 @@ private void verifyCloneAssertions(final Method method, final Object obj1, final if (isEquals != null) { if (isEquals) { - if (method.getReturnType().getName().equals("[B")) { + if ("[B".equals(method.getReturnType().getName())) { assertArrayEquals((byte[]) o1, (byte[]) o2); } else { assertEquals(o1, o2); @@ -181,7 +182,7 @@ void testCloneFilename() { void testCloneInternalId() { verifyClone("getInternalId", ibdo1, IS_NOT_SAME, IS_NOT_EQUALS, EQUAL_AFTER_FULL_CLONE); // Now assert that if an exception occurs, the IDs will differ - try (final MockedStatic helper = Mockito.mockStatic(IBaseDataObjectHelper.class, Mockito.CALLS_REAL_METHODS)) { + try (MockedStatic helper = Mockito.mockStatic(IBaseDataObjectHelper.class, Mockito.CALLS_REAL_METHODS)) { helper.when(() -> IBaseDataObjectHelper.setPrivateFieldValue(any(), any(), any())) .thenThrow(IllegalAccessException.class); @@ -275,7 +276,7 @@ void testCloneTransactionId() { verifyClone("getTransactionId", ibdo1, DONT_CHECK, IS_NOT_EQUALS, EQUAL_AFTER_FULL_CLONE); } - private void checkThrowsNull(final Executable e) { + private static void checkThrowsNull(final Executable e) { assertThrows(NullPointerException.class, e); } diff --git a/src/test/java/emissary/core/IBaseDataObjectXmlHelperTest.java b/src/test/java/emissary/core/IBaseDataObjectXmlHelperTest.java index d636d5c4c6..4356b046ef 100644 --- a/src/test/java/emissary/core/IBaseDataObjectXmlHelperTest.java +++ b/src/test/java/emissary/core/IBaseDataObjectXmlHelperTest.java @@ -1,20 +1,20 @@ package emissary.core; +import emissary.core.IBaseDataObjectXmlCodecs.ElementEncoders; import emissary.core.channels.InMemoryChannelFactory; import emissary.core.channels.SeekableByteChannelFactory; import emissary.kff.KffDataObjectHandler; import emissary.test.core.junit5.UnitTest; +import emissary.util.ByteUtil; import emissary.util.PlaceComparisonHelper; import org.jdom2.Document; -import org.jdom2.Element; import org.jdom2.input.SAXBuilder; import org.jdom2.input.sax.XMLReaders; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringReader; -import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; @@ -22,6 +22,9 @@ import java.util.List; import java.util.Random; +import static emissary.core.IBaseDataObjectXmlCodecs.DEFAULT_ELEMENT_DECODERS; +import static emissary.core.IBaseDataObjectXmlCodecs.DEFAULT_ELEMENT_ENCODERS; +import static emissary.core.IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -34,11 +37,18 @@ void testParentIbdoNoFieldsChanged() throws Exception { final List actualChildren = new ArrayList<>(); final IBaseDataObject actualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, - actualChildren); + actualChildren, DEFAULT_ELEMENT_ENCODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, actualChildren, "testParentIbdoNoFieldsChanged", DiffCheckConfiguration.onlyCheckData()); assertNull(diff); + + final IBaseDataObject sha256ActualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, + actualChildren, SHA256_ELEMENT_ENCODERS); + final String sha256Diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, sha256ActualIbdo, expectedChildren, + actualChildren, "testParentIbdoNoFieldsChangedSha256", DiffCheckConfiguration.onlyCheckData()); + + assertNull(sha256Diff); } @Test @@ -76,11 +86,18 @@ void testParentIbdoAllFieldsChanged() throws Exception { expectedIbdo.addAlternateView("AlternateView2Key", "AlternateView2Value".getBytes(StandardCharsets.ISO_8859_1)); final IBaseDataObject actualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, - actualChildren); + actualChildren, DEFAULT_ELEMENT_ENCODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, actualChildren, "testParentIbdoAllFieldsChanged", DiffCheckConfiguration.onlyCheckData()); assertNull(diff); + + final IBaseDataObject sha256ActualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, + actualChildren, SHA256_ELEMENT_ENCODERS); + final String sha256Diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, sha256ActualIbdo, expectedChildren, + actualChildren, "testParentIbdoAllFieldsChangedSha256", DiffCheckConfiguration.onlyCheckData()); + + assertNull(sha256Diff); } @Test @@ -89,8 +106,9 @@ void testBase64Conversion() throws Exception { final IBaseDataObject expectedIbdo = new BaseDataObject(); final List expectedChildren = new ArrayList<>(); final List actualChildren = new ArrayList<>(); + final byte[] bytes = "\001Data".getBytes(StandardCharsets.ISO_8859_1); - expectedIbdo.setData("\001Data".getBytes(StandardCharsets.ISO_8859_1)); + expectedIbdo.setData(bytes); expectedIbdo.setBirthOrder(5); expectedIbdo.setBroken("\001Broken1"); expectedIbdo.setBroken("\001Broken2"); @@ -120,11 +138,21 @@ void testBase64Conversion() throws Exception { "\200AlternateView2Value".getBytes(StandardCharsets.ISO_8859_1)); final IBaseDataObject actualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, - actualChildren); + actualChildren, DEFAULT_ELEMENT_ENCODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, actualChildren, "testBase64Conversion", DiffCheckConfiguration.onlyCheckData()); assertNull(diff); + + final IBaseDataObject sha256ActualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, + actualChildren, SHA256_ELEMENT_ENCODERS); + + expectedIbdo.setData(ByteUtil.sha256Bytes(bytes).getBytes(StandardCharsets.ISO_8859_1)); + + final String sha256Diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, sha256ActualIbdo, expectedChildren, + actualChildren, "testSha256Conversion", DiffCheckConfiguration.onlyCheckData()); + + assertNull(sha256Diff); } @Test @@ -151,9 +179,9 @@ void testAttachmentsAndExtractedRecords() throws Exception { expectedChildren.add(childIbdo2); final IBaseDataObject actualIbdo = ibdoFromXmlFromIbdo(expectedIbdo, expectedChildren, initialIbdo, - actualChildren); + actualChildren, DEFAULT_ELEMENT_ENCODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, - actualChildren, "testBase64Conversion", DiffCheckConfiguration.onlyCheckData()); + actualChildren, "testAttachmentsAndExtractedRecords", DiffCheckConfiguration.onlyCheckData()); assertNull(diff); } @@ -169,11 +197,18 @@ void testBadChannelFactory() throws Exception { dataExceptionIbdo.setChannelFactory(new ExceptionChannelFactory()); final IBaseDataObject actualIbdo = ibdoFromXmlFromIbdo(dataExceptionIbdo, expectedChildren, initialIbdo, - actualChildren); + actualChildren, DEFAULT_ELEMENT_ENCODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, actualChildren, "testBadChannelFactory", DiffCheckConfiguration.onlyCheckData()); assertNull(diff); + + final IBaseDataObject sha256ActualIbdo = ibdoFromXmlFromIbdo(dataExceptionIbdo, expectedChildren, initialIbdo, + actualChildren, SHA256_ELEMENT_ENCODERS); + final String sha256Diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, sha256ActualIbdo, expectedChildren, + actualChildren, "testBadChannelFactory", DiffCheckConfiguration.onlyCheckData()); + + assertNull(sha256Diff); } @Test @@ -186,12 +221,12 @@ void testBadIntegerDecoding() throws Exception { priorityIbdo.setPriority(100); - final String xmlString = IBaseDataObjectXmlHelper.xmlFromIbdo(priorityIbdo, expectedChildren, initialIbdo); + final String xmlString = IBaseDataObjectXmlHelper.xmlFromIbdo(priorityIbdo, expectedChildren, initialIbdo, DEFAULT_ELEMENT_ENCODERS); final String newXmlString = xmlString.replace("100", "100A"); final SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); final Document document = builder.build(new StringReader(newXmlString)); - final IBaseDataObject actualIbdo = IBaseDataObjectXmlHelper.ibdoFromXml(document, actualChildren); + final IBaseDataObject actualIbdo = IBaseDataObjectXmlHelper.ibdoFromXml(document, actualChildren, DEFAULT_ELEMENT_DECODERS); final String diff = PlaceComparisonHelper.checkDifferences(expectedIbdo, actualIbdo, expectedChildren, actualChildren, "testBadChannelFactory", DiffCheckConfiguration.onlyCheckData()); @@ -199,29 +234,6 @@ void testBadIntegerDecoding() throws Exception { assertNull(diff); } - @Test - void testSetParameterOnIbdoException() throws Exception { - final Class keyClass = null; - final Class valueClass = int.class; - final IBaseDataObject ibdo = new BaseDataObject(); - final String ibdoMethodName = "methodNotInIbdo"; - final Object parameter = null; - final Element element = null; - final List differences = new ArrayList<>(); - final Method method = IBaseDataObjectXmlHelper.class.getDeclaredMethod("setParameterOnIbdo", Class.class, - Class.class, IBaseDataObject.class, String.class, Object.class, Element.class); - - method.setAccessible(true); - - method.invoke(IBaseDataObjectXmlHelper.class, keyClass, valueClass, ibdo, ibdoMethodName, parameter, element); - - method.setAccessible(false); - - IBaseDataObjectDiffHelper.diff(new BaseDataObject(), ibdo, differences, DiffCheckConfiguration.onlyCheckData()); - - assertEquals(0, differences.size()); - } - @Test void testCreateStandardInitialIbdo() { final byte[] bytes = new byte[20]; @@ -254,14 +266,13 @@ void testCreateStandardInitialIbdo() { } private static IBaseDataObject ibdoFromXmlFromIbdo(final IBaseDataObject ibdo, final List children, - final IBaseDataObject initialIbdo, final List outputChildren) throws Exception { - final String xmlString = IBaseDataObjectXmlHelper.xmlFromIbdo(ibdo, children, initialIbdo); + final IBaseDataObject initialIbdo, final List outputChildren, final ElementEncoders elementEncoders) throws Exception { + final String xmlString = IBaseDataObjectXmlHelper.xmlFromIbdo(ibdo, children, initialIbdo, elementEncoders); final SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); final Document document = builder.build(new StringReader(xmlString)); - final IBaseDataObject ibdo2 = IBaseDataObjectXmlHelper.ibdoFromXml(document, outputChildren); - return ibdo2; + return IBaseDataObjectXmlHelper.ibdoFromXml(document, outputChildren, DEFAULT_ELEMENT_DECODERS); } private static class ExceptionChannelFactory implements SeekableByteChannelFactory { @@ -279,12 +290,12 @@ public void close() throws IOException { } @Override - public int read(ByteBuffer dst) throws IOException { + public int read(final ByteBuffer dst) throws IOException { throw new IOException("This SBC only throws Exceptions"); } @Override - public int write(ByteBuffer src) throws IOException { + public int write(final ByteBuffer src) throws IOException { throw new IOException("This SBC only throws Exceptions"); } @@ -294,7 +305,7 @@ public long position() throws IOException { } @Override - public SeekableByteChannel position(long newPosition) throws IOException { + public SeekableByteChannel position(final long newPosition) throws IOException { throw new IOException("This SBC only throws Exceptions"); } @@ -304,10 +315,10 @@ public long size() throws IOException { } @Override - public SeekableByteChannel truncate(long size) throws IOException { + public SeekableByteChannel truncate(final long size) throws IOException { throw new IOException("This SBC only throws Exceptions"); } }; } - }; + } } diff --git a/src/test/java/emissary/test/core/junit5/RegressionTest.java b/src/test/java/emissary/test/core/junit5/RegressionTest.java index c95f509ac0..d164a3ced5 100644 --- a/src/test/java/emissary/test/core/junit5/RegressionTest.java +++ b/src/test/java/emissary/test/core/junit5/RegressionTest.java @@ -2,13 +2,17 @@ import emissary.core.IBaseDataObject; import emissary.core.IBaseDataObjectHelper; +import emissary.core.IBaseDataObjectXmlCodecs; +import emissary.core.IBaseDataObjectXmlCodecs.ElementDecoders; +import emissary.core.IBaseDataObjectXmlCodecs.ElementEncoders; +import emissary.util.ByteUtil; import com.google.errorprone.annotations.ForOverride; -import org.jdom2.DataConversionException; import org.jdom2.Document; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.fail; @@ -107,6 +111,71 @@ protected String getInitialForm(final String resource) { return RegressionTestUtil.getInitialFormFromFilename(resource); } + /** + * This method returns the XML element decoders. + * + * @return the XML element decoders. + */ + protected ElementDecoders getDecoders() { + return IBaseDataObjectXmlCodecs.DEFAULT_ELEMENT_DECODERS; + } + + /** + * This method returns the XML element encoders. + * + * @return the XML element encoders. + */ + protected ElementEncoders getEncoders() { + return IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS; + } + + /** + * When the data is able to be retrieved from the XML (e.g. when getEncoders() returns the default encoders), then this + * method should be empty. However, in this case getEncoders() is returning the sha256 encoders which means the original + * data cannot be retrieved from the XML. Therefore, in order to test equivalence, all of the non-printable data in the + * IBaseDataObjects needs to be converted to a sha256 hash. The full encoders can be used by overriding the + * checkAnswersPreHook(...) to be empty and overriding getEncoders() to return the DEFAULT_ELEMENT_ENCODERS. + */ + @Override + protected void checkAnswersPreHook(final Document answers, final IBaseDataObject payload, final List attachments, + final String tname) { + if (!IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS.equals(getEncoders())) { + return; + } + + if (payload.data() != null && ByteUtil.hasNonPrintableValues(payload.data())) { + final String hash = ByteUtil.sha256Bytes(payload.data()); + + if (hash != null) { + payload.setData(hash.getBytes(StandardCharsets.UTF_8)); + } + } + + if (payload.getExtractedRecords() != null) { + for (final IBaseDataObject extractedRecord : payload.getExtractedRecords()) { + if (ByteUtil.hasNonPrintableValues(extractedRecord.data())) { + final String hash = ByteUtil.sha256Bytes(extractedRecord.data()); + + if (hash != null) { + extractedRecord.setData(hash.getBytes(StandardCharsets.UTF_8)); + } + } + } + } + + if (attachments != null) { + for (final IBaseDataObject attachment : attachments) { + if (ByteUtil.hasNonPrintableValues(attachment.data())) { + final String hash = ByteUtil.sha256Bytes(attachment.data()); + + if (hash != null) { + attachment.setData(hash.getBytes(StandardCharsets.UTF_8)); + } + } + } + } + } + // Everything above can be overridden by extending classes to modify behaviour as they see fit. // Below this point, methods should not be able to be overridden as they are inherently part of RegressionTest. @@ -151,7 +220,7 @@ private void generateAnswerFiles(final String resource) throws Exception { tweakFinalResultsBeforeSerialisation(resource, finalResults); // Generate the full XML (setup & answers from before & after) - RegressionTestUtil.writeAnswerXml(resource, initialIbdo, finalIbdo, finalResults); + RegressionTestUtil.writeAnswerXml(resource, initialIbdo, finalIbdo, finalResults, getEncoders()); } @Override @@ -162,13 +231,12 @@ protected final Document getAnswerDocumentFor(final String resource) { @Override protected final void setupPayload(final IBaseDataObject payload, final Document answers) { - RegressionTestUtil.setupPayload(payload, answers); + RegressionTestUtil.setupPayload(payload, answers, getDecoders()); } @Override protected final void checkAnswers(final Document answers, final IBaseDataObject payload, - final List attachments, final String tname) throws DataConversionException { - RegressionTestUtil.checkAnswers(answers, payload, attachments, tname, place.getClass().getName()); + final List attachments, final String tname) { + RegressionTestUtil.checkAnswers(answers, payload, attachments, place.getClass().getName(), getDecoders()); } - } diff --git a/src/test/java/emissary/test/core/junit5/RegressionTestUtil.java b/src/test/java/emissary/test/core/junit5/RegressionTestUtil.java index 55bd243597..ccee875e69 100644 --- a/src/test/java/emissary/test/core/junit5/RegressionTestUtil.java +++ b/src/test/java/emissary/test/core/junit5/RegressionTestUtil.java @@ -4,6 +4,8 @@ import emissary.core.DiffCheckConfiguration; import emissary.core.IBaseDataObject; import emissary.core.IBaseDataObjectHelper; +import emissary.core.IBaseDataObjectXmlCodecs.ElementDecoders; +import emissary.core.IBaseDataObjectXmlCodecs.ElementEncoders; import emissary.core.IBaseDataObjectXmlHelper; import emissary.core.channels.FileChannelFactory; import emissary.core.channels.SeekableByteChannelFactory; @@ -12,7 +14,6 @@ import emissary.util.io.ResourceReader; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; -import org.jdom2.DataConversionException; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; @@ -37,23 +38,37 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; -public class RegressionTestUtil { - - // XML builder to read XML answer file in - private static final SAXBuilder xmlBuilder = new SAXBuilder(XMLReaders.NONVALIDATING); +/** + * This class contains utility methods used by RegressionTest. + */ +public final class RegressionTestUtil { + /** + * XML builder to read XML answer file in + */ + private static final SAXBuilder XML_BUILDER = new SAXBuilder(XMLReaders.NONVALIDATING); - // Default configuration to only check data when comparing + /** + * Default configuration to only check data when comparing + */ private static final DiffCheckConfiguration DIFF_CHECK = DiffCheckConfiguration.onlyCheckData(); - // Logger instance - private static final Logger logger = LoggerFactory.getLogger(RegressionTestUtil.class); + /** + * Logger instance + */ + private static final Logger LOGGER = LoggerFactory.getLogger(RegressionTestUtil.class); - // Open options for (over-)writing answers XML + /** + * Open options for (over-)writing answers XML + */ private static final Set CREATE_WRITE_TRUNCATE = new HashSet<>(Arrays.asList(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)); - // test/resources folder - private static Path TEST_RESX = getTestResx(); + /** + * test/resources folder + */ + private static final Path TEST_RESX = getTestResx(); + + private RegressionTestUtil() {} /** * Dynamically finds the src/test/resources directory to write the XML to. @@ -79,14 +94,14 @@ public static Path getTestResx() { * @see ExtractionTest#checkAnswers(Document, IBaseDataObject, List, String) */ public static void checkAnswers(final Document answers, final IBaseDataObject payload, - final List attachments, final String tname, final String placeName) throws DataConversionException { + final List attachments, final String placeName, final ElementDecoders decoders) { final Element root = answers.getRootElement(); final Element parent = root.getChild(IBaseDataObjectXmlHelper.ANSWERS_ELEMENT_NAME); assertNotNull(parent, "No 'answers' section found!"); final List expectedAttachments = new ArrayList<>(); - final IBaseDataObject expectedIbdo = IBaseDataObjectXmlHelper.ibdoFromXml(answers, expectedAttachments); + final IBaseDataObject expectedIbdo = IBaseDataObjectXmlHelper.ibdoFromXml(answers, expectedAttachments, decoders); final String differences = PlaceComparisonHelper.checkDifferences(expectedIbdo, payload, expectedAttachments, attachments, placeName, DIFF_CHECK); @@ -103,7 +118,9 @@ public static void checkAnswers(final Document answers, final IBaseDataObject pa */ public static Document getAnswerDocumentFor(final String resource) { try { - return xmlBuilder.build(RegressionTestUtil.getXmlPath(resource).toFile()); + final Path path = RegressionTestUtil.getXmlPath(resource); + + return path == null ? null : XML_BUILDER.build(path.toFile()); } catch (final JDOMException | IOException e) { // Fail if invalid XML document fail(String.format("No valid answer document provided for %s", resource), e); @@ -120,7 +137,7 @@ public static Document getAnswerDocumentFor(final String resource) { public static Path getXmlPath(final String resource) { final int datPos = resource.lastIndexOf(ResourceReader.DATA_SUFFIX); if (datPos == -1) { - logger.debug("Resource is not a DATA file {}", resource); + LOGGER.debug("Resource is not a DATA file {}", resource); return null; } @@ -134,12 +151,13 @@ public static Path getXmlPath(final String resource) { * @param resource referencing the DAT file * @param initialIbdo for 'setup' section * @param finalIbdo for 'answers' section + * @param encoders for encoding ibdo into XML * @param results for 'answers' section */ public static void writeAnswerXml(final String resource, final IBaseDataObject initialIbdo, final IBaseDataObject finalIbdo, - final List results) { + final List results, final ElementEncoders encoders) { // Generate the full XML (setup & answers from before & after) - final String xmlContent = IBaseDataObjectXmlHelper.xmlFromIbdo(finalIbdo, results, initialIbdo); + final String xmlContent = IBaseDataObjectXmlHelper.xmlFromIbdo(finalIbdo, results, initialIbdo, encoders); // Write out the XML to disk writeXml(resource, xmlContent); } @@ -152,7 +170,10 @@ public static void writeAnswerXml(final String resource, final IBaseDataObject i */ public static void writeXml(final String resource, final String xmlContent) { final Path path = getXmlPath(resource); - logger.info("Writing answers file to path: {}", path.toString()); + if (path == null) { + fail(String.format("Could not get path for resource = %s", resource)); + } + LOGGER.info("Writing answers file to path: {}", path); try (FileChannel fc = FileChannel.open(path, CREATE_WRITE_TRUNCATE); SeekableInMemoryByteChannel simbc = new SeekableInMemoryByteChannel(xmlContent.getBytes())) { fc.transferFrom(simbc, 0, simbc.size()); @@ -168,7 +189,7 @@ public static void writeXml(final String resource, final String xmlContent) { * @param payload the ibdo to reset * @param answers an XML Document object to set the ibdo payload to */ - public static void setupPayload(final IBaseDataObject payload, final Document answers) { + public static void setupPayload(final IBaseDataObject payload, final Document answers, final ElementDecoders decoders) { final Element root = answers.getRootElement(); if (root != null) { @@ -179,7 +200,7 @@ public static void setupPayload(final IBaseDataObject payload, final Document an payload.setFileType(null); // Remove default filetype put on by ExtractionTest. // The only other fields set are data and filename. - IBaseDataObjectXmlHelper.ibdoFromXmlMainElements(parent, payload); + IBaseDataObjectXmlHelper.ibdoFromXmlMainElements(parent, payload, decoders); } } }