001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.io.ByteArrayInputStream;
021import java.io.Closeable;
022import java.io.FilterInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.SequenceInputStream;
026import java.security.DigestInputStream;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import java.util.List;
030
031/**
032 * Abstract base class for payload types that include a header. The header
033 * is parse and validated. If the header is valid the header is exposed as
034 * a byte array including optionally extra data implemented as fields,
035 * the rest of the payload is exposed as the complete payload of this object.
036 * If on the other hand the header is not valid the input stream is left
037 * as is so the complete payload can be read instead.
038 * Streams are also available to read the complete payload even though the
039 * header has been identified and parsed into optional extra fields.
040 *
041 * @author nicl
042 */
043public abstract class PayloadWithHeaderAbstract implements Closeable {
044
045    /** Has record been closed before. */
046    protected boolean bClosed;
047
048    /** <code>InputStream</code> to read payload. */
049    protected ByteCountingPushBackInputStream in_pb;
050
051    /** Payload length. */
052    protected long totalLength;
053
054    protected String digestAlgorithm;
055
056    /** Could the header be validated. */
057    protected boolean bIsValid;
058
059    /** Stream used to ensure we don't use more than the pushback buffer on
060     *  headers and at the same time store everything read in an array. */
061    protected MaxLengthRecordingInputStream in_flr;
062
063    /** The raw header read as bytes. */
064    protected byte[] header;
065
066    /** Message digest object. */
067    protected MessageDigest md;
068
069    /** Digest bytes. */
070    protected byte[] digest;
071
072    /** Automatic digesting of payload input stream. */
073    protected DigestInputStream in_digest;
074
075    /** Boolean indicating no such algorithm exception under initialization. */
076    protected boolean bNoSuchAlgorithmException;
077
078    /** Payload stream returned to user. */
079    protected InputStream in_payload;
080
081    /** Pushback input stream exposed to the outside. */
082    protected ByteCountingPushBackInputStream in_pb_exposed;
083
084    /** Sequence of the header as a stream combined with the payload stream. */
085    protected SequenceInputStream in_complete;
086
087    /** Object size, in bytes. */
088    public long payloadLength = 0L;
089
090    /** Validation errors and warnings. */
091    public Diagnostics diagnostics;
092
093    protected void initProcess() throws IOException {
094        in_flr = new MaxLengthRecordingInputStream(
095                in_pb, in_pb.getPushbackSize());
096        bIsValid = readHeader(in_flr, totalLength);
097        if (bIsValid) {
098            /*
099             * Payload Digest.
100             */
101            if (digestAlgorithm != null) {
102                try {
103                    md = MessageDigest.getInstance(digestAlgorithm);
104                } catch (NoSuchAlgorithmException e) {
105                    bNoSuchAlgorithmException = true;
106                }
107            }
108            if (md != null) {
109                in_digest = new DigestInputStreamNoSkip(in_pb, md);
110                in_payload = in_digest;
111            } else {
112                in_payload = in_pb;
113            }
114            /*
115             * Ensure close() is not called on the payload stream!
116             */
117            in_payload = new FilterInputStream(in_payload) {
118                @Override
119                public void close() throws IOException {
120                }
121            };
122            header = in_flr.getRecording();
123            in_pb_exposed = new ByteCountingPushBackInputStream(in_payload, in_pb.getPushbackSize());
124            in_complete = new SequenceInputStream(new ByteArrayInputStream(header), in_payload);
125            in_flr = null;
126        } else {
127            // Undo read and leave callers input stream in original state.
128            header = in_flr.getRecording();
129            in_pb.unread(header);
130            in_flr = null;
131            bClosed = true;
132        }
133    }
134
135    /**
136     * Reads the header and updates the payloadLength field if the header is valid.
137     * @param in the input stream to parse.
138     * @param payloadLength the record length.
139     * @return boolean indicating whether the header could be read
140     * @throws IOException I/O exception while reading headers
141     */
142    protected abstract boolean readHeader(MaxLengthRecordingInputStream in,
143            long payloadLength) throws IOException;
144
145    /**
146     * Get a <code>List</code> of all the headers found during parsing.
147     * @return <code>List</code> of <code>HeaderLine</code>
148     */
149    public List<HeaderLine> getHeaderList() {
150        throw new UnsupportedOperationException();
151    }
152
153    /**
154     * Get a header line structure or null, if no header line structure is
155     * stored with the given header name.
156     * @param field header name
157     * @return <code>HeaderLine</code> structure or null
158     */
159    public HeaderLine getHeader(String field) {
160        throw new UnsupportedOperationException();
161    }
162
163    /**
164     * Returns the result of the header validation.
165     * @return the result of the header validation
166     */
167    public boolean isValid() {
168        return bIsValid;
169    }
170
171    /**
172     * Get the raw header as bytes.
173     * @return raw header as bytes
174     */
175    public byte[] getHeader() {
176        return header;
177    }
178
179    /**
180     * Returns the <code>MessageDigest</code> used on payload stream.
181     * @return <code>MessageDigest</code> used on payload stream
182     */
183    public byte[] getDigest() {
184        if (digest == null && md != null) {
185            digest = md.digest();
186        }
187        return digest;
188    }
189
190    /**
191     * Get HTTP payload length.
192     * @return HTTP payload length
193     */
194    public long getPayloadLength() {
195        if (!bIsValid) {
196            throw new IllegalStateException("HttpHeader not valid");
197        }
198        return payloadLength;
199    }
200
201    /**
202     * Get payload total length, header and payload.
203     * @return payload total length, header and payload
204     */
205    public long getTotalLength() {
206        if (!bIsValid) {
207            throw new IllegalStateException("HttpHeader not valid");
208        }
209        return totalLength;
210    }
211
212    /**
213     * Get the number of unavailable bytes missing due to unexpected EOF.
214     * This method always returns <code>0</code> as long as the stream is open.
215     * @return number of unavailable bytes missing due to unexpected EOF
216     * @throws IOException if errors occur calling available method on stream
217     */
218    public long getUnavailable() throws IOException {
219        if (!bIsValid) {
220            throw new IllegalStateException("HttpHeader not valid");
221        }
222        return totalLength - in_pb.getConsumed();
223    }
224
225    /**
226     * Get an <code>InputStream</code> containing both the header and the
227     * payload.
228     * @return <code>InputStream</code> containing both the header and the
229     * payload.
230     */
231    public InputStream getInputStreamComplete() {
232        if (!bIsValid) {
233            throw new IllegalStateException("HttpHeader not valid");
234        }
235        return in_complete;
236    }
237
238    /**
239     * Get an <code>InputStream</code> containing only the payload.
240     * @return <code>InputStream</code> containing only the payload.
241     */
242    public ByteCountingPushBackInputStream getPayloadInputStream() {
243        if (!bIsValid) {
244            throw new IllegalStateException("HttpHeader not valid");
245        }
246        return in_pb_exposed;
247    }
248
249    /**
250     * Get payload remaining length.
251     * @return payload remaining length
252     * @throws IOException if errors occur calling available method on stream
253     */
254    public long getRemaining() throws IOException {
255        if (!bIsValid) {
256            throw new IllegalStateException("HttpHeader not valid");
257        }
258        return totalLength - in_pb.getConsumed();
259    }
260
261    /**
262     * Check to see if the header has been closed.
263     * @return boolean indicating whether this header is closed or not
264     */
265    public boolean isClosed() {
266        return bClosed;
267    }
268
269    /**
270     * Closes the this payload stream, skipping unread bytes in the process.
271     * @throws IOException I/O exception in closing process
272     */
273    public void close() throws IOException {
274        if (!bClosed) {
275            if (md != null) {
276                // Skip remaining unread bytes to ensure payload is completely
277                // digested. Skipping because the DigestInputStreamNoSkip
278                // has been altered to read when skipping.
279                while (in_digest.skip(totalLength) > 0) {
280                }
281            }
282            if (in_pb != null) {
283                in_pb.close();
284                in_pb = null;
285            }
286            bClosed = true;
287        }
288    }
289
290}