001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.request.resource;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022
023import org.apache.commons.io.input.BoundedInputStream;
024import org.apache.wicket.protocol.http.servlet.ResponseIOException;
025import org.apache.wicket.request.resource.AbstractResource.WriteCallback;
026import org.apache.wicket.request.resource.IResource.Attributes;
027import org.apache.wicket.util.io.IOUtils;
028import org.apache.wicket.util.io.Streams;
029import org.apache.wicket.util.lang.Args;
030
031/**
032 * Used to read a part of an input stream and writes it to the output stream of the response taken
033 * from attributes in {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)}
034 * method.
035 *
036 * @author Tobias Soloschenko
037 * @since 7.0.0
038 */
039public class PartWriterCallback extends WriteCallback
040{
041        /**
042         * The input stream to read from
043         */
044        private final InputStream inputStream;
045
046        /**
047         * The total length to read if {@link #endbyte} is not specified
048         */
049        private final Long contentLength;
050
051        /**
052         * The byte to start reading from. If omitted then the input stream will be read from its
053         * beginning
054         */
055        private Long startbyte;
056
057        /**
058         * The end byte to read from the {@link #inputStream}. If omitted then the input stream will be
059         * read till its end
060         */
061        private Long endbyte;
062
063        /**
064         * The size of the buffer that is used for the copying of the data
065         */
066        private int bufferSize;
067
068        /**
069         * If the given input stream is going to be closed
070         */
071        private boolean close = false;
072
073
074        /**
075         * Creates a part writer callback.<br>
076         * <br>
077         * Reads a part of the given input stream. If the startbyte parameter is not null the number of
078         * bytes are skipped till the stream is read. If the endbyte is not null the stream is read till
079         * endbyte, else to the end of the whole stream. If startbyte and endbyte is null the whole
080         * stream is copied.
081         *
082         * @param inputStream
083         *            the input stream to read from
084         * @param contentLength
085         *            content length of the input stream. Ignored if <em>endByte</em> is specified
086         * @param startbyte
087         *            the start position to read from (if not null the number of bytes are skipped till
088         *            the stream is read)
089         * @param endbyte
090         *            the end position to read to (if not null the stream is going to be read till
091         *            endbyte, else to the end of the whole stream)
092         */
093        public PartWriterCallback(InputStream inputStream, Long contentLength, Long startbyte,
094                Long endbyte)
095        {
096                this.inputStream = inputStream;
097                this.contentLength = Args.notNull(contentLength, "contentLength");
098                this.startbyte = startbyte;
099                this.endbyte = endbyte;
100        }
101
102        /**
103         * Writes the data
104         *
105         * @param attributes
106         *            the attributes to get the output stream of the response
107         * @throws IOException
108         *             if something went wrong while writing the data to the output stream
109         */
110        @Override
111        public void writeData(Attributes attributes) throws IOException
112        {
113                try
114                {
115                        OutputStream outputStream = attributes.getResponse().getOutputStream();
116                        byte[] buffer = new byte[getBufferSize()];
117
118                        if (startbyte != null || endbyte != null)
119                        {
120                                // skipping the first bytes which are
121                                // requested to be skipped by the client
122                                if (startbyte != null)
123                                {
124                                        inputStream.skip(startbyte);
125                                }
126                                else
127                                {
128                                        // If no start byte has been given set it to 0
129                                        // which means no bytes has been skipped
130                                        startbyte = 0L;
131                                }
132
133                                // If there are no end bytes given read the whole stream till the end
134                                if (endbyte == null || Long.valueOf(-1).equals(endbyte))
135                                {
136                                        endbyte = contentLength;
137                                }
138
139                                BoundedInputStream boundedInputStream = null;
140                                try
141                                {
142                                        // Stream is going to be read from the starting point next to the skipped bytes
143                                        // till the end byte computed by the range between startbyte / endbyte
144                                        boundedInputStream = new BoundedInputStream(inputStream,
145                                                (endbyte - startbyte) + 1);
146
147                                        // The original input stream is going to be closed by the end of the request
148                                        // so set propagate close to false
149                                        boundedInputStream.setPropagateClose(false);
150
151                                        // The read bytes in the current buffer
152                                        int readBytes;
153
154                                        while ((readBytes = boundedInputStream.read(buffer)) != -1)
155                                        {
156                                                outputStream.write(buffer, 0, readBytes);
157                                        }
158                                }
159                                finally
160                                {
161                                        IOUtils.closeQuietly(boundedInputStream);
162                                }
163                        }
164                        else
165                        {
166                                // No range has been given so copy the content
167                                // from input stream to the output stream
168                                Streams.copy(inputStream, outputStream, getBufferSize());
169                        }
170                }
171                catch (ResponseIOException e)
172                {
173                        // the client has closed the connection and
174                        // doesn't read the stream further on
175                        // (in tomcats
176                        // org.apache.catalina.connector.ClientAbortException)
177                        // we ignore this case
178                }
179                if (close)
180                {
181                        IOUtils.close(inputStream);
182                }
183        }
184
185        /**
186         * Sets the buffer size used to send the data to the client
187         *
188         * @return the buffer size used to send the data to the client (default is 4096)
189         */
190        public int getBufferSize()
191        {
192                return bufferSize > 0 ? bufferSize : 4096;
193        }
194
195        /**
196         * Sets the buffer size used to send the data to the client
197         *
198         * @param bufferSize
199         *            the buffer size used to send the data to the client
200         * @return the part writer callback
201         */
202        public PartWriterCallback setBufferSize(int bufferSize)
203        {
204                this.bufferSize = bufferSize;
205                return this;
206        }
207
208        /**
209         * If the given input stream is going to be closed
210         * 
211         * @return if the given input stream is going to be closed
212         */
213        public boolean isClose()
214        {
215                return close;
216        }
217
218        /**
219         * If set true the given input stream is going to be closed
220         * 
221         * @param close
222         *            if the given input stream is going to be closed
223         * @return the part writer callback
224         */
225        public PartWriterCallback setClose(boolean close)
226        {
227                this.close = close;
228                return this;
229
230        }
231
232}