001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.nio.codecs;
029
030import java.io.IOException;
031import java.nio.ByteBuffer;
032import java.nio.channels.FileChannel;
033import java.nio.channels.ReadableByteChannel;
034
035import org.apache.http.ConnectionClosedException;
036import org.apache.http.impl.io.HttpTransportMetricsImpl;
037import org.apache.http.nio.FileContentDecoder;
038import org.apache.http.nio.reactor.SessionInputBuffer;
039import org.apache.http.util.Args;
040
041/**
042 * Content decoder that cuts off after a defined number of bytes. This class
043 * is used to receive content of HTTP messages where the end of the content
044 * entity is determined by the value of the {@code Content-Length header}.
045 * Entities transferred using this stream can be maximum {@link Long#MAX_VALUE}
046 * long.
047 * <p>
048 * This decoder is optimized to transfer data directly from the underlying
049 * I/O session's channel to a {@link FileChannel}, whenever
050 * possible avoiding intermediate buffering in the session buffer.
051 *
052 * @since 4.0
053 */
054public class LengthDelimitedDecoder extends AbstractContentDecoder
055        implements FileContentDecoder {
056
057    private final long contentLength;
058
059    private long len;
060
061    public LengthDelimitedDecoder(
062            final ReadableByteChannel channel,
063            final SessionInputBuffer buffer,
064            final HttpTransportMetricsImpl metrics,
065            final long contentLength) {
066        super(channel, buffer, metrics);
067        Args.notNegative(contentLength, "Content length");
068        this.contentLength = contentLength;
069    }
070
071    @Override
072    public int read(final ByteBuffer dst) throws IOException {
073        Args.notNull(dst, "Byte buffer");
074        if (this.completed) {
075            return -1;
076        }
077        final int chunk = (int) Math.min((this.contentLength - this.len), Integer.MAX_VALUE);
078
079        final int bytesRead;
080        if (this.buffer.hasData()) {
081            final int maxLen = Math.min(chunk, this.buffer.length());
082            bytesRead = this.buffer.read(dst, maxLen);
083        } else {
084            bytesRead = readFromChannel(dst, chunk);
085        }
086        if (bytesRead == -1) {
087            this.completed = true;
088            if (this.len < this.contentLength) {
089                throw new ConnectionClosedException(
090                        "Premature end of Content-Length delimited message body (expected: "
091                        + this.contentLength + "; received: " + this.len);
092            }
093        }
094        this.len += bytesRead;
095        if (this.len >= this.contentLength) {
096            this.completed = true;
097        }
098        if (this.completed && bytesRead == 0) {
099            return -1;
100        } else {
101            return bytesRead;
102        }
103    }
104
105    @Override
106    public long transfer(
107            final FileChannel dst,
108            final long position,
109            final long count) throws IOException {
110
111        if (dst == null) {
112            return 0;
113        }
114        if (this.completed) {
115            return -1;
116        }
117
118        final int chunk = (int) Math.min((this.contentLength - this.len), Integer.MAX_VALUE);
119
120        final long bytesRead;
121        if (this.buffer.hasData()) {
122            final int maxLen = Math.min(chunk, this.buffer.length());
123            dst.position(position);
124            bytesRead = this.buffer.read(dst, maxLen);
125        } else {
126            if (this.channel.isOpen()) {
127                if (position > dst.size()) {
128                    throw new IOException("Position past end of file [" + position +
129                            " > " + dst.size() + "]");
130                }
131                bytesRead = dst.transferFrom(this.channel, position, count < chunk ? count : chunk);
132            } else {
133                bytesRead = -1;
134            }
135            if (bytesRead > 0) {
136                this.metrics.incrementBytesTransferred(bytesRead);
137            }
138        }
139        if (bytesRead == -1) {
140            this.completed = true;
141            if (this.len < this.contentLength) {
142                throw new ConnectionClosedException(
143                        "Premature end of Content-Length delimited message body (expected: "
144                        + this.contentLength + "; received: " + this.len);
145            }
146        }
147        this.len += bytesRead;
148        if (this.len >= this.contentLength) {
149            this.completed = true;
150        }
151        return bytesRead;
152    }
153
154    @Override
155    public String toString() {
156        final StringBuilder sb = new StringBuilder();
157        sb.append("[content length: ");
158        sb.append(this.contentLength);
159        sb.append("; pos: ");
160        sb.append(this.len);
161        sb.append("; completed: ");
162        sb.append(this.completed);
163        sb.append("]");
164        return sb.toString();
165    }
166}