public class HTTPRequest extends HTTPMessage
{
    public HTTPRequest(
            String command,
            String httpVersion,
            String path,
            Map<String, String> headers,
            byte[] content)
    {
        super(httpVersion, headers, content);
        this.command = command;
        this.path = path;
    }
    
    // <editor-fold defaultstate="collapsed" desc="Builder">
    public static Builder create()
    {
        return new Builder();
    }

    public static class Builder
    {
        private String command = "GET";
        private String httpVersion = "HTTP/1.1";
        private String path = "/";
        private Map<String, String> headers = new HashMap<>();
        private byte[] content = null;
        
        public Builder setHTTPVersion(double version)
        {
            this.httpVersion = "HTTP/" + version;
            return this;
        }
        public Builder setHTTPVersion(String version)
        {
            version = version.trim();
            
            if(!version.toUpperCase().startsWith("HTTP/"))
                version = "HTTP/" + version;
            
            this.httpVersion = version;
            return this;
        }
        
        public Builder setPath(String path)
        {
            try
            {
                this.path = URLEncoder.encode(path.trim().replace("\\", "/"), "UTF-8").replace("%2F", "/").replace("+", "%20").replace("%25", "%");
            }
            catch (UnsupportedEncodingException ex)
            {
                this.path = path.trim();
            }
            return this;
        }
        public Builder setPath(FileSystemPath path)
        {
            setPath(path.toString());
            return this;
        }
        
        public Builder setContent(byte[] content)
        {
            this.content = content;
            return this;
        }
        public Builder setContent(String content)
        {
            return setContent(content, "UTF-8");
        }
        public Builder setContent(String content, String charset)
        {
            try
            {
                this.content = content.getBytes(charset);
            }
            catch(UnsupportedEncodingException ex)
            { }
            
            return this;
        }
        
        public Builder setHeaders(Map<String, String> headers)
        {
            this.headers.clear();
            addHeaders(headers);
            
            return this;
        }
        public Builder addHeaders(Map<String, String> headers)
        {
            headers.entrySet()
                    .stream()
                    .forEach(e -> this.headers.put(formatHeaderName(e.getKey()), e.getValue()));
            
            return this;
        }
        public Builder setHeader(String name, String value)
        {
            this.headers.put(formatHeaderName(name), value);
            return this;
        }
        
        public Builder setCommand(String command)
        {
            this.command = command.trim().toUpperCase();
            return this;
        }
        
        
        public HTTPRequest build()
        {
            if(content == null)
                content = new byte[0];
            
            setHeader("Content-Length", String.valueOf(content.length));
            
            return new HTTPRequest(
                    command,
                    httpVersion,
                    path,
                    headers,
                    content);
        }
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Parser">
    public static HTTPRequest parseHTTPRequest(byte[] byteMessage)
    {
        final Builder builder = HTTPRequest.create();
        
        parseHTTPMessage(byteMessage,
                headLine -> 
                {
                    builder.setCommand(headLine[0]);
                    builder.setPath(headLine[1]);
                    builder.setHTTPVersion(headLine[2]);
                },
                builder::setHeader,
                builder::setContent);
        
        return builder.build();
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Properties">
    private final String command;
    private final String path;
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Accessors">
    public String getCommand()
    {
        return command;
    }
    public String getPath()
    {
        return path;
    }
    public String getDecodedPath()
    {
        try
        {
            return URLDecoder.decode(path, "UTF-8").trim();
        }
        catch (UnsupportedEncodingException ex)
        {
            return path;
        }
    }
    // </editor-fold>
    
    @Override
    protected String getHeadLine()
    {
        return command + " " + path + " " + httpVersion;
    }
}
 public class HTTPResponse extends HTTPMessage
{
    public HTTPResponse(
            int code,
            String message,
            String httpVersion,
            Map<String, String> headers,
            byte[] content)
    {
        super(httpVersion, headers, content);
        this.code = code;
        this.message = message;
    }
    
    // <editor-fold defaultstate="collapsed" desc="Builder">
    public static Builder create()
    {
        return new Builder();
    }

    public static class Builder
    {
        private int code = 207;
        private String httpVersion = "HTTP/1.1";
        private String message = "Multi-Status";
        private Map<String, String> headers = new HashMap<>();
        private byte[] content = null;
        
        public Builder setHTTPVersion(double version)
        {
            this.httpVersion = "HTTP/" + version;
            return this;
        }
        public Builder setHTTPVersion(String version)
        {
            version = version.trim();
            
            if(!version.toUpperCase().startsWith("HTTP/"))
                version = "HTTP/" + version;
            
            this.httpVersion = version;
            return this;
        }
        
        public Builder setCode(String code)
        {
            this.code = Integer.parseInt(code);
            return this;
        }
        public Builder setCode(int code)
        {
            this.code = code;
            return this;
        }
        
        public Builder setContent(byte[] content)
        {
            this.content = content;
            return this;
        }
        public Builder setContent(String content)
        {
            return setContent(content, "UTF-8");
        }
        public Builder setContent(String content, String charset)
        {
            try
            {
                this.content = content.getBytes(charset);
            }
            catch(UnsupportedEncodingException ex)
            { }
            
            return this;
        }
        
        public Builder setHeaders(Map<String, String> headers)
        {
            this.headers.clear();
            addHeaders(headers);
            
            return this;
        }
        public Builder addHeaders(Map<String, String> headers)
        {
            headers.entrySet()
                    .stream()
                    .forEach(e -> this.headers.put(formatHeaderName(e.getKey()), e.getValue()));
            
            return this;
        }
        public Builder setHeader(String name, String value)
        {
            this.headers.put(formatHeaderName(name), value);
            return this;
        }
        
        public Builder setMessage(String message)
        {
            this.message = message.trim();
            return this;
        }
        
        
        public HTTPResponse build()
        {
            if(content == null)
                content = new byte[0];
            
            setHeader("Content-Length", String.valueOf(content.length));
            
            return new HTTPResponse(
                    code,
                    message,
                    httpVersion,
                    headers,
                    content);
        }
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Parser">
    public static HTTPResponse parseHTTPResponse(byte[] byteMessage)
    {
        final Builder builder = create();
        
        parseHTTPMessage(byteMessage,
                headLine -> 
                {
                    builder.setHTTPVersion(headLine[0]);
                    builder.setCode(headLine[1]);
                    builder.setMessage(headLine[2]);
                },
                builder::setHeader,
                builder::setContent);
        
        return builder.build();
    }
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Properties">
    private final int code;
    private final String message;
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Accessors">
    public int getCode()
    {
        return code;
    }
    public String getMessage()
    {
        return message;
    }
    // </editor-fold>
    
    @Override
    protected String getHeadLine()
    {
        return httpVersion + " " + code + " " + message;
    }
    
    
    // <editor-fold defaultstate="collapsed" desc="Code manager">
    public <T extends Throwable> HTTPResponse throwCode(int code, Supplier<T> supplier) throws T
    {
        if(this.code == code)
            throw supplier.get();
        return this;
    }
    public <T extends Throwable> HTTPResponse throwCodes(int[] codes, Supplier<T> supplier) throws T
    {
        for(int code : codes)
            if(this.code == code)
                throw supplier.get();
        return this;
    }
    // </editor-fold>
}
 public class ExtendableByteBuffer
{
    public ExtendableByteBuffer()
    { }
    
    protected int internalBufferSize = 1500;
    protected List<byte[]> content = new ArrayList<>();
    protected int totalLength = 0;
    
    
    
    public ExtendableByteBuffer setInternalBufferSize(int internalBufferSize)
    {
        this.internalBufferSize = internalBufferSize;
        return this;
    }
    
    
    
    
    
    
    
    
    
    
    public ExtendableByteBuffer write(ExtendableByteBuffer byteBuffer)
    {
        content.addAll(byteBuffer.content);
        totalLength += byteBuffer.totalLength;
        
        return this;
    }
    public ExtendableByteBuffer write(InputStream is, int stopSize) throws IOException
    {
        byte[] data = new byte[internalBufferSize];
        int len;
        while((len = is.read(data)) >= 0)
            if(len > 0)
            {
                byte[] d = new byte[len];
                System.arraycopy(data, 0, d, 0, len);
                content.add(d);
                totalLength += len;
                
                if(totalLength >= stopSize)
                    return this;
            }
        
        return this;
    }
    public ExtendableByteBuffer write(InputStream is) throws IOException
    {
        byte[] data = new byte[internalBufferSize];
        int len;
        while((len = is.read(data)) >= 0)
            if(len > 0)
            {
                byte[] d = new byte[len];
                System.arraycopy(data, 0, d, 0, len);
                content.add(d);
                totalLength += len;
            }
        
        return this;
    }
    public ExtendableByteBuffer writeOnce(InputStream is) throws IOException
    {
        byte[] data = new byte[internalBufferSize];
        int len;
        if((len = is.read(data)) >= 0)
        {
            byte[] d = new byte[len];
            System.arraycopy(data, 0, d, 0, len);
            content.add(d);
            totalLength += len;
        }
        
        return this;
    }
    public ExtendableByteBuffer write(Byte value)
    {
        write((byte)value);
        return this;
    }
    public ExtendableByteBuffer write(byte value)
    {
        content.add(new byte[] { value });
        totalLength++;
        
        return this;
    }
    public ExtendableByteBuffer write(Character value)
    {
        write((char)value);
        return this;
    }
    public ExtendableByteBuffer write(char value)
    {
        content.add(new byte[] { (byte)value });
        totalLength++;
        
        return this;
    }
    public ExtendableByteBuffer write(byte[] values, int len)
    {
        if(len == 0)
            return this;
        
        byte[] newBytes = new byte[len];
        System.arraycopy(values, 0, newBytes, 0, len);
        content.add(newBytes);
        totalLength += newBytes.length;
        
        return this;
    }
    public ExtendableByteBuffer write(byte[] values, int len, int index)
    {
        if(len == 0 || index + len >= values.length)
            return this;
        
        byte[] newBytes = new byte[len];
        System.arraycopy(values, index, newBytes, 0, len);
        content.add(newBytes);
        totalLength += newBytes.length;
        
        return this;
    }
    public ExtendableByteBuffer write(byte[] values)
    {
        content.add(values);
        totalLength += values.length;
        
        return this;
    }
    public ExtendableByteBuffer write(char[] values, int len)
    {
        if(len == 0)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(char[] values, int len, int index)
    {
        if(len == 0 || index + len >= values.length)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i + index];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(char[] values)
    {
        byte[] data = new byte[values.length];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Byte[] values, int len)
    {
        if(len == 0)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Byte[] values, int len, int index)
    {
        if(len == 0 || index + len >= values.length)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i + index];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Byte[] values)
    {
        byte[] data = new byte[values.length];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Character[] values, int len)
    {
        if(len == 0)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)(char)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Character[] values, int len, int index)
    {
        if(len == 0 || index + len >= values.length)
            return this;
        
        byte[] data = new byte[len];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)(char)values[i + index];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Character[] values)
    {
        byte[] data = new byte[values.length];
        for(int i = 0; i < data.length; i++)
            data[i] = (byte)(char)values[i];
        
        write(data);
        
        return this;
    }
    public ExtendableByteBuffer write(Iterable<Byte> values)
    {
        ArrayList<Byte> bytes = new ArrayList<>();
        values.forEach(c -> bytes.add(c));
        
        write(bytes.stream().toArray(Byte[]::new));
        
        return this;
    }
    
    
    
    
    
    
    
    
    public int length()
    {
        return totalLength;
    }
    
    
    
    
    
    
    
    
    
    
    
    public ByteBuffer toByteBuffer()
    {
        byte[] contentData = new byte[totalLength];
        int index = 0;
        for(byte[] bs : content)
        {
            System.arraycopy(bs, 0, contentData, index, bs.length);
            index += bs.length;
        }
        
        return ByteBuffer.wrap(contentData);
    }
    public byte[] toBytes()
    {
        return toByteBuffer().array();
    }
    public char[] toChars()
    {
        return toByteBuffer().asCharBuffer().array();
    }
    public double[] toDoubles()
    {
        return toByteBuffer().asDoubleBuffer().array();
    }
    public float[] toFloats()
    {
        return toByteBuffer().asFloatBuffer().array();
    }
    public int[] toInts()
    {
        return toByteBuffer().asIntBuffer().array();
    }
    public long[] toLongs()
    {
        return toByteBuffer().asLongBuffer().array();
    }
    public short[] toShorts()
    {
        return toByteBuffer().asShortBuffer().array();
    }
    @Override
    public String toString()
    {
        return new String(toBytes());
    }
    public String toString(String charset) throws UnsupportedEncodingException
    {
        return new String(toBytes(), charset);
    }
    public String toString(Charset charset)
    {
        return new String(toBytes(), charset);
    }
    public Collection<String> toLines()
    {
        return Arrays.asList(toString()
                .replace("\r\n", "\n")
                .replace("\r", "\n")
                .split("\n"));
    }
    public Collection<String> toLines(String charset) throws UnsupportedEncodingException
    {
        return Arrays.asList(toString(charset)
                .replace("\r\n", "\n")
                .replace("\r", "\n")
                .split("\n"));
    }
    public Collection<String> toLines(Charset charset)
    {
        return Arrays.asList(toString(charset)
                .replace("\r\n", "\n")
                .replace("\r", "\n")
                .split("\n"));
    }
    
    public Document toXML() throws ParserConfigurationException, SAXException, IOException
    {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        DocumentBuilder xmlBuilder = factory.newDocumentBuilder();

        ByteArrayInputStream bais = new ByteArrayInputStream(toBytes());
        return xmlBuilder.parse(bais);
    }
}
 public class AlreadyExistingException extends HTTPException
{
    public AlreadyExistingException()
    {
        super("Resource already existing");
    }
}
 public class DeadResourceException extends HTTPException
{
    public DeadResourceException()
    {
        super("This resource is dead");
    }
}
 public class UnexpectedException extends HTTPException
{
    public UnexpectedException(Throwable ex)
    {
        super("Not expected error", ex);
    }
}
 public class UnimplementedMethodException extends HTTPException
{
    public UnimplementedMethodException()
    {
        super("Command not found");
    }
}
 public class Helper 
{
    private Helper()
    { }
    
    public static String toString(Date date)
    {
        return new SimpleDateFormat("EEE, dd MMM YYYY HH:mm:ss", Locale.ENGLISH).format(date).replace(".", "") + " GMT";
    }
    
    public static String toUTF8(String str)
    {
        try
        {
            return URLEncoder.encode(str, "UTF-8").replace("%2F", "/").replace("+", " ");
        }
        catch (UnsupportedEncodingException ex)
        {
            return str;
        }
    }
    
    public static String toBase10(String base64)
    {
        return new String(DatatypeConverter.parseBase64Binary(base64.trim().substring("Basic ".length()).trim()));
    }
    
    public static String toHex(byte[] data)
    {
        BigInteger bigInt = new BigInteger(1, data);
        String value = bigInt.toString(16);
        
        while(value.length() < 32)
            value = "0" + value;
        
        return value;
    }
}
 public class NotFoundException extends HTTPException
{
    public NotFoundException()
    {
        super("Resource not found");
    }
    
    public static HTTPResponse.Builder getHTTPResponse()
    {
        return HTTPResponse.create()
                .setCode(404)
                .setMessage("Not found");
    }
}
 public class UserRequiredException extends HTTPException
{
    public UserRequiredException()
    {
        super("User required");
    }
}
 public class WrongResourceTypeException extends HTTPException
{
    public WrongResourceTypeException()
    {
        super("This resource type can't call this method.");
    }
    public WrongResourceTypeException(String description)
    {
        super(description);
    }
}
 public class HTTPEnvRequest
{
    public static Builder create()
    {
        return new Builder();
    }
    public static class Builder
    {
        public Builder()
        { }
        
        private HTTPRequest request;
        private HTTPCommand command;
        private HTTPServerSettings settings;
        private Collection<IResourceMutation> mutations = null;
        private byte[] bytesReceived = null;
        
        public Builder setRequest(HTTPRequest request)
        {
            this.request = request;
            return this;
        }
        
        public Builder setCommand(HTTPCommand command)
        {
            this.command = command;
            return this;
        }
        
        public Builder setBytesReceived(byte[] bytesReceived)
        {
            this.bytesReceived = bytesReceived;
            return this;
        }
        
        public Builder setSettings(HTTPServerSettings settings)
        {
            this.settings = settings;
            return this;
        }
        
        public Builder setMutations(Collection<IResourceMutation> mutations)
        {
            this.mutations = mutations;
            return this;
        }
        public Builder addMutations(IResourceMutation[] mutations)
        {
            this.mutations.addAll(Arrays.asList(mutations));
            return this;
        }
        public Builder addMutations(Collection<IResourceMutation> mutations)
        {
            this.mutations.addAll(mutations);
            return this;
        }
        public Builder addMutation(IResourceMutation mutation)
        {
            this.mutations.add(mutation);
            return this;
        }
        
        public HTTPEnvRequest build()
        {
            if(mutations == null)
                mutations = Collections.EMPTY_LIST;
            
            return new HTTPEnvRequest(
                    bytesReceived,
                    request,
                    command,
                    settings.getAuthenticationManager().checkAuth(request),
                    settings,
                    settings.getFileManager().getResourceMutations());
        }
    }
    
    public HTTPEnvRequest(
            byte[] bytesReceived,
            HTTPRequest request,
            HTTPCommand command,
            HTTPUser user,
            HTTPServerSettings settings,
            Collection<IResourceMutation> mutations)
    {
        this.bytesReceived = bytesReceived;
        this.request = request;
        this.command = command;
        this.user = user;
        this.settings = settings;
        this.mutations = mutations;
    }
    
    private byte[] bytesReceived;
    private FileSystemPath path = null;
    
    private final HTTPRequest request;
    private final HTTPCommand command;
    private final HTTPUser user;
    private final HTTPServerSettings settings;
    private final Collection<IResourceMutation> mutations;
    
    public FileSystemPath getPath()
    {
        if(path == null)
            path = settings.getFileSystemPathManager()
                    .createFromString(request.getDecodedPath());
        
        return path;
    }
    
    public Collection<IResourceMutation> getMutations()
    {
        return mutations;
    }
    
    public byte[] getBytesReceived()
    {
        if(bytesReceived == null)
            bytesReceived = request.toBytes();
        return bytesReceived;
    }
    
    public HTTPRequest getRequest()
    {
        return request;
    }
    
    public HTTPCommand getCommand()
    {
        return command;
    }
    
    public HTTPUser getUser()
    {
        return user;
    }
    
    public HTTPServerSettings getSettings()
    {
        return settings;
    }
}

import http.server.message.HTTPRequest;
import http.server.message.HTTPResponse;
import http.ExtendableByteBuffer;
import http.server.exceptions.AlreadyExistingException;
import http.server.exceptions.DeadResourceException;
import http.server.exceptions.UnexpectedException;
import http.server.exceptions.UnimplementedMethodException;
import webdav.server.tools.Helper;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Date;
import http.server.exceptions.NotFoundException;
import http.server.exceptions.UserRequiredException;
import http.server.exceptions.WrongResourceTypeException;
import http.server.message.HTTPEnvRequest;

package http.server;


public class HTTPServerRuntime implements Runnable
{
    private final int FULL_REQUEST_TIMEOUT = 50; // ms
    
    public HTTPServerRuntime(Socket socket, HTTPServerSettings settings) throws IOException
    {
        this.socket = socket;
        
        this.settings = settings;
        
        this.in = new BufferedInputStream(socket.getInputStream());
        this.out = new BufferedOutputStream(socket.getOutputStream());
    }
    
    private final Socket socket;
    private final BufferedInputStream in;
    private final BufferedOutputStream out;
    private final HTTPServerSettings settings;
    
    
    
    
    
    
    
    
    boolean finishedToRead = false;
    boolean firstTime = true;
    HTTPCommand cmd = null;
    HTTPRequest inputMsg = null;
    
    
    protected byte[] computeRequest(byte[] input, HTTPEnvRequest.Builder envBuilder) throws UserRequiredException, NotFoundException, UnimplementedMethodException, UnexpectedException
    {
        if(firstTime || cmd == null)
        { // First time
            inputMsg = HTTPRequest.parseHTTPRequest(input);
            
            final HTTPRequest inputMsgFinal = inputMsg;

            cmd = settings
                    .getAllowedCommands()
                    .stream()
                    .filter(c -> c.equals(inputMsgFinal.getCommand()))
                    .findFirst()
                    .orElseThrow(UnimplementedMethodException::new);

            HTTPEnvRequest env = envBuilder
                    .setRequest(inputMsg)
                    .setCommand(cmd)
                    .setBytesReceived(input)
                    .build();
            

            HTTPResponse.Builder outputMsg = settings.getRequestFilters()
