aboutsummaryrefslogtreecommitdiffstats
path: root/src/java/org/apache/fop/afp/util/AFPResourceUtil.java
blob: 98d2a8f8a0ad1c3eb80e6ac53ee0ce6c33a66ff3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

package org.apache.fop.afp.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collection;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.afp.AFPConstants;
import org.apache.fop.afp.modca.AbstractAFPObject.Category;
import org.apache.fop.afp.modca.ResourceObject;
import org.apache.fop.afp.parser.MODCAParser;
import org.apache.fop.afp.parser.UnparsedStructuredField;

/**
 * TODO better docs
 * Utility for AFP resource handling
 *
 *
 * A utility class to read structured fields from a MO:DCA document. Each
 * component of a mixed object document is explicitly defined and delimited
 * in the data. This is accomplished through the use of MO:DCA data structures,
 * called structured fields. Structured fields are used to envelop document
 * components and to provide commands and information to applications using
 * the data. Structured fields may contain one or more parameters. Each
 * parameter provides one value from a set of values defined by the architecture.
 * <p/>
 * MO:DCA structured fields consist of two parts: an introducer that identifies
 * the length and type of the structured field, and data that provides the
 * structured field's effect. The data is contained in a set of parameters,
 * which can consist of other data structures and data elements. The maximum
 * length of a structured field is 32767 bytes.
 * <p/>
 */
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 END_FIELD_ANY_NAME = (byte) (0xFF & 0xFF);

    private static final Log LOG = LogFactory.getLog(AFPResourceUtil.class);

    private AFPResourceUtil() {
        //nop
    }

    /**
     * Get the next structured field as identified by the identifier
     * parameter (this must be a valid MO:DCA structured field).
     * @param identifier the three byte identifier
     * @param inputStream the inputStream
     * @throws IOException if an I/O exception occurred
     * @return the next structured field or null when there are no more
     */
    public static byte[] getNext(byte[] identifier, InputStream inputStream) throws IOException {
        MODCAParser parser = new MODCAParser(inputStream);
        while (true) {
            UnparsedStructuredField field = parser.readNextStructuredField();
            if (field == null) {
                return null;
            }
            if (field.getSfClassCode() == identifier[0]
                    && field.getSfTypeCode() == identifier[1]
                    && field.getSfCategoryCode() == identifier[2]) {
                return field.getCompleteFieldAsBytes();
            }
        }
    }

    private static String getResourceName(UnparsedStructuredField field)
            throws UnsupportedEncodingException {
        //The first 8 bytes of the field data represent the resource name
        byte[] nameBytes = new byte[8];

        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);
    }

    /**
     * Copy a complete resource file to a given {@link OutputStream}.
     * @param in external resource input
     * @param out output destination
     * @throws IOException if an I/O error occurs
     */
    public static void copyResourceFile(final InputStream in, OutputStream out)
                throws IOException {
        MODCAParser parser = new MODCAParser(in);
        while (true) {
            UnparsedStructuredField field = parser.readNextStructuredField();
            if (field == null) {
                break;
            }
            out.write(MODCAParser.CARRIAGE_CONTROL_CHAR);
            field.writeTo(out);
        }
    }

    /**
     * Copy a named resource to a given {@link OutputStream}. The MO:DCA fields read from the
     * {@link InputStream} are scanned for the resource with the given name.
     * @param name name of structured field
     * @param in external resource input
     * @param out output destination
     * @throws IOException if an I/O error occurs
     */
    public static void copyNamedResource(String name,
            final InputStream in, final OutputStream out) throws IOException {
        final MODCAParser parser = new MODCAParser(in);
        Collection<String> resourceNames = new java.util.HashSet<String>();

        //Find matching "Begin" field
        final UnparsedStructuredField fieldBegin;
        while (true) {
            final UnparsedStructuredField field = parser.readNextStructuredField();

            if (field == null) {
                throw new IOException("Requested resource '" + name
                        + "' not found. Encountered resource names: " + resourceNames);
            }

            if (field.getSfTypeCode() != TYPE_CODE_BEGIN) { //0xA8=Begin
                continue; //Not a "Begin" 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"
                            + field);
                }
                fieldBegin = field;
                break; //Name doesn't match
            }
        }

        //Decide whether the resource file has to be wrapped in a resource object
        boolean wrapInResource;
        if (fieldBegin.getSfCategoryCode() == Category.PAGE_SEGMENT) {
            //A naked page segment must be wrapped in a resource object
            wrapInResource = true;
        } else if (fieldBegin.getSfCategoryCode() == Category.NAME_RESOURCE) {
            //A resource object can be copied directly
            wrapInResource = false;
        } else {
            throw new IOException("Cannot handle resource: " + fieldBegin);
        }

        //Copy structured fields (wrapped or as is)
        if (wrapInResource) {
            ResourceObject resourceObject =  new ResourceObject(name) {
                protected void writeContent(OutputStream os) throws IOException {
                    copyNamedStructuredFields(name, fieldBegin, parser, out);
                }
            };
            resourceObject.setType(ResourceObject.TYPE_PAGE_SEGMENT);
            resourceObject.writeToStream(out);
        } else {
            copyNamedStructuredFields(name, fieldBegin, parser, out);
        }
    }

    private static void copyNamedStructuredFields(final String name, UnparsedStructuredField fieldBegin,
            MODCAParser parser, OutputStream out) throws IOException {
        UnparsedStructuredField field = fieldBegin;
        while (true) {
            if (field == null) {
                throw new IOException("Ending structured field not found for resource " + name);
            }
            out.write(MODCAParser.CARRIAGE_CONTROL_CHAR);
            field.writeTo(out);

            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;
    }
}