/* * JLibs: Common Utilities for Java * Copyright (C) 2009 Santhosh Kumar T <santhosh.tekuri@gmail.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package jlibs.nio.http.msg.parser; import jlibs.nio.http.msg.*; import jlibs.nio.http.util.ContentDisposition; import jlibs.nio.http.util.USAscii; import jlibs.nio.util.Parser; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.StandardOpenOption; import static jlibs.nio.http.msg.parser.MultipartParser.State.*; import static jlibs.nio.http.util.USAscii.*; /** * @author Santhosh Kumar Tekuri * * todo: handle Content-Transfer-Encoding */ public class MultipartParser implements Parser{ public final MultipartPayload payload; private Status errorStatus; public MultipartParser(MultipartPayload payload, Status errorStatus){ this.payload = payload; this.errorStatus = errorStatus; String boundary = payload.getMediaType().getBoundary(); delimiter = ByteBuffer.allocate(4+boundary.length()+2); delimiter.put(CR); delimiter.put(LF); delimiter.put(DASH); delimiter.put(DASH); USAscii.append(delimiter, boundary); delimiter.put(CR); delimiter.put(LF); delimiter.flip(); closeDelimiter = ByteBuffer.allocate(4+boundary.length()+4); closeDelimiter.put(CR); closeDelimiter.put(LF); closeDelimiter.put(DASH); closeDelimiter.put(DASH); USAscii.append(closeDelimiter, boundary); closeDelimiter.put(DASH); closeDelimiter.put(DASH); closeDelimiter.put(CR); closeDelimiter.put(LF); closeDelimiter.flip(); state = DELIMITER; delimiter.get(); delimiter.get(); } private ByteBuffer delimiter; private ByteBuffer closeDelimiter; enum State { HEADERS, CONTENT, DELIMITER, CLOSE_DELIMITER, DRAIN } private State state; private Part part; private FileChannel fileChannel; private HeadersParser headersParser = new HeadersParser(); @Override public boolean parse(ByteBuffer buffer, boolean eof) throws IOException{ char ch; int pos = buffer.position(); while(buffer.hasRemaining()){ switch(state){ case HEADERS: if(!headersParser.parse(buffer, eof)){ if(eof) throw new EOFException(); return false; } state = CONTENT; pos = buffer.position(); if(!buffer.hasRemaining()) break; Headers headers = headersParser.getHeaders(); ContentDisposition cd = headers.getSingleValue(Message.CONTENT_DISPOSITION, ContentDisposition::new); if(cd!=null && cd.getFileName()!=null){ File file = File.createTempFile("jlibs", "upload"); part = new FilePart(file, headers); fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE); } if(part==null) part = new DefaultPart(headersParser.getHeaders()); payload.parts.add(part); case CONTENT: while(buffer.hasRemaining()){ ch = (char)buffer.get(); if(ch==CR){ state = DELIMITER; delimiter.get(); break; } } if(!buffer.hasRemaining()) break; case DELIMITER: while(buffer.hasRemaining()){ if(delimiter.position()==delimiter.capacity()-2){ ch = (char)buffer.get(); if(ch==DASH){ delimiter.clear(); state = CLOSE_DELIMITER; closeDelimiter.position(closeDelimiter.capacity()-3); break; }else buffer.position(buffer.position()-1); } if(delimiter.hasRemaining()){ if(buffer.get()!=delimiter.get()){ buffer.position(buffer.position()-1); delimiter.position(delimiter.position()-1); writeMissing(pos, buffer, delimiter); state = CONTENT; break; } }else{ writeContent(pos, buffer, delimiter); pos = buffer.position(); if(fileChannel!=null){ fileChannel.close(); fileChannel = null; } state = HEADERS; headersParser.reset(new Headers(), errorStatus); part = null; break; } } break; case CLOSE_DELIMITER: while(buffer.hasRemaining() && closeDelimiter.hasRemaining()){ if(buffer.get()!=closeDelimiter.get()){ buffer.position(buffer.position()-1); closeDelimiter.position(closeDelimiter.position()-1); writeMissing(pos, buffer, closeDelimiter); state = CONTENT; break; } } if(state==CLOSE_DELIMITER && !closeDelimiter.hasRemaining()){ writeContent(pos, buffer, closeDelimiter); if(fileChannel!=null){ fileChannel.close(); fileChannel = null; } pos = buffer.position(); state = DRAIN; }else break; case DRAIN: buffer.position(buffer.limit()); } } if(eof){ if(state!=DRAIN) throw new EOFException(); }else{ if(state==CONTENT){ ByteBuffer src = buffer.duplicate(); src.position(pos); src.limit(buffer.position()); write(src); }else if(state==DELIMITER || state==CLOSE_DELIMITER){ ByteBuffer _delimiter = state==DELIMITER ? delimiter : closeDelimiter; if(buffer.position()-pos>_delimiter.position()){ ByteBuffer src = buffer.duplicate(); src.position(pos); src.limit(buffer.position()-_delimiter.position()); write(src); } } } return eof; } private void writeMissing(int pos, ByteBuffer buffer, ByteBuffer delimiter) throws IOException{ if(buffer.position()-pos<delimiter.position()){ delimiter.limit(delimiter.position()-(buffer.position()-pos)); delimiter.position(0); write(delimiter); } delimiter.clear(); } private void writeContent(int pos, ByteBuffer buffer, ByteBuffer delimiter) throws IOException{ if(buffer.position()-pos>delimiter.capacity()){ ByteBuffer src = buffer.duplicate(); src.position(pos); src.limit(buffer.position()-delimiter.capacity()); write(src); } delimiter.clear(); } private void write(ByteBuffer src) throws IOException{ if(part==null) src.position(src.limit()); else if(part instanceof FilePart){ while(src.hasRemaining()) fileChannel.write(src); }else ((DefaultPart)part).buffers.write(src); } @Override public void cleanup(){ try{ if(fileChannel!=null){ fileChannel.close(); fileChannel = null; } }catch(IOException ex){ throw new RuntimeException(ex); } } }