/* Took the basic code from Axis 1.2 and modified to fit into the cloud code base */ /* * Copyright 2001-2004 The Apache Software Foundation. * * Licensed 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. */ package com.cloud.bridge.io; import java.io.IOException; import java.io.InputStream; import java.io.FilterInputStream; import org.apache.log4j.Logger; /** * This class takes the input stream and turns it multiple streams. DIME version 0 format <pre> 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ --- | VERSION |B|E|C| TYPE_T| OPT_T | OPTIONS_LENGTH | A +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ID_LENGTH | TYPE_LENGTH | Always present 12 bytes +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ even on chunked data. | DATA_LENGTH | V +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ --- | / / OPTIONS + PADDING / / (absent for version 0) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | / / ID + PADDING / / | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | / / TYPE + PADDING / / | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | / / DATA + PADDING / / | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ </pre> * This implementation of input stream does not support marking operations. * * Incoming data is DIME encoded when its MIME type is "application/dime". * Then use this class to pull out 2 streams: * (1) The first stream is the SOAP request, * (2) The second stream is a chunked attachment (e.g., a file to store) * * The DIME format is defined at this reference: * http://msdn.microsoft.com/en-us/library/aa480488.aspx * * @author Rick Rineholt */ public class DimeDelimitedInputStream extends FilterInputStream { protected final static Logger logger = Logger.getLogger(DimeDelimitedInputStream.class); InputStream is = null; //The source input stream. boolean closed = true; //The stream has been closed. boolean theEnd = false; //There are no more streams left. boolean moreChunks = false; //More chunks are a coming! boolean MB = false; //Message begin flag boolean ME = false; //Message end flag String type = null; // String id = null; // String tnf = null; //DIME type format long recordLength = 0L; //length of the current record. long bytesRead = 0L; //How many bytes of the current record have been read. int dataPadLength = 0; //How many pad bytes there are. protected int streamNo = 0; protected IOException streamInError = null; private static byte[] trash = new byte[4]; protected static int streamCount = 0; //number of streams produced. protected static synchronized int newStreamNo() { logger.debug( "streamNo " + (streamCount + 1)); return ++streamCount; } /** * There can be multiple streams in a DIME encoding. For example, the first * stream can be a SOAP message, and the second stream a binary attachment (e.g., * a file). During reading after an EOF is returned, this function should be * called to see if there is another stream following the last. * * @return the dime delimited stream, null if there are no more streams * @throws IOException if there was an error loading the data for the next stream */ public synchronized DimeDelimitedInputStream getNextStream() throws IOException { if (null != streamInError) throw streamInError; if (theEnd) return null; //Each Stream must be read in succession if (bytesRead < recordLength || moreChunks) throw new RuntimeException("attach.dimeReadFullyError"); dataPadLength -= readPad(dataPadLength); //Create an new dime stream that comes after this one. return new DimeDelimitedInputStream( this.is ); } /** * Create a new dime stream. * * @param is the <code>InputStream</code> to wrap * @throws IOException if anything goes wrong */ public DimeDelimitedInputStream( InputStream is ) throws IOException { super(null); streamNo = newStreamNo(); closed = false; this.is = is; readHeader( false ); } /** * Make sure to skip the pad which appear in several parts of a DIME message. * @param size * @return * @throws IOException */ private final int readPad( int size ) throws IOException { if (0 == size) return 0; int read = readFromStream(trash, 0, size); if (size != read) { streamInError = new IOException("attach.dimeNotPaddedCorrectly"); throw streamInError; } return read; } private final int readFromStream( byte[] b ) throws IOException { return readFromStream( b, 0, b.length ); } private final int readFromStream( byte[] b, int start, int length ) throws IOException { int br = 0; int brTotal = 0; if (length == 0) return 0; do { try { br = is.read( b, brTotal + start, length - brTotal ); } catch (IOException e) { streamInError = e; throw e; } if (br > 0) brTotal += br; } while( br > -1 && brTotal < length ); return br > -1 ? brTotal : br; } /** * Get the id for this stream part. * @return the id; */ public String getContentId() { return id; } public String getDimeTypeNameFormat() { return tnf; } /** * Get the type, as read from the header. * @return the type of this dime */ public String getType() { return type; } /** * Read from the DIME stream. * * @param b is the array to read into. * @param off is the offset * @return the number of bytes read. -1 if endof stream * @throws IOException if data could not be read from the stream */ public synchronized int read( byte[] b, int off, int len ) throws IOException { if (closed) { dataPadLength -= readPad(dataPadLength); throw new IOException( "streamClosed" ); } return _read( b, off, len ); } protected int _read( byte[] b, int off, int len ) throws IOException { int totalbytesread = 0; int bytes2read = 0; if (len < 0) throw new IllegalArgumentException( "attach.readLengthError" + len ); if (off < 0) throw new IllegalArgumentException( "attach.readOffsetError" + off ); if (b == null) throw new IllegalArgumentException( "attach.readArrayNullError" ); if (b.length < off + len) throw new IllegalArgumentException("attach.readArraySizeError " + b.length + " " + len + " " + off ); if (null != streamInError) throw streamInError; if (0 == len) return 0; //quick. // odd case no data to read -- give back 0 next time -1; if (recordLength == 0 && bytesRead == 0 && !moreChunks) { ++bytesRead; if (ME) finalClose(); return 0; } if (bytesRead >= recordLength && !moreChunks) { dataPadLength -= readPad( dataPadLength ); if (ME) finalClose(); return -1; } do { if (bytesRead >= recordLength && moreChunks) readHeader( true ); bytes2read = (int) Math.min( recordLength - bytesRead, (long)len - totalbytesread ); try { bytes2read = is.read( b, off + totalbytesread, bytes2read ); } catch (IOException e) { streamInError = e; throw e; } if (0 < bytes2read) { totalbytesread += bytes2read; bytesRead += bytes2read; } } while( bytes2read > -1 && totalbytesread < len && (bytesRead < recordLength || moreChunks)); if ( 0 > bytes2read ) { if (moreChunks) { streamInError = new IOException("attach.DimeStreamError0"); throw streamInError; } if (bytesRead < recordLength) { streamInError = new IOException("attach.DimeStreamError1 " + (recordLength - bytesRead)); throw streamInError; } if (!ME) { streamInError = new IOException("attach.DimeStreamError0"); throw streamInError; } //in theory the last chunk of data should also have been padded, but lets be tolerant of that. dataPadLength = 0; } else if (bytesRead >= recordLength) { //get rid of pading. try { dataPadLength -= readPad( dataPadLength ); } catch (IOException e) { //in theory the last chunk of data should also have been padded, but lets be tolerant of that. if (!ME) throw e; else { dataPadLength = 0; streamInError = null; } } } if (bytesRead >= recordLength && ME) finalClose(); return totalbytesread >= 0 ? totalbytesread : -1; } /** * The DIME header is read into local class data fields and are not * passed as part of the stream data. * * @param isChunk * @throws IOException */ protected void readHeader( boolean isChunk ) throws IOException { bytesRead = 0; //How many bytes of the record have been read. if (isChunk) { if (!moreChunks) throw new RuntimeException("attach.DimeStreamError2"); dataPadLength -= readPad(dataPadLength); //Just in case it was left over. } byte[] header = new byte[12]; if (header.length != readFromStream( header) ) { streamInError = new IOException("attach.DimeStreamError3 " + header.length ); throw streamInError; } //VERSION byte version = (byte) ((header[0] >>> 3) & 0x1f); if (version > 1) { streamInError = new IOException("attach.DimeStreamError4 " + version ); throw streamInError; } //B, E, C MB = 0 != (0x4 & header[0]); ME = 0 != (0x2 & header[0]); moreChunks = 0 != (0x1 & header[0]); //TYPE_T if (!isChunk) { switch( ((header[1] >>> 4) & (byte)0x0f) ) { case 0x00: tnf = "UNCHANGED"; break; case 0x01: tnf = "MIME"; break; case 0x02: tnf = "URI"; break; default: tnf = "UNKNOWN"; break; } } //OPTIONS_LENGTH int optionsLength = ((((int) header[2]) << 8) & 0xff00) | ((int) header[3]); //ID_LENGTH int idLength = ((((int) header[4]) << 8) & 0xff00) | ((int) header[5]); //TYPE_LENGTH int typeLength = ((((int) header[6]) << 8) & 0xff00) | ((int) header[7]); //DATA_LENGTH recordLength = ((((long) header[8] ) << 24) & 0xff000000L) | ((((long) header[9] ) << 16) & 0xff0000L ) | ((((long) header[10]) << 8 ) & 0xff00L ) | ((long) header[11] & 0xffL ); //OPTIONS + PADDING if (0 != optionsLength) { byte[] optBytes = new byte[optionsLength]; if (optionsLength != readFromStream( optBytes )) { streamInError = new IOException("attach.DimeStreamError5 " + optionsLength ); throw streamInError; } optBytes = null; // throw it away, don't know anything about options. int pad = (int) ((4L - (optionsLength & 0x3L)) & 0x03L); if (pad != readFromStream( header, 0, pad )) { streamInError = new IOException("attach.DimeStreamError7"); throw streamInError; } } // ID + PADDING if (0 < idLength) { byte[] idBytes = new byte[ idLength]; if (idLength != readFromStream( idBytes )) { streamInError = new IOException("attach.DimeStreamError8"); throw streamInError; } if (idLength != 0 && !isChunk) id = new String(idBytes); int pad = (int) ((4L - (idLength & 0x3L)) & 0x03L); if (pad != readFromStream( header, 0, pad )) { streamInError = new IOException("attach.DimeStreamError9"); throw streamInError; } } //TYPE + PADDING if (0 < typeLength) { byte[] typeBytes = new byte[typeLength]; if (typeLength != readFromStream( typeBytes )) { streamInError = new IOException("attach.DimeStreamError10"); throw streamInError; } if (typeLength != 0 && !isChunk) type = new String(typeBytes); int pad = (int) ((4L - (typeLength & 0x3L)) & 0x03L); if (pad != readFromStream( header, 0, pad )) { streamInError = new IOException("attach.DimeStreamError11"); throw streamInError; } } logger.debug("MB:" + MB + ", ME:" + ME + ", CF:" + moreChunks + "Option length:" + optionsLength + ", ID length:" + idLength + ", typeLength:" + typeLength + ", TYPE_T:" + tnf); logger.debug("id:\"" + id + "\""); logger.debug("type:\"" + type + "\""); logger.debug("recordlength:\"" + recordLength + "\""); dataPadLength = (int) ((4L - (recordLength & 0x3L)) & 0x03L); } /** * Read from the delimited stream. * * @param b is the array to read into. Read as much as possible * into the size of this array. * @return the number of bytes read. -1 if endof stream * @throws IOException if data could not be read from the stream */ public int read( byte[] b ) throws IOException { return read( b, 0, b.length ); } // fixme: this seems a bit inefficient /** * Read from the boundary delimited stream. * * @return the byte read, or -1 if endof stream * @throws IOException if there was an error reading the data */ public int read() throws IOException { byte[] b = new byte[1]; int read = read( b, 0, 1 ); if (read < 0) return -1; // fixme: should we also check for read != 1? return (b[0] & 0xff); // convert byte value to a positive int } /** * Closes the stream. * This will take care of flushing any remaining data to the stream. * Multiple calls to this method will result in the stream being closed once * and then all subsequent calls being ignored. * * @throws IOException if the stream could not be closed */ public void close() throws IOException { synchronized( this ) { if (closed) return; closed = true; //mark it closed. } logger.debug("bStreamClosed " + streamNo); if (bytesRead < recordLength || moreChunks) { //We need get this off the stream. Easy way to flush through the stream; byte[] readrest = new byte[1024 * 16]; int bread = 0; do { bread = _read( readrest, 0, readrest.length ); //should also close the original stream. } while( bread > -1 ); } dataPadLength -= readPad( dataPadLength ); } /** * Skip n bytes of data in the DIME stream, while reading and processing * any headers in the current stream. * * @param n - number of data bytes to skip * @return number of bytes actually skipped * @throws IOException */ public long skip( long n ) throws IOException { long bytesSkipped = 0; long bytes2Read = 0; byte[] dumpbytes = new byte[1024]; while( n > 0 ) { bytes2Read = (n > 1024 ? 1024 : n); bytes2Read = _read( dumpbytes, 0, (int)bytes2Read ); n -= bytes2Read; bytesSkipped += bytes2Read; } return bytesSkipped; } /** * Mark the stream. This is not supported. */ public void mark( int readlimit ) { //do nothing } public void reset() throws IOException { streamInError = new IOException("attach.bounday.mns"); throw streamInError; } public boolean markSupported() { return false; } public synchronized int available() throws IOException { if (null != streamInError) throw streamInError; int chunkAvail = (int) Math.min((long)Integer.MAX_VALUE, recordLength - bytesRead); int streamAvail = 0; try { streamAvail = is.available(); } catch( IOException e ) { streamInError = e; throw e; } if (chunkAvail == 0 && moreChunks && (12 + dataPadLength) <= streamAvail) { dataPadLength -= readPad(dataPadLength); readHeader( true ); return available(); } return Math.min( streamAvail, chunkAvail ); } protected void finalClose() throws IOException { try { theEnd = true; if(null != is) is.close(); } finally { is= null; } } }