From 8f37765b5c9b6dbe80d860207802aa4f7f8b1a94 Mon Sep 17 00:00:00 2001 From: Mehdi Houshmand Date: Wed, 4 Jul 2012 07:04:03 +0000 Subject: [PATCH] Bugzilla#53502: MODCA end structured field now more conformant with the spec by allowing 0xFFFF match (match any). Submitted by Robert Meyer git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1357110 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/fop/afp/util/AFPResourceUtil.java | 97 ++++++++++----- .../fop/afp/AFPResourceUtilTestCase.java | 117 +++++++++++++----- .../org/apache/fop/afp/resource_any_name.afp | Bin 0 -> 15613 bytes .../apache/fop/afp/resource_name_match.afp | Bin 0 -> 15619 bytes .../apache/fop/afp/resource_name_mismatch.afp | Bin 0 -> 15617 bytes .../apache/fop/afp/resource_no_end_name.afp | Bin 0 -> 15611 bytes 6 files changed, 151 insertions(+), 63 deletions(-) create mode 100644 test/java/org/apache/fop/afp/resource_any_name.afp create mode 100644 test/java/org/apache/fop/afp/resource_name_match.afp create mode 100644 test/java/org/apache/fop/afp/resource_name_mismatch.afp create mode 100644 test/java/org/apache/fop/afp/resource_no_end_name.afp diff --git a/src/java/org/apache/fop/afp/util/AFPResourceUtil.java b/src/java/org/apache/fop/afp/util/AFPResourceUtil.java index 979376b3e..98d2a8f8a 100644 --- a/src/java/org/apache/fop/afp/util/AFPResourceUtil.java +++ b/src/java/org/apache/fop/afp/util/AFPResourceUtil.java @@ -56,8 +56,11 @@ import org.apache.fop.afp.parser.UnparsedStructuredField; */ public final class AFPResourceUtil { - private static final byte TYPE_CODE_BEGIN = (byte)(0xA8 & 0xFF); - private static final byte TYPE_CODE_END = (byte)(0xA9 & 0xFF); + private static final byte TYPE_CODE_BEGIN = (byte) (0xA8 & 0xFF); + + private static final byte TYPE_CODE_END = (byte) (0xA9 & 0xFF); + + private static final byte END_FIELD_ANY_NAME = (byte) (0xFF & 0xFF); private static final Log LOG = LogFactory.getLog(AFPResourceUtil.class); @@ -92,10 +95,13 @@ public final class AFPResourceUtil { throws UnsupportedEncodingException { //The first 8 bytes of the field data represent the resource name byte[] nameBytes = new byte[8]; - System.arraycopy(field.getData(), 0, nameBytes, 0, 8); - String asciiName; - asciiName = new String(nameBytes, AFPConstants.EBCIDIC_ENCODING); - return asciiName; + + byte[] fieldData = field.getData(); + if (fieldData.length < 8) { + throw new IllegalArgumentException("Field data does not contain a resource name"); + } + System.arraycopy(fieldData, 0, nameBytes, 0, 8); + return new String(nameBytes, AFPConstants.EBCIDIC_ENCODING); } /** @@ -128,12 +134,13 @@ public final class AFPResourceUtil { public static void copyNamedResource(String name, final InputStream in, final OutputStream out) throws IOException { final MODCAParser parser = new MODCAParser(in); - Collection resourceNames = new java.util.HashSet(); + Collection resourceNames = new java.util.HashSet(); //Find matching "Begin" field final UnparsedStructuredField fieldBegin; while (true) { - UnparsedStructuredField field = parser.readNextStructuredField(); + final UnparsedStructuredField field = parser.readNextStructuredField(); + if (field == null) { throw new IOException("Requested resource '" + name + "' not found. Encountered resource names: " + resourceNames); @@ -142,8 +149,10 @@ public final class AFPResourceUtil { if (field.getSfTypeCode() != TYPE_CODE_BEGIN) { //0xA8=Begin continue; //Not a "Begin" field } - String resourceName = getResourceName(field); + final String resourceName = getResourceName(field); + resourceNames.add(resourceName); + if (resourceName.equals(name)) { if (LOG.isDebugEnabled()) { LOG.debug("Start of requested structured field found:\n" @@ -170,45 +179,65 @@ public final class AFPResourceUtil { if (wrapInResource) { ResourceObject resourceObject = new ResourceObject(name) { protected void writeContent(OutputStream os) throws IOException { - copyStructuredFields(name, fieldBegin, parser, out); + copyNamedStructuredFields(name, fieldBegin, parser, out); } }; resourceObject.setType(ResourceObject.TYPE_PAGE_SEGMENT); resourceObject.writeToStream(out); } else { - copyStructuredFields(name, fieldBegin, parser, out); + copyNamedStructuredFields(name, fieldBegin, parser, out); } } - private static void copyStructuredFields(String name, UnparsedStructuredField fieldBegin, + private static void copyNamedStructuredFields(final String name, UnparsedStructuredField fieldBegin, MODCAParser parser, OutputStream out) throws IOException { - boolean inRequestedResource; - - //The "Begin" field first - out.write(MODCAParser.CARRIAGE_CONTROL_CHAR); - fieldBegin.writeTo(out); - UnparsedStructuredField field; - - //Then the rest of the fields until the corresponding "End" field - inRequestedResource = true; - do { - field = parser.readNextStructuredField(); + UnparsedStructuredField field = fieldBegin; + while (true) { if (field == null) { - break; //Unexpected EOF - } - - if (field.getSfTypeCode() == TYPE_CODE_END) { - String resourceName = getResourceName(field); - if (resourceName.equals(name)) { - inRequestedResource = false; //Signal end of loop - } + throw new IOException("Ending structured field not found for resource " + name); } out.write(MODCAParser.CARRIAGE_CONTROL_CHAR); field.writeTo(out); - } while (inRequestedResource); - if (inRequestedResource) { - throw new IOException("Ending structured field not found for resource " + name); + + if (isEndOfStructuredField(field, fieldBegin, name)) { + break; + } + field = parser.readNextStructuredField(); } } + private static boolean isEndOfStructuredField(UnparsedStructuredField field, + UnparsedStructuredField fieldBegin, String name) throws UnsupportedEncodingException { + return fieldMatchesEndTagType(field) + && fieldMatchesBeginCategoryCode(field, fieldBegin) + && fieldHasValidName(field, name); + } + + private static boolean fieldMatchesEndTagType(UnparsedStructuredField field) { + return field.getSfTypeCode() == TYPE_CODE_END; + } + + private static boolean fieldMatchesBeginCategoryCode(UnparsedStructuredField field, + UnparsedStructuredField fieldBegin) { + return fieldBegin.getSfCategoryCode() == field.getSfCategoryCode(); + } + + /** + * The AFP specification states that it is valid for the end structured field to have: + * - No tag name specified, which will cause it to match any existing tag type match. + * - The name has FFFF as its first two bytes + * - The given name matches the previous structured field name + */ + private static boolean fieldHasValidName(UnparsedStructuredField field, String name) + throws UnsupportedEncodingException { + if (field.getData().length > 0) { + if (field.getData()[0] == field.getData()[1] + && field.getData()[0] == END_FIELD_ANY_NAME) { + return true; + } else { + return name.equals(getResourceName(field)); + } + } + return true; + } } diff --git a/test/java/org/apache/fop/afp/AFPResourceUtilTestCase.java b/test/java/org/apache/fop/afp/AFPResourceUtilTestCase.java index 6ab7f475d..a7cf57ebe 100644 --- a/test/java/org/apache/fop/afp/AFPResourceUtilTestCase.java +++ b/test/java/org/apache/fop/afp/AFPResourceUtilTestCase.java @@ -22,23 +22,34 @@ package org.apache.fop.afp; import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.Arrays; +import org.junit.Test; + import org.apache.commons.io.IOUtils; import org.apache.fop.afp.util.AFPResourceUtil; import org.junit.Test; +import static org.junit.Assert.assertTrue; + /** * Tests the {@link AFPResourceUtil} class. */ public class AFPResourceUtilTestCase { private static final String RESOURCE_FILENAME = "expected_resource.afp"; - private static final String NAMED_RESOURCE_FILENAME = "expected_named_resource.afp"; - private static final String PSEG = "XFEATHER"; + private static final String RESOURCE_ANY_NAME = "resource_any_name.afp"; + private static final String RESOURCE_NAME_MATCH = "resource_name_match.afp"; + private static final String RESOURCE_NAME_MISMATCH = "resource_name_mismatch.afp"; + private static final String RESOURCE_NO_END_NAME = "resource_no_end_name.afp"; + + private static final String PSEG_A = "XFEATHER"; + private static final String PSEG_B = "S1CODEQR"; /** * Tests copyResourceFile() @@ -46,57 +57,105 @@ public class AFPResourceUtilTestCase { */ @Test public void testCopyResourceFile() throws Exception { + compareResources(new ResourceCopier() { + public void copy(InputStream in, OutputStream out) throws IOException { + AFPResourceUtil.copyResourceFile(in, out); + } + }, RESOURCE_FILENAME, RESOURCE_FILENAME); + } - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + /** + * Tests copyNamedResource() + * @throws Exception - + */ + @Test + public void testCopyNamedResource() throws Exception { + compareResources(new ResourceCopier() { + public void copy(InputStream in, OutputStream out) throws IOException { + AFPResourceUtil.copyNamedResource(PSEG_A, in, out); + } + }, RESOURCE_FILENAME, NAMED_RESOURCE_FILENAME); + } - InputStream in = null; + private void compareResources(ResourceCopier copyResource, String resourceA, String resourceB) + throws IOException { + ByteArrayOutputStream baos = copyResource(resourceA, copyResource); + byte[] expectedBytes = resourceAsByteArray(resourceB); + assertTrue(Arrays.equals(expectedBytes, baos.toByteArray())); + } + private ByteArrayOutputStream copyResource(String resource, ResourceCopier resourceCopier) + throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream in = null; try { - in = getClass().getResourceAsStream(RESOURCE_FILENAME); - AFPResourceUtil.copyResourceFile(in, baos); + in = getClass().getResourceAsStream(resource); + resourceCopier.copy(in, baos); } finally { in.close(); } + return baos; + } + private byte[] resourceAsByteArray(String resource) throws IOException { + InputStream in = null; byte[] expectedBytes = null; - try { - in = getClass().getResourceAsStream(RESOURCE_FILENAME); + in = getClass().getResourceAsStream(resource); expectedBytes = IOUtils.toByteArray(in); } finally { in.close(); } - - assertTrue(Arrays.equals(expectedBytes, baos.toByteArray())); - + return expectedBytes; } /** - * Tests copyNamedResource() + * Tests the validity of a closing structured field having an FF FF name which + * allows it to match any existing matching starting field * @throws Exception - */ @Test - public void testCopyNamedResource() throws Exception { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + public void testResourceAnyName() throws Exception { + testResource(RESOURCE_ANY_NAME, PSEG_B); + } - InputStream in = null; + /** + * Tests a matching end structured field name + * @throws Exception - + */ + @Test + public void testResourceNameMatch() throws Exception { + testResource(RESOURCE_NAME_MATCH, PSEG_B); + } - try { - in = getClass().getResourceAsStream(RESOURCE_FILENAME); - AFPResourceUtil.copyNamedResource(PSEG, in, baos); - } finally { - in.close(); - } + /** + * Tests to see whether a matching structured field pair with mismatching + * names fails. + * @throws Exception - + */ + @Test(expected=Exception.class) + public void testResourceNameMismatch() throws Exception { + testResource(RESOURCE_NAME_MISMATCH, PSEG_B); + } - byte[] expectedBytes = null; + /** + * Tests a matching structured end field with no name + * @throws Exception - + */ + @Test + public void testResourceNoEndName() throws Exception { + testResource(RESOURCE_NO_END_NAME, PSEG_B); + } - try { - in = getClass().getResourceAsStream(NAMED_RESOURCE_FILENAME); - expectedBytes = IOUtils.toByteArray(in); - } finally { - in.close(); - } + private void testResource(String resource, final String pseg) throws Exception { + copyResource(resource, new ResourceCopier() { + public void copy(InputStream in, OutputStream out) throws IOException { + AFPResourceUtil.copyNamedResource(pseg, in, out); + } + }); + } - assertTrue(Arrays.equals(expectedBytes, baos.toByteArray())); + private interface ResourceCopier { + public void copy(InputStream in, OutputStream out) throws IOException; } } diff --git a/test/java/org/apache/fop/afp/resource_any_name.afp b/test/java/org/apache/fop/afp/resource_any_name.afp new file mode 100644 index 0000000000000000000000000000000000000000..a91cabfaedae5b8a16e02200d22c12634e459a35 GIT binary patch literal 15613 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