/**
* The MIT License (MIT)
*
* Copyright (c) 2014-2017 Yegor Bugayenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.takes.rq;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.LinkedList;
import lombok.EqualsAndHashCode;
import org.takes.HttpException;
import org.takes.Request;
import org.takes.misc.Opt;
import org.takes.misc.Utf8String;
/**
* Live request.
*
* <p>The class is immutable and thread-safe.
*
* @author Yegor Bugayenko (yegor256@gmail.com)
* @version $Id: 610f345166c362a4ebc2f96ba75aa2a5e1ddb894 $
* @since 0.1
*/
@EqualsAndHashCode(callSuper = true)
public final class RqLive extends RqWrap {
/**
* Ctor.
* @param input Input stream
* @throws IOException If fails
*/
public RqLive(final InputStream input) throws IOException {
super(RqLive.parse(input));
}
/**
* Parse input stream.
* @param input Input stream
* @return Request
* @throws IOException If fails
* @checkstyle ExecutableStatementCountCheck (100 lines)
*/
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
private static Request parse(final InputStream input) throws IOException {
boolean eof = true;
final Collection<String> head = new LinkedList<>();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Opt<Integer> data = new Opt.Empty<>();
data = RqLive.data(input, data, false);
while (data.get() > 0) {
eof = false;
if (data.get() == '\r') {
RqLive.checkLineFeed(input, baos, head.size() + 1);
if (baos.size() == 0) {
break;
}
data = new Opt.Single<>(input.read());
final Opt<String> header = RqLive.newHeader(data, baos);
if (header.has()) {
head.add(header.get());
}
data = RqLive.data(input, data, false);
continue;
}
baos.write(RqLive.legalCharacter(data, baos, head.size() + 1));
data = RqLive.data(input, new Opt.Empty<Integer>(), true);
}
if (eof) {
throw new IOException("empty request");
}
return new Request() {
@Override
public Iterable<String> head() {
return head;
}
@Override
public InputStream body() {
return input;
}
};
}
/**
* Checks whether or not the next byte to read is a Line Feed.
* <p><i>Please note that this method assumes that the previous byte read
* was a Carriage Return.</i>
* @param input The input stream to read
* @param baos Current read header
* @param position Header line number
* @throws IOException If the next byte is not a Line Feed as expected
*/
private static void checkLineFeed(final InputStream input,
final ByteArrayOutputStream baos, final Integer position)
throws IOException {
if (input.read() != '\n') {
throw new HttpException(
HttpURLConnection.HTTP_BAD_REQUEST,
String.format(
"there is no LF after CR in header, line #%d: \"%s\"",
position,
new Utf8String(baos.toByteArray()).string()
)
);
}
}
/**
* Builds current read header.
* @param data Current read character
* @param baos Current read header
* @return Read header
*/
private static Opt<String> newHeader(final Opt<Integer> data,
final ByteArrayOutputStream baos) {
Opt<String> header = new Opt.Empty<>();
if (data.get() != ' ' && data.get() != '\t') {
header = new Opt.Single<>(
new Utf8String(baos.toByteArray()).string()
);
baos.reset();
}
return header;
}
/**
* Returns a legal character based n the read character.
* @param data Character read
* @param baos Byte stream containing read header
* @param position Header line number
* @return A legal character
* @throws HttpException if character is illegal
*/
private static Integer legalCharacter(final Opt<Integer> data,
final ByteArrayOutputStream baos, final Integer position)
throws HttpException {
// @checkstyle MagicNumber (1 line)
if ((data.get() > 0x7f || data.get() < 0x20)
&& data.get() != '\t') {
throw new HttpException(
HttpURLConnection.HTTP_BAD_REQUEST,
String.format(
"illegal character 0x%02X in HTTP header line #%d: \"%s\"",
data.get(),
position,
new Utf8String(baos.toByteArray()).string()
)
);
}
return data.get();
}
/**
* Obtains new byte if hasn't.
* @param input Stream
* @param data Empty or current data
* @param available Indicates whether or not it should check first if there
* are available bytes
* @return Next or current data
* @throws IOException if input.read() fails
*/
private static Opt<Integer> data(final InputStream input,
final Opt<Integer> data, final boolean available) throws IOException {
final Opt<Integer> ret;
if (data.has()) {
ret = data;
} else if (available && input.available() <= 0) {
ret = new Opt.Single<>(-1);
} else {
ret = new Opt.Single<>(input.read());
}
return ret;
}
}