org.eclipse.oomph.internal.util.HTTPServer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.oomph.internal.util.HTTPServer.java

Source

/*
 * Copyright (c) 2014, 2015 Eike Stepper (Berlin, Germany) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Eike Stepper - initial API and implementation
 */
package org.eclipse.oomph.internal.util;

import org.eclipse.oomph.util.HexUtil;
import org.eclipse.oomph.util.IOExceptionWithCause;
import org.eclipse.oomph.util.IOUtil;
import org.eclipse.oomph.util.OomphPlugin.BundleFile;
import org.eclipse.oomph.util.StringUtil;

import org.eclipse.core.runtime.Platform;

import org.osgi.framework.Bundle;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Eike Stepper
 */
public final class HTTPServer {
    private static final boolean DEBUG = false;

    private static final boolean DEBUG_REQUEST = false;

    private static final boolean DEBUG_RESPONSE = false;

    private static final Map<String, String> CONTENT_TYPES = new HashMap<String, String>();

    private static final ImageContext IMAGE_CONTEXT = new ImageContext();

    private static final String PATH_SEPARATOR = "/";

    private static final String STATUS_OK = "200 OK";

    private static final String STATUS_SEE_OTHER = "303 See Other";

    private static final String STATUS_BAD_REQUEST = "400 Bad Request";

    private static final String STATUS_FORBIDDEN = "403 Forbidden";

    private static final String STATUS_NOT_FOUND = "404 Not Found";

    private static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error";

    private static final String STATUS_NOT_IMPLEMENTED = "501 Not Implemented";

    private final List<Context> contexts = new ArrayList<Context>();

    private final ExecutorService threadPool = Executors.newCachedThreadPool();

    private Acceptor acceptor;

    public HTTPServer() throws IOException {
        this(15000, 50000);
    }

    public HTTPServer(int minPort, int maxPort) throws IOException {
        addContext(IMAGE_CONTEXT);

        for (int port = minPort; port <= maxPort; port++) {
            try {
                ServerSocket serverSocket = new ServerSocket(port, 500);
                acceptor = new Acceptor(serverSocket);
                return;
            } catch (BindException ex) {
                // Try next port.
            } catch (InterruptedException ex) {
                throw new IOExceptionWithCause("Start interrupted", ex);
            }
        }

        throw new IOException("No port available between " + minPort + " and " + maxPort);
    }

    public int getPort() {
        if (acceptor != null) {
            return acceptor.getPort();
        }

        return 0;
    }

    public String getURL() {
        int port = acceptor != null ? acceptor.getPort() : 0;
        return "http://127.0.0.1:" + port;
    }

    public synchronized void addContext(Context context) {
        contexts.add(context);
        Collections.sort(contexts);
    }

    public synchronized void removeContext(Context context) {
        contexts.remove(context);
    }

    public synchronized Context getContext(String path) {
        for (Context context : contexts) {
            if (path.startsWith(context.getPath())) {
                return context;
            }
        }

        return null;
    }

    public void stop() throws IOException {
        if (acceptor != null) {
            acceptor.interrupt();
            threadPool.shutdown();
        }
    }

    @Override
    public String toString() {
        return getURL();
    }

    private static void registerContentType(String contentType, String... extensions) {
        for (String extension : extensions) {
            CONTENT_TYPES.put(extension, contentType);
        }
    }

    static {
        registerContentType("application/java-archive", "jar");
        registerContentType("application/javascript", "js");
        registerContentType("application/json", "json");
        registerContentType("application/jsonml+json", "jsonml");
        registerContentType("application/pdf", "pdf");
        registerContentType("application/xaml+xml", "xaml");
        registerContentType("application/xhtml+xml", "xhtml", "xht");
        registerContentType("application/xml", "xml", "xsl");
        registerContentType("application/xml-dtd", "dtd");
        registerContentType("application/xslt+xml", "xslt");
        registerContentType("application/zip", "zip");

        registerContentType("image/bmp", "bmp");
        registerContentType("image/gif", "gif");
        registerContentType("image/jpeg", "jpeg", "jpg", "jpe");
        registerContentType("image/png", "png");
        registerContentType("image/svg+xml", "svg", "svgz");
        registerContentType("image/tiff", "tiff", "tif");
        registerContentType("image/x-icon", "ico");

        registerContentType("text/css", "css");
        registerContentType("text/html", "html", "htm");
        registerContentType("text/plain", "txt", "text", "conf", "def", "list", "log", "in");
        registerContentType("text/x-java-source", "java");
    }

    public static void main(String[] args) throws Exception {
        HTTPServer server = new HTTPServer(80, 100);
        server.addContext(new FileContext("/file/c", true, new File("C:")));
        server.addContext(new FileContext("/file/e", true, new File("E:")));

        System.out.println("http://localhost:" + server.getPort());
        System.out.println();
        while (System.in.available() == 0) {
            Thread.sleep(50);
        }

        server.stop();
    }

    /**
     * @author Eike Stepper
     */
    private final class Acceptor extends Thread {
        private final ServerSocket serverSocket;

        private final CountDownLatch started = new CountDownLatch(1);

        public Acceptor(ServerSocket serverSocket) throws InterruptedException {
            super("Httpd");
            this.serverSocket = serverSocket;
            setDaemon(true);

            start();
            started.await();
        }

        public int getPort() {
            return serverSocket.getLocalPort();
        }

        @Override
        public void run() {
            started.countDown();

            while (true) {
                try {
                    Socket socket = serverSocket.accept();
                    RequestHandler requestHandler = new RequestHandler(socket);
                    threadPool.execute(requestHandler);
                } catch (Exception ex) {
                    if (interrupted()) {
                        return;
                    }

                    UtilPlugin.INSTANCE.log(ex);
                }
            }
        }

        @Override
        public void interrupt() {
            try {
                super.interrupt();
            } finally {
                IOUtil.closeSilent(serverSocket);
            }
        }
    }

    /**
     * @author Eike Stepper
     */
    private final class RequestHandler implements Runnable {
        private final Socket socket;

        public RequestHandler(Socket socket) {
            this.socket = socket;
        }

        public void run() {
            InputStream inputStream = null;
            OutputStream outputStream = null;

            try {
                try {
                    inputStream = socket.getInputStream();
                    outputStream = socket.getOutputStream();
                } catch (Exception ex) {
                    if (isBadState()) {
                        return;
                    }

                    UtilPlugin.INSTANCE.log(ex);
                }

                if (inputStream != null && outputStream != null) {
                    BufferedReader input = new BufferedReader(new InputStreamReader(inputStream));
                    DataOutputStream output = new DataOutputStream(outputStream);

                    try {
                        handleRequest(input, output);
                    } catch (Exception ex) {
                        if (isBadState()) {
                            return;
                        }

                        if (DEBUG || !(ex instanceof SocketException)) {
                            UtilPlugin.INSTANCE.log(ex);
                        }

                        Context.sendResponse(output, STATUS_INTERNAL_SERVER_ERROR, null, 0, true);
                    }

                    try {
                        output.flush();
                    } catch (IOException ex) {
                        if (DEBUG) {
                            UtilPlugin.INSTANCE.log(ex);
                        }
                    }
                }
            } finally {
                IOUtil.closeSilent(outputStream);
                IOUtil.closeSilent(inputStream);
                IOUtil.closeSilent(socket);
            }
        }

        private void handleRequest(BufferedReader input, DataOutputStream output) throws Exception {
            String line = input.readLine();
            if (line == null) {
                return;
            }

            if (DEBUG_REQUEST) {
                System.out.println();
                String l = line;
                while (!StringUtil.isEmpty(l)) {
                    System.out.println(l);
                    l = input.readLine();
                }
            }

            line = line.replace(" ", " ");
            String[] tokens = line.split(" ");
            if (tokens.length < 2) {
                Context.sendResponse(output, STATUS_BAD_REQUEST, null, 0, false);
                return;
            }

            String method = tokens[0];
            boolean head = "HEAD".equalsIgnoreCase(method);

            if (!head && !"GET".equalsIgnoreCase(method)) {
                Context.sendResponse(output, STATUS_NOT_IMPLEMENTED, null, 0, false);
                return;
            }

            URI uri = new URI("xxx:" + tokens[1]);
            String path = Context.decodePath(uri.getPath());

            Context context = getContext(path);
            if (context == null) {
                if (PATH_SEPARATOR.equals(path)) {
                    Context.sendResponse(output, STATUS_OK, "index.html", 0, false);

                    for (Context c : contexts) {
                        if (c != IMAGE_CONTEXT) {
                            String href = c.getPath();
                            output.writeBytes("<img src=\"" + ImageContext.CONTEXT_PATH + ImageContext.NAME_CONTEXT
                                    + "\" valign=\"middle\"/> <a href=\"" + href + PATH_SEPARATOR + "\">" + href
                                    + "</a><br>\r\n");
                        }
                    }

                    return;
                }

                Context.sendResponse(output, STATUS_NOT_FOUND, null, 0, false);
                return;
            }

            path = path.substring(context.getPath().length());
            if (!path.startsWith(PATH_SEPARATOR)) {
                path = PATH_SEPARATOR + path;
            }

            if (DEBUG) {
                System.out.println(context + " " + path);
            }

            context.handleRequest(path, output, !head);
        }

        private boolean isBadState() {
            return socket.isClosed() || !socket.isConnected() || socket.isInputShutdown()
                    || socket.isOutputShutdown();
        }
    }

    /**
     * @author Eike Stepper
     */
    public static abstract class Context implements Comparable<Context> {
        private static final String URL_ENCODING = "UTF-8";

        private final String path;

        private final boolean allowDirectory;

        protected Context(String path, boolean allowDirectory) {
            if (!path.startsWith(PATH_SEPARATOR)) {
                throw new IllegalArgumentException("Path must start with a slash: " + path);
            }

            this.path = path;
            this.allowDirectory = allowDirectory;
        }

        public final String getPath() {
            return path;
        }

        public final boolean isAllowDirectory() {
            return allowDirectory;
        }

        public final int compareTo(Context o) {
            return o.path.length() - path.length();
        }

        public Object getRoot() {
            return null;
        }

        public String getURL(HTTPServer server) {
            return server.getURL() + path;
        }

        @Override
        public final String toString() {
            String string = getClass().getSimpleName() + "[" + path;
            Object root = getRoot();
            if (root != null) {
                string += " --> " + root;
            }

            return string + "]";
        }

        protected void handleRequest(String path, DataOutputStream output, boolean responseBody)
                throws IOException {
            if (isDirectory(path)) {
                if (!allowDirectory) {
                    Context.sendResponse(output, STATUS_FORBIDDEN, null, 0, false);
                } else {
                    if (!path.endsWith(PATH_SEPARATOR)) {
                        path += PATH_SEPARATOR;
                        Context.sendResponse(output, STATUS_SEE_OTHER, path, 0, false);
                    } else {
                        Context.sendResponse(output, STATUS_OK, "index.html", 0, false);
                    }

                    if (path.length() > 1) {
                        output.writeBytes("<img src=\"" + ImageContext.CONTEXT_PATH + ImageContext.NAME_FOLDER_UP
                                + "\" valign=\"middle\"/> <a href=\"../\">..</a><br>\r\n");
                    }

                    String[] children = getChildren(path);
                    if (children != null) {
                        final String finalPath = path;
                        Arrays.sort(children, new Comparator<String>() {
                            public int compare(String n1, String n2) {
                                int t1 = getType(n1);
                                int t2 = getType(n2);

                                int result = t1 - t2;
                                if (result == 0) {
                                    result = StringUtil.safe(n1).toLowerCase()
                                            .compareTo(StringUtil.safe(n2).toLowerCase());
                                }

                                return result;
                            }

                            private int getType(String child) {
                                return isDirectory(finalPath + child) ? 1 : 2;
                            }
                        });

                        for (String child : children) {
                            boolean directory = isDirectory(path + child);
                            output.writeBytes("<img src=\"" + ImageContext.CONTEXT_PATH
                                    + (directory ? ImageContext.NAME_FOLDER : ImageContext.NAME_FILE)
                                    + "\" valign=\"middle\"/> ");

                            String trailingSlash = directory ? PATH_SEPARATOR : "";
                            output.writeBytes("<a href=\"" + encodePath(child) + trailingSlash + "\">" + child
                                    + trailingSlash + "</a><br>\r\n");
                        }
                    }
                }

                return;
            }

            if (!isFile(path)) {
                Context.sendResponse(output, STATUS_NOT_FOUND, null, 0, false);
                return;
            }

            long lastModified = getLastModified(path);
            Context.sendResponse(output, STATUS_OK, path, lastModified, false);

            if (responseBody) {
                InputStream stream = null;

                try {
                    stream = getContents(path);
                    IOUtil.copy(stream, output);
                } finally {
                    IOUtil.close(stream);
                }
            }
        }

        protected abstract boolean isDirectory(String path);

        protected abstract boolean isFile(String path);

        protected abstract String[] getChildren(String path) throws IOException;

        protected abstract InputStream getContents(String path) throws IOException;

        protected long getLastModified(String path) throws IOException {
            return System.currentTimeMillis();
        }

        protected static String encodePath(String path) throws UnsupportedEncodingException {
            StringBuilder builder = new StringBuilder();
            StringTokenizer tokenizer = new StringTokenizer(path, PATH_SEPARATOR);
            while (tokenizer.hasMoreTokens()) {
                if (builder.length() != 0) {
                    builder.append(PATH_SEPARATOR);
                }

                String token = tokenizer.nextToken();
                builder.append(URLEncoder.encode(token, URL_ENCODING));
            }

            return builder.toString();
        }

        protected static String decodePath(String path) throws UnsupportedEncodingException {
            return URLDecoder.decode(path, URL_ENCODING);
        }

        @SuppressWarnings({ "deprecation", "restriction" })
        protected static String formatDate(long lastModified) {
            return org.apache.http.impl.cookie.DateUtils.formatDate(new Date(lastModified));
        }

        protected static void sendResponse(DataOutputStream output, String status, String fileName,
                long lastModified, boolean ignoreExceptions) {
            try {
                output.writeBytes("HTTP/1.0 ");
                output.writeBytes(status);
                output.writeBytes("\r\nConnection: close\r\nServer: ");
                output.writeBytes(HTTPServer.class.getName());
                output.writeBytes("\r\n");

                String location = null;
                if (status == STATUS_SEE_OTHER) {
                    location = fileName;
                    fileName = "index.html";
                }

                int lastDot = fileName == null ? -1 : fileName.lastIndexOf('.');
                String extension = lastDot == -1 ? "txt" : fileName.substring(lastDot + 1);

                String contentType = CONTENT_TYPES.get(extension);
                if (contentType == null) {
                    contentType = CONTENT_TYPES.get("txt");
                }

                output.writeBytes("Content-Type: ");
                output.writeBytes(contentType);
                output.writeBytes("\r\n");

                if (lastModified != 0) {
                    output.writeBytes("Last-Modified: ");
                    output.writeBytes(formatDate(lastModified));
                    output.writeBytes("\r\n");
                }

                if (location != null) {
                    output.writeBytes("Location: /file/c");
                    output.writeBytes(location);
                    output.writeBytes("\r\n");
                    output.writeBytes("\r\n");
                    return;
                }

                output.writeBytes("\r\n");

                if (status == STATUS_OK) {
                    return;
                }

                output.writeBytes(status);
                output.writeBytes("\r\n");
            } catch (IOException ex) {
                if (ignoreExceptions) {
                    return;
                }

                if (ex instanceof SocketException
                        && ex.getMessage().equals("Software caused connection abort: socket write error")) {
                    return;
                }

                UtilPlugin.INSTANCE.log(ex);
            }
        }
    }

    /**
     * @author Eike Stepper
     */
    public static class ImageContext extends Context {
        public static final String CONTEXT_PATH = PATH_SEPARATOR + "~";

        public static final String NAME_CONTEXT = PATH_SEPARATOR + "context";

        public static final String NAME_FOLDER_UP = PATH_SEPARATOR + "folderup";

        public static final String NAME_FOLDER = PATH_SEPARATOR + "folder";

        public static final String NAME_FILE = PATH_SEPARATOR + "file";

        private static final byte[] ICON_CONTEXT = HexUtil.hexToBytes(
                "47494638396110001000e600002d2b4f3c4967f2f6ff3c4a672e4162304262394a68002e77002b723146672f4363344a6d304362384d6f374c6d384c6d394b6a384a683d5070b3bdcd002f77003177344a6c324767304463354b6c384e6eb2bdcdb4bfceb4bcc7b3bfceb8c3d0246ebd2770be2870be2971be4e69874f69874f6a87b4bdc7f2f8ff256fbd2973c14e6c8af1f8fff3f9fff7fbff4e6d8aeef7fff0f8fff4faffeff8fff1f9fff6fbff448fc3448ec2448fc2f2fafff4fbfff7fcff529bbf519abe579cbff9fdfffcfeff539cbf549cbf579dbff8fefffdfffffeffffa1c3bea2c3bea1c2bda3c3bea2c2bdd1e8e4a3c3bdcbefc2cbeec2cbecbeccedbeeaefe7e9eee5eaeee5e9ede3e6eae0eaeee3ffffc4ffffcbffffd2ffffd7ffffdcebebe2c8c498c7c498cac59aecebe2e6e3d2e6e2cfe6e2d0e5e1cfe7e3d2e5e1d0f4f0e3f3efe3ffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021f9040100006b002c00000000100010000007b3806b8283848583001f524c2a2636244169861f1a19604f62106749213e8552195c0b1b6a096a6a50013d844c600b5b161e5fa60447295d832a4e130b5a0d0e5346456a493c8326666a1c0d59120755406a6524833810175e0f12582b08450a1137832464443b54142f151d353fd2833c49322839570827562d2ec4835d2050053066e8a011830192146108f508a084c50c0c33044419a0aad090114dc61818b344c4104382d0082971a30424902853aa141408003b");

        private static final byte[] ICON_FOLDER_UP = HexUtil.hexToBytes(
                "47494638396110001000841d009c6a3c41913b4a9045ac7a44ac7a4cac825cb48a54bc9264bc926cc4a274ccaa74d4b27cdcb2745edb63e4c27c72e57f93e596ecd28cfcd27cf4da949df3a7fcda8cfce29cfce2a4aff8b7fcea9cfceab4fcf2ccfcfefcffffffffffffffffff21f904010a001f002c000000001000100000056de0278e64699ee7b2280c3a1a5ccc19ae925951e42809df93898d7023180a13408de62280082e4b0d0178b1081e108ae0c0e5220e1f4345d0c05230024bc53289800d128120e094572a39f067203938e80212120e126e230007824d81827d2505908090902e8b2e2502979a2421003b");

        private static final byte[] ICON_FOLDER = HexUtil.hexToBytes(
                "47494638396110001000c40000f8e898f8f0c8f8e8b0e8d088f0d890f8e098f8e0a0f8d888f8d078e0c078f8d890f8d080d8b070bc8532c38b36b47f32a56c24ad722bbc7f32c38536ad6c249e6627ad722f9e5f1d9e5f208f5219ffffffffffff00000000000000000000000021f9040100001b002c0000000010001000000555e0268e64699ea7a33ae84869b046b513001043c24c3c4f3681a070d8204904c8a412437a189e068b744add4414584b61cbdd0e2c1bc8e160199bcd5fd1056159b8dfeeb4286341d8ef76f9a8c2effb5b8081828021003b");

        private static final byte[] ICON_FILE = HexUtil.hexToBytes(
                "47494638396110001000a529007b744f7e754e7e764e84784c867b4c8b7c4a8d7e4a9280489b83459d85449e8544a38842a38843a98a41ad8c3fb18f3eb4903dd4b268d4b269d5b269dabd7ce0c88fdde8f7e1ebf8e4edf9e8effaecf1faedf2faeef3faf0f4fbf3f7fbf5f7fbf6f7fcf5f8fbf5f8fcf6f8fbf7f9fbf8f9fbf8f9fcf7fafcf9fafcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff21f904010a003f002c0000000010001000000669c09f10422c1285482404c56c4a2049e4c3442d9d4894c923fa73843e20d04854a1441cd18667cd6637a28c8e7c3e5f44159bfc86a3df24ee1a8182827f490819888989075106188f909005510417969797035102169d9e9e019b9fa3a14900a7a8a9005cacadae4941003b");

        private static final byte[] ICON_UNKNOWN = HexUtil.hexToBytes(
                "47494638396110001000d50000f7f8fcf3f5fbf8f9fcf5f7fcf3f5faf1f4fbeef2faf2f5fbf5f7fbebf0f9f0f4fbe5ecf8e7eef9e9eff9edf2faf4f7fce5edf9e4ecf8e8eff9ebf1faf0f4faf6f8fbe3ecf8e6eef9e5edf8ebf1f9eef3fae4edf8e6eef8eaf1faf2f6fbf1f5fadbe4eee8f0f9edf3faf0f5fa2862964476a45d89b15f8ab16a92b76c94b8799dbe95b2cca2bbd35080aa7b745081774e928048897a4c85784d9782478e7d4aa68942a287439d8445b4903db18e3eaf8d3fab8b40ffffff00000000000000000021f9040100003c002c000000001000100000068d409e70482c0e71c8a41249cc099ed0688ea88b5a053ae20e5001954860107747ac211e27526a05460c6a44dbe1c0521d022d12c16323de280a2305232624261f0a3744331a06060e852822061a3344301913131d609a09133044342112a52c2c210d120d3444311c171c0c60b017173144320b18181110bf181b1132442f16c7c8c92f442ecdcecfcd46d24541003b");

        public ImageContext() {
            super(CONTEXT_PATH, false);
        }

        @Override
        protected boolean isDirectory(String path) {
            return false;
        }

        @Override
        protected boolean isFile(String path) {
            return true;
        }

        @Override
        protected String[] getChildren(String path) throws IOException {
            return null;
        }

        @Override
        protected InputStream getContents(String path) throws IOException {
            byte[] bytes = getImage(path);
            return new ByteArrayInputStream(bytes);
        }

        private byte[] getImage(String path) {
            if (NAME_CONTEXT.equals(path)) {
                return ICON_CONTEXT;
            }

            if (NAME_FOLDER_UP.equals(path)) {
                return ICON_FOLDER_UP;
            }

            if (NAME_FOLDER.equals(path)) {
                return ICON_FOLDER;
            }

            if (NAME_FILE.equals(path)) {
                return ICON_FILE;
            }

            return ICON_UNKNOWN;
        }

        public static void main(String[] args) {
            System.out.println("private static final byte[] ICON_CONTEXT = HexUtil.hexToBytes(\""
                    + HexUtil.bytesToHex(IOUtil.readFile(new File("/develop/icons/configuration_obj.gif")))
                    + "\");");

            System.out.println("private static final byte[] ICON_FOLDER_UP = HexUtil.hexToBytes(\""
                    + HexUtil.bytesToHex(IOUtil.readFile(new File("/develop/icons/FolderUp.gif"))) + "\");");

            System.out.println("private static final byte[] ICON_FOLDER = HexUtil.hexToBytes(\""
                    + HexUtil.bytesToHex(IOUtil.readFile(new File("/develop/icons/folder7.gif"))) + "\");");

            System.out.println("private static final byte[] ICON_FILE = HexUtil.hexToBytes(\""
                    + HexUtil.bytesToHex(IOUtil.readFile(new File("/develop/icons/file.gif"))) + "\");");

            System.out.println("private static final byte[] ICON_UNKNOWN = HexUtil.hexToBytes(\""
                    + HexUtil.bytesToHex(IOUtil.readFile(new File("/develop/icons/unknown_obj.gif"))) + "\");");
        }
    }

    /**
     * @author Eike Stepper
     */
    public static class FileContext extends Context {
        private final File root;

        public FileContext(String path, boolean allowDirectory, File root) {
            super(path, allowDirectory);
            this.root = root;
        }

        @Override
        public final File getRoot() {
            return root;
        }

        @Override
        protected boolean isDirectory(String path) {
            return getFile(path).isDirectory();
        }

        @Override
        protected boolean isFile(String path) {
            return getFile(path).isFile();
        }

        @Override
        protected String[] getChildren(String path) throws IOException {
            return getFile(path).list();
        }

        @Override
        protected InputStream getContents(String path) throws IOException {
            File file = getFile(path);
            if (DEBUG_RESPONSE /* && path.endsWith(".html") */) {
                try {
                    String contents = IOUtil.readUTF8(file);
                    System.out.println(contents);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            return new FileInputStream(file);
        }

        @Override
        protected long getLastModified(String path) throws IOException {
            File file = getFile(path);
            return file.lastModified();
        }

        private File getFile(String path) {
            if (root == null) {
                return new File(path);
            }

            return new File(root, path);
        }
    }

    /**
     * @author Eike Stepper
     */
    public static class PluginContext extends Context {
        public PluginContext(String path, boolean allowDirectory) {
            super(path, allowDirectory);
        }

        @Override
        protected boolean isDirectory(String path) {
            if (path.length() == 1) {
                // The context root is a directory of the resolved plugins.
                return true;
            }

            try {
                BundleFile file = getFile(path);
                return file.isDirectory();
            } catch (FileNotFoundException ex) {
                return false;
            }
        }

        @Override
        protected boolean isFile(String path) {
            try {
                BundleFile file = getFile(path);
                return !file.isDirectory();
            } catch (FileNotFoundException ex) {
                return false;
            }
        }

        @Override
        protected String[] getChildren(String path) throws IOException {
            String[] result;
            if (path.length() == 1) {
                // The context root is a directory of the resolved plugins.
                Bundle[] bundles = UtilPlugin.INSTANCE.getBundleContext().getBundles();

                result = new String[bundles.length];
                for (int i = 0; i < bundles.length; i++) {
                    result[i] = bundles[i].getSymbolicName();
                }
            } else {
                BundleFile file = getFile(path);
                List<BundleFile> children = file.getChildren();

                result = new String[children.size()];
                for (int i = 0; i < result.length; i++) {
                    result[i] = children.get(i).getName();
                }
            }

            return result;
        }

        @Override
        protected InputStream getContents(String path) throws IOException {
            BundleFile file = getFile(path);
            if (DEBUG_RESPONSE /* && path.endsWith(".html") */) {
                try {
                    String contents = file.getContentsString();
                    System.out.println(contents);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            return file.getContents();
        }

        @Override
        protected long getLastModified(String path) throws IOException {
            BundleFile file = getFile(path);
            return file.getBundle().getLastModified();
        }

        private BundleFile getFile(String path) throws FileNotFoundException {
            String[] segments = path.split(PATH_SEPARATOR);

            Bundle bundle = Platform.getBundle(segments[1]);
            BundleFile file = new RootBundleFile(bundle);

            for (int i = 2; file != null && i < segments.length; i++) {
                String segment = segments[i];
                file = file.getChild(segment);
            }

            if (file == null) {
                throw new FileNotFoundException(path);
            }

            return file;
        }

        /**
         * @author Eike Stepper
         */
        private static class RootBundleFile extends BundleFile {
            private Bundle bundle;

            public RootBundleFile(Bundle bundle) {
                super("", true, null);
                this.bundle = bundle;
            }

            @Override
            public Bundle getBundle() {
                return bundle;
            }

            @Override
            public String getPath() {
                return "";
            }
        }
    }
}