/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.wink.common.internal.providers.multipart;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.wink.common.internal.CaseInsensitiveMultivaluedMap;
import org.apache.wink.common.internal.i18n.Messages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* TODO: Add the option to get the preamble
* TODO: known limitations:the headers length of a part can not exceed the buff.length
*
*/
public class MultiPartParser {
public final static String SEP = "\n"; //$NON-NLS-1$
private static final Logger logger = LoggerFactory.getLogger(MultiPartParser.class);
private final static String UTF8 = "UTF-8"; //$NON-NLS-1$
private InputStream is;
private byte[] boundaryBA;
static private byte[] boundaryDelimiterBA = "--".getBytes(); //$NON-NLS-1$
private MultivaluedMap<String, String> partHeaders;
private PartInputStream partIS;
private static int BOUNDARY_TYPE_START = 0;
private static int BOUNDARY_TYPE_END = 1;
// private static int BOUNDARY_TYPE_INVALID = 2;
private byte[] buff;
// the next byte to return
private int buffIdx = 0;
// The number of bytes that were set on the buffer (red from is);
private int buffSize = 0;
// the position of the next boundary ("--boundary"), -1 if does not exist in
// this buffer
private int boundryIdx = -1;
// The index of the byte that is suspected of been the boundary
private int saveIdx = 0;
// This is a temp array that is used to read stuff into it.
private byte[] temp = new byte[1024];
public MultiPartParser(InputStream is, String boundary) {
this.is = is;
try {
boundaryBA = ("--" + boundary).getBytes(UTF8); //$NON-NLS-1$
} catch(UnsupportedEncodingException e) {
logger.debug("Error parsing multi part: " + e.getMessage(), e);
}
// make sure to allocate a buffer that is at least double then the
// boundary length
int buffLength = Math.max(8192, boundaryBA.length * 2);
buff = new byte[buffLength];
}
/**
* This method shift the bytes to the left and update the indexes it is used
* to clear room for additional bytes to be read.
*/
private void shiftBuff() {
System.arraycopy(buff, buffIdx, buff, 0, buffSize - buffIdx);
buffSize -= buffIdx;
saveIdx -= buffIdx;
if (saveIdx < 0)
saveIdx = 0;
/*
* throw new
* RuntimeException("This should never happend, we found a bug.");
*/
boundryIdx = Math.max(-1, boundryIdx - buffIdx);
buffIdx = 0;
// for debug purposes
// Arrays.fill(buff, buffIdx, buff.length-1, (byte)0);
}
public boolean nextPart() throws IOException {
// if this is the first next just get rid of the PREAMBLE
if (partIS == null) {
partIS = new PartInputStream();
}
// clear the part/preamble bytes that were not read
digestPartStream();
if (digestBoundary() == BOUNDARY_TYPE_END)
return false;
partIS.setState(PartInputStream.STATE_NOT_ACTIVE);
partIS = new PartInputStream();
partHeaders = parseHeaders();
return partHeaders != null;
}
public InputStream getPartBodyStream() {
return partIS;
}
public MultivaluedMap<String, String> getPartHeaders() {
return partHeaders;
}
// read till end of stream (next boundary)
private void digestPartStream() throws IOException {
while (partIS.read(temp) != -1) {
}
}
private boolean compareByte(byte[] a, int aOffset, byte[] b, int bOffset, int length) {
for (int i = 0; i < length; i++) {
if (a[aOffset + i] != b[bOffset + i])
return false;
}
return true;
}
private int digestBoundary() throws IOException {
// it might be that there is a new line before the boundary
digestNewLine();
// promote pointers to the end of the boundary
buffIdx += boundaryBA.length;
saveIdx += boundaryBA.length; // DO NOT DELETE
// check if this is an end boundary
int unredBytes = verifyByteReadyForRead(2);
if (unredBytes >= 2) {
if (compareByte(buff, buffIdx, boundaryDelimiterBA, 0, boundaryDelimiterBA.length))
return BOUNDARY_TYPE_END;
}
// OK
digestNewLine();
boundryIdx = -1;
findBounderyIfNeeded();
return BOUNDARY_TYPE_START;
}
private void findBounderyIfNeeded() {
if (boundryIdx == -1) {
boundryIdx = indexOf(buff, saveIdx, buffSize, boundaryBA);
if (boundryIdx != -1) {
int nlSize = 0;
if (boundryIdx > 1) {
if (buff[boundryIdx - 2] == '\r' && buff[boundryIdx - 1] == '\n')
nlSize = 2;
else
nlSize = 1;
}
if (boundryIdx == 1) {
nlSize = 1;
}
saveIdx = boundryIdx - nlSize;
} else {
// the boundary was not found, but we can promote the save till
// boundary size + NL size
saveIdx = Math.max(saveIdx, buffSize - (boundaryBA.length + 2));
}
}
}
private int verifyByteReadyForRead(int required) throws IOException {
int unreadBytes = buffSize - buffIdx - 1;
if (unreadBytes < required) {
fetch(required - unreadBytes);
unreadBytes = buffSize - buffIdx;
}
return unreadBytes;
}
/**
* @param minmum - the minimum number of byte to insist of fetching, the
* method might fetch less only in case it get to the end of the
* stream
* @return number of bytes that were fetched, -1 if no more to fetch
* @throws IOException
*/
private int fetch(int minmum) throws IOException {
int res = 0;
int max2featch = buff.length - buffSize;
if (max2featch < minmum) {
shiftBuff();
max2featch = buff.length - buffSize;
}
while (res < minmum && max2featch > 0) {
max2featch = buff.length - buffSize;
int read = is.read(buff, buffSize, max2featch);
if (read == -1) {
if (res == 0)
return -1;
else
break;
}
res += read;
buffSize += read;
}
findBounderyIfNeeded();
return res;
}
private void digestNewLine() throws IOException {
// make sure we have enough byte to read
int unreadBytes = verifyByteReadyForRead(2);
int size = 0;
if (unreadBytes >= 2 && buff[buffIdx] == '\r' && buff[buffIdx + 1] == '\n')
size = 2;
else if (buff[buffIdx] == '\r')
size = 1;
else if (buff[buffIdx] == '\n')
size = 1;
buffIdx += size;
if (saveIdx < buffIdx)
saveIdx = buffIdx;
}
private int indexOf(byte[] ba, int start, int end, byte[] what) {
for (int i = start; i < end - what.length + 1; i++) {
// only if the first byte equals do the compare (to improve
// performance)
if (ba[i] == what[0])
if (compareByte(ba, i, what, 0, what.length))
return i;
}
return -1;
}
/**
* @return
* @throws IOException
*/
private MultivaluedMap<String, String> parseHeaders() throws IOException {
MultivaluedMap<String, String> headers = new CaseInsensitiveMultivaluedMap<String>();
String line;
do {
line = readLine();
if (line == null || line.equals("")) //$NON-NLS-1$
break;
int semIdx = line.indexOf(":"); //$NON-NLS-1$
headers.add(line.substring(0, semIdx).trim(), line.substring(semIdx + 1).trim());
} while (true);
if (saveIdx < buffIdx)
saveIdx = buffIdx;
return headers;
}
private String readLine() throws IOException {
int lineIdx = 0;
int breakeSize = 0;
while (lineIdx <= verifyByteReadyForRead(lineIdx)) {
if (buff[buffIdx + lineIdx] == '\n') {
breakeSize = 1;
break;
}
if (buff[buffIdx + lineIdx] == '\r') {
if ((verifyByteReadyForRead(lineIdx + 1) >= lineIdx + 1) && (buff[buffIdx + lineIdx
+ 1] == '\n')) {
breakeSize = 2;
break;
} else {
breakeSize = 1;
break;
}
}
lineIdx++;
}
// got to the end of input without NL
if (lineIdx == 0) {
buffIdx += breakeSize;
return null;
}
String hdr = new String(buff, buffIdx, lineIdx, UTF8);
buffIdx += lineIdx + breakeSize;
return hdr;
}
public class PartInputStream extends InputStream {
// The state of the part Stream
// 0 active
// 1 not active (the Parser already moved to the next part.)
private int state = 0;
public final static int STATE_ACTIVE = 0;
public final static int STATE_NOT_ACTIVE = 1;
public void setState(int status) {
this.state = status;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (state == STATE_NOT_ACTIVE) {
throw new IOException(Messages.getMessage("multiPartStreamAlreadyClosed")); //$NON-NLS-1$
}
int available = verifyNumOfByteToReadB4Boundary(len);
if (available < 1) {
return available;
}
int size2copy = Math.min(len, available);
System.arraycopy(buff, buffIdx, b, off, size2copy);
buffIdx += size2copy;
return size2copy;
}
@Override
public int read() throws IOException {
if (state == STATE_NOT_ACTIVE) {
throw new IOException(Messages.getMessage("multiPartStreamAlreadyClosed")); //$NON-NLS-1$
}
int i = verifyNumOfByteToReadB4Boundary(1);
if (i < 1)
return -1;
// make sure that the return value is 0 - 255
int res = buff[buffIdx] & 0xff;
if (res < 0) {
int t = 0;
t++;
}
buffIdx++;
return res;
}
/**
* @param minmum - the minimum number of byte to insist of fetching, the
* method might fetch less in case it get to the end of the
* stream or in case there minimum exceed the num of byte it
* can hold in the buffer
* @return number of bytes that were fetched, -1 if no more to fetch
* @throws IOException
*/
private int verifyNumOfByteToReadB4Boundary(int minmum) throws IOException {
int availabe = saveIdx - buffIdx;
if (availabe >= minmum)
return availabe;
//
if (saveIdx <= boundryIdx) {
if (availabe == 0)
return -1;
return availabe;
}
int fetched = fetch(minmum - availabe);
availabe = saveIdx - buffIdx;
if (availabe == 0 && fetched == -1)
return -1;
return availabe;
}
@Override
public int available() {
return saveIdx - buffIdx;
}
}
}