View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client.util;
20  
21  import java.io.Closeable;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.nio.ByteBuffer;
25  import java.util.Iterator;
26  import java.util.NoSuchElementException;
27  
28  import org.eclipse.jetty.client.api.ContentProvider;
29  import org.eclipse.jetty.util.BufferUtil;
30  import org.eclipse.jetty.util.log.Log;
31  import org.eclipse.jetty.util.log.Logger;
32  
33  /**
34   * A {@link ContentProvider} for an {@link InputStream}.
35   * <p />
36   * The input stream is read once and therefore fully consumed.
37   * Invocations to the {@link #iterator()} method after the first will return an "empty" iterator
38   * because the stream has been consumed on the first invocation.
39   * <p />
40   * However, it is possible for subclasses to override {@link #onRead(byte[], int, int)} to copy
41   * the content read from the stream to another location (for example a file), and be able to
42   * support multiple invocations of {@link #iterator()}, returning the iterator provided by this
43   * class on the first invocation, and an iterator on the bytes copied to the other location
44   * for subsequent invocations.
45   * <p />
46   * It is possible to specify, at the constructor, a buffer size used to read content from the
47   * stream, by default 4096 bytes.
48   * <p />
49   * The {@link InputStream} passed to the constructor is by default closed when is it fully
50   * consumed (or when an exception is thrown while reading it), unless otherwise specified
51   * to the {@link #InputStreamContentProvider(java.io.InputStream, int, boolean) constructor}.
52   */
53  public class InputStreamContentProvider implements ContentProvider
54  {
55      private static final Logger LOG = Log.getLogger(InputStreamContentProvider.class);
56  
57      private final InputStream stream;
58      private final int bufferSize;
59      private final boolean autoClose;
60  
61      public InputStreamContentProvider(InputStream stream)
62      {
63          this(stream, 4096);
64      }
65  
66      public InputStreamContentProvider(InputStream stream, int bufferSize)
67      {
68          this(stream, bufferSize, true);
69      }
70  
71      public InputStreamContentProvider(InputStream stream, int bufferSize, boolean autoClose)
72      {
73          this.stream = stream;
74          this.bufferSize = bufferSize;
75          this.autoClose = autoClose;
76      }
77  
78      @Override
79      public long getLength()
80      {
81          return -1;
82      }
83  
84      /**
85       * Callback method invoked just after having read from the stream,
86       * but before returning the iteration element (a {@link ByteBuffer}
87       * to the caller.
88       * <p />
89       * Subclasses may override this method to copy the content read from
90       * the stream to another location (a file, or in memory if the content
91       * is known to fit).
92       *
93       * @param buffer the byte array containing the bytes read
94       * @param offset the offset from where bytes should be read
95       * @param length the length of the bytes read
96       * @return a {@link ByteBuffer} wrapping the byte array
97       */
98      protected ByteBuffer onRead(byte[] buffer, int offset, int length)
99      {
100         if (length <= 0)
101             return BufferUtil.EMPTY_BUFFER;
102         return ByteBuffer.wrap(buffer, offset, length);
103     }
104 
105     /**
106      * Callback method invoked when an exception is thrown while reading
107      * from the stream.
108      *
109      * @param failure the exception thrown while reading from the stream.
110      */
111     protected void onReadFailure(Throwable failure)
112     {
113     }
114 
115     @Override
116     public Iterator<ByteBuffer> iterator()
117     {
118         return new InputStreamIterator();
119     }
120 
121     /**
122      * Iterating over an {@link InputStream} is tricky, because {@link #hasNext()} must return false
123      * if the stream reads -1. However, we don't know what to return until we read the stream, which
124      * means that stream reading must be performed by {@link #hasNext()}, which introduces a side-effect
125      * on what is supposed to be a simple query method (with respect to the Query Command Separation
126      * Principle).
127      * <p />
128      * Alternatively, we could return {@code true} from {@link #hasNext()} even if we don't know that
129      * we will read -1, but then when {@link #next()} reads -1 it must return an empty buffer.
130      * However this is problematic, since GETs with no content indication would become GET with chunked
131      * content, and not understood by servers.
132      * <p />
133      * Therefore we need to make sure that {@link #hasNext()} does not perform any side effect (so that
134      * it can be called multiple times) until {@link #next()} is called.
135      */
136     private class InputStreamIterator implements Iterator<ByteBuffer>, Closeable
137     {
138         private Throwable failure;
139         private ByteBuffer buffer;
140         private Boolean hasNext;
141 
142         @Override
143         public boolean hasNext()
144         {
145             try
146             {
147                 if (hasNext != null)
148                     return hasNext;
149 
150                 byte[] bytes = new byte[bufferSize];
151                 int read = stream.read(bytes);
152                 LOG.debug("Read {} bytes from {}", read, stream);
153                 if (read > 0)
154                 {
155                     hasNext = Boolean.TRUE;
156                     buffer = onRead(bytes, 0, read);
157                     return true;
158                 }
159                 else if (read < 0)
160                 {
161                     hasNext = Boolean.FALSE;
162                     buffer = null;
163                     close();
164                     return false;
165                 }
166                 else
167                 {
168                     hasNext = Boolean.TRUE;
169                     buffer = BufferUtil.EMPTY_BUFFER;
170                     return true;
171                 }
172             }
173             catch (Throwable x)
174             {
175                 LOG.debug(x);
176                 if (failure == null)
177                 {
178                     failure = x;
179                     onReadFailure(x);
180                     // Signal we have more content to cause a call to
181                     // next() which will throw NoSuchElementException.
182                     hasNext = Boolean.TRUE;
183                     buffer = null;
184                     close();
185                     return true;
186                 }
187                 throw new IllegalStateException();
188             }
189         }
190 
191         @Override
192         public ByteBuffer next()
193         {
194             if (failure != null)
195             {
196                 // Consume the failure so that calls to hasNext() will return false.
197                 hasNext = Boolean.FALSE;
198                 buffer = null;
199                 throw (NoSuchElementException)new NoSuchElementException().initCause(failure);
200             }
201             if (!hasNext())
202                 throw new NoSuchElementException();
203 
204             ByteBuffer result = buffer;
205             if (result == null)
206             {
207                 hasNext = Boolean.FALSE;
208                 buffer = null;
209                 throw new NoSuchElementException();
210             }
211             else
212             {
213                 hasNext = null;
214                 buffer = null;
215                 return result;
216             }
217         }
218 
219         @Override
220         public void remove()
221         {
222             throw new UnsupportedOperationException();
223         }
224 
225         @Override
226         public void close()
227         {
228             if (autoClose)
229             {
230                 try
231                 {
232                     stream.close();
233                 }
234                 catch (IOException x)
235                 {
236                     LOG.ignore(x);
237                 }
238             }
239         }
240     }
241 }