com.jolira.testing.CachingRESTProxy.java Source code

Java tutorial

Introduction

Here is the source code for com.jolira.testing.CachingRESTProxy.java

Source

/**
 * Copyright (c) 2010 jolira. All rights reserved. This program and the accompanying materials are made available under
 * the terms of the GNU Public License 2.0 which is available at http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 */

package com.jolira.testing;

import static com.jolira.testing.StaticWebContentServer.DEFAULT_MIME_TYPE;
import static com.jolira.testing.StaticWebContentServer.mimeTypeByExtension;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_OK;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Map.Entry;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.Parser;

/**
 * REST has a number of wonderful characteristics. One of which is that it can be cached very easily. This class allows
 * for easy caching. This class is such proxy that employs caching for backend systems. The backend is only called if
 * the entry is not found in the cache.
 * 
 * @author jfk
 * @date Aug 1, 2010 9:12:44 PM
 * @since 1.0
 */
public class CachingRESTProxy {
    static abstract class CachedResponse {
        abstract String getContentType();

        abstract Cookie[] getCookies();

        abstract File getResource();

        abstract int getStatus();
    }

    static final String SET_COOKIE = "Set-Cookie";
    private static final String ENCODING = System.getProperty("file.encoding");
    private static final String STATUS_PROPERTY = "status";
    private static final String CONTENT_TYPE = "Content-Type";
    private static final String CACHE = "cache";
    private static final String SERVER = "server";
    private static final String USE_SSL = "ssl";
    private static final String HELP = "help";

    private static String getCookieKey(final int idx) {
        return SET_COOKIE + '.' + idx;
    }

    /**
     * @param query
     * @return the default content type for the query
     */
    protected final static String getDefaultContentType(final File query) {
        final String name = query.getName();
        final int dot = name.lastIndexOf('.');
        final String ext = dot == -1 ? null : name.substring(dot);

        return ext == null ? DEFAULT_MIME_TYPE : mimeTypeByExtension.get(ext);
    }

    private static File getPropertiesFile(final File query) {
        return new File(query, ".prp");
    }

    private static String getQuery(final String target, final HttpServletRequest request)
            throws UnsupportedEncodingException {
        final StringBuilder buf = new StringBuilder();

        buf.append(target);

        final Map<String, String[]> params = getSortedParameters(request);
        final Set<Entry<String, String[]>> entries = params.entrySet();
        boolean first = true;

        for (final Entry<String, String[]> entry : entries) {
            final String name = entry.getKey();
            final String[] vals = entry.getValue();

            for (final String val : vals) {
                final String encoded = URLEncoder.encode(val, ENCODING);

                if (first) {
                    first = false;
                    buf.append('?');
                } else {
                    buf.append('&');
                }

                buf.append(name);
                buf.append('=');
                buf.append(encoded);
            }
        }

        return buf.toString();
    }

    private static File getResourceFile(final File query) {
        return new File(query, ".dmp");
    }

    private static Map<String, String[]> getSortedParameters(final HttpServletRequest request) {
        final Map<String, String[]> params = new TreeMap<String, String[]>();
        @SuppressWarnings("unchecked")
        final Enumeration<String> names = request.getParameterNames();

        if (names == null) {
            return params;
        }

        while (names.hasMoreElements()) {
            final String name = names.nextElement();
            final String[] vals = request.getParameterValues(name);

            params.put(name, vals);
        }

        return params;
    }

    /**
     * @param args
     * @throws Exception
     */
    public static void main(final String[] args) throws Exception {
        final Parser parser = new GnuParser();
        final HelpFormatter formatter = new HelpFormatter();
        final Options options = new Options();

        options.addOption("c", CACHE, true, "chache directory (mandatory!)");
        options.addOption("s", SERVER, true, "server name and port number as server:port");
        options.addOption("x", USE_SSL, false, "use ssl");
        options.addOption("?", HELP, false, "display help");

        final CommandLine cli = parser.parse(options, args);
        final String server = cli.getOptionValue(SERVER);
        final String cache = cli.getOptionValue(CACHE);
        final boolean ssl = cli.hasOption(USE_SSL);

        if (cli.hasOption(HELP) || cache == null) {
            formatter.printHelp(CachingRESTProxy.class.getName(), options);
            return;
        }

        final CachingRESTProxy proxy = new CachingRESTProxy(ssl, server, new File(cache));

        proxy.start();
    }

    private final WebServerEmulator server;
    private final File cache;

    private final boolean ssl;

    private final String backend;

    /**
     * Create a new proxy.
     * 
     * @param ssl
     *            {@literal true} to indicate that the server uses SSL.
     * @param server
     *            the server name and password
     * @param cache
     *            the cache
     */
    public CachingRESTProxy(final boolean ssl, final String server, final File cache) {
        backend = server;
        this.ssl = ssl;
        this.cache = cache;
        this.server = ssl ? new SecureWebServerEmulator() {
            @Override
            protected void handle(final String target, final HttpServletRequest request,
                    final HttpServletResponse response) throws IOException, ServletException {
                CachingRESTProxy.this.handle(target, request, response);
            }
        } : new WebServerEmulator() {
            @Override
            protected void handle(final String target, final HttpServletRequest request,
                    final HttpServletResponse response) throws IOException, ServletException {
                CachingRESTProxy.this.handle(target, request, response);
            }
        };
    }

    private void cacheResponse(final File queryDir, final HttpURLConnection connection, final InputStream in)
            throws IOException, FileNotFoundException {
        final String contentType = connection.getHeaderField(CONTENT_TYPE);
        final int code = connection.getResponseCode();
        final String defaultContentType = getDefaultContentType(queryDir);
        final Map<String, List<String>> fields = connection.getHeaderFields();
        final Collection<String> cookies = fields.get(SET_COOKIE);
        final boolean simple = code == SC_OK && equalsContentType(defaultContentType, contentType)
                && isEmpty(cookies);
        final File resourceFile = simple ? queryDir : getResourceFile(queryDir);

        copy(in, resourceFile);

        if (simple) {
            return;
        }

        final Properties prps = new Properties();

        if (contentType != null) {
            prps.put(CONTENT_TYPE, contentType);
        }

        if (cookies != null) {
            int idx = 0;

            for (final String cookie : cookies) {
                final String key = getCookieKey(idx++);

                prps.put(key, cookie);
            }
        }

        prps.put(STATUS_PROPERTY, Integer.toString(code));

        final File propertiesFile = getPropertiesFile(queryDir);
        final OutputStream out = new FileOutputStream(propertiesFile);

        try {
            prps.store(out, "gerated by " + CachingRESTProxy.class);
        } finally {
            out.close();
        }
    }

    private boolean cacheResponse(final String query, final File queryDir, final HttpServletRequest request)
            throws IOException {
        if (backend == null) {
            return false;
        }

        final String protocol = ssl ? "https" : "http";
        final String _url = protocol + "://" + backend + query;
        final URL url = new URL(_url);
        final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        final Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            final StringBuilder cookieVal = new StringBuilder();

            for (final Cookie cookie : cookies) {
                final String value = cookie.getValue();
                final String name = cookie.getName();

                cookieVal.append(name);
                cookieVal.append('=');
                cookieVal.append(value);
                cookieVal.append(';');
            }

            connection.setRequestProperty("Cookie", cookieVal.toString());
        }

        final InputStream in = connection.getInputStream();

        try {
            cacheResponse(queryDir, connection, in);
        } finally {
            in.close();
        }

        return true;
    }

    private void copy(final InputStream in, final File resource) throws IOException {
        final File dir = resource.getParentFile();

        if (!dir.exists()) {
            dir.mkdirs();
        }

        final OutputStream out = new BufferedOutputStream(new FileOutputStream(resource));

        try {
            final byte[] buffer = new byte[65535];

            for (;;) {
                final int read = in.read(buffer);

                if (read == -1) {
                    break;
                }

                out.write(buffer, 0, read);
            }
        } finally {
            out.close();
        }
    }

    private boolean equalsContentType(final String type1, final String type2) {
        if (type1 == null) {
            return type2 == null;
        }

        final String _type1 = getBaseContentType(type1);
        final String _type2 = getBaseContentType(type2);

        return _type1.equals(_type2);
    }

    private String getBaseContentType(final String type) {
        final int idx = type.indexOf(';');

        if (idx == -1) {
            return type;
        }

        return type.substring(0, idx);
    }

    private CachedResponse getCached(final File query) throws IOException {
        if (!query.exists()) {
            return null;
        }

        if (query.isFile()) {
            return getSimpleResponse(query);
        }

        final Properties prps = new Properties();
        final File propertiesFile = getPropertiesFile(query);

        if (!propertiesFile.exists()) {
            return null;
        }

        final File resourceFile = getResourceFile(query);
        final InputStream in = new FileInputStream(propertiesFile);

        try {
            prps.load(in);
        } finally {
            in.close();
        }

        return new CachedResponse() {
            @Override
            String getContentType() {
                return prps.getProperty(CONTENT_TYPE);
            }

            @Override
            Cookie[] getCookies() {
                return CachingRESTProxy.this.getCookies(prps);
            }

            @Override
            File getResource() {
                return resourceFile;
            }

            @Override
            int getStatus() {
                final String status = prps.getProperty(STATUS_PROPERTY);

                return Integer.parseInt(status);
            }
        };
    }

    /**
     * @param prps
     * @return the cookies stored in the properties file
     */
    protected final Cookie[] getCookies(final Properties prps) {
        final Collection<Cookie> cookies = new ArrayList<Cookie>();

        for (int idx = 0;; idx++) {
            final String key = getCookieKey(idx);
            final String value = prps.getProperty(key);

            if (value == null) {
                break;
            }

            final Cookie cookie = parseCookie(value);

            cookies.add(cookie);
        }

        final int size = cookies.size();

        return cookies.toArray(new Cookie[size]);
    }

    private File getDirectory(final String query) {
        File file = cache;
        final StringTokenizer izer = new StringTokenizer(query, "/");

        while (izer.hasMoreTokens()) {
            final String token = izer.nextToken();

            file = new File(file, token);
        }

        return file;
    }

    /**
     * @return the host name
     * @see WebServerEmulator#getHostName()
     */
    public String getHostName() {
        return server.getHostName();
    }

    /**
     * @return the server name
     * @see WebServerEmulator#getName()
     */
    public String getName() {
        return server.getName();
    }

    /**
     * @return the listen port
     * @see WebServerEmulator#getPort()
     */
    public int getPort() {
        return server.getPort();
    }

    private CachedResponse getSimpleResponse(final File query) {
        return new CachedResponse() {
            @Override
            String getContentType() {
                return getDefaultContentType(query);
            }

            @Override
            Cookie[] getCookies() {
                return null;
            }

            @Override
            File getResource() {
                return query;
            }

            @Override
            int getStatus() {
                return SC_OK;
            }
        };
    }

    /**
     * @param target
     * @param request
     * @param response
     * @throws IOException
     */
    protected void handle(final String target, final HttpServletRequest request, final HttpServletResponse response)
            throws IOException {
        final String query = getQuery(target, request);
        final File queryDir = getDirectory(query);

        while (!handleCachedResponse(queryDir, response)) {
            if (!cacheResponse(query, queryDir, request)) {
                response.setStatus(SC_NOT_FOUND);
                return;
            }
        }
    }

    private boolean handleCachedResponse(final File query, final HttpServletResponse response) throws IOException {
        final CachedResponse cached = getCached(query);

        if (cached == null) {
            return false;
        }

        final int status = cached.getStatus();
        final String mimeType = cached.getContentType();
        final File resource = cached.getResource();
        final Cookie[] cookies = cached.getCookies();

        if (cookies != null) {
            for (final Cookie cookie : cookies) {
                response.addCookie(cookie);
            }
        }
        response.setStatus(status);
        respond(mimeType, response, resource);

        return true;
    }

    private boolean isEmpty(final Collection<String> cookies) {
        if (cookies == null) {
            return true;
        }

        final int size = cookies.size();

        return size == 0;
    }

    private Cookie parseCookie(final String value) {
        final StringTokenizer izer = new StringTokenizer(value, ";");
        final String _value = izer.nextToken();
        final int pos = _value.indexOf('=');
        final String name = _value.substring(0, pos);
        final String val = _value.substring(pos + 1);
        final Cookie cookie = new Cookie(name, val);

        while (izer.hasMoreTokens()) {
            final String token = izer.nextToken();
            final int _pos = token.indexOf('=');
            final String directive = _pos == -1 ? token : token.substring(0, _pos);
            final String _val = _pos == -1 ? null : token.substring(_pos + 1);

            if ("Domain".equalsIgnoreCase(directive)) {
                cookie.setDomain(_val);
            } else if ("Secure".equalsIgnoreCase(directive)) {
                cookie.setSecure(true);
            } else if ("Path".equalsIgnoreCase(directive)) {
                cookie.setPath(_val);
            }

        }

        return cookie;
    }

    /**
     * @param mimeType
     * @param response
     * @param resource
     * @throws IOException
     * @see WebServerEmulator#respond(java.lang.String, javax.servlet.http.HttpServletResponse, java.io.File)
     */
    protected void respond(final String mimeType, final HttpServletResponse response, final File resource)
            throws IOException {
        server.respond(mimeType, response, resource);
    }

    /**
     * @return the port number
     * @throws Exception
     * @see WebServerEmulator#start()
     */
    public int start() throws Exception {
        return server.start();
    }

    /**
     * @throws Exception
     * @see WebServerEmulator#stop()
     */
    public void stop() throws Exception {
        server.stop();
    }
}