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}