dk.netarkivet.common.distribute.HTTPRemoteFileRegistry.java Source code

Java tutorial

Introduction

Here is the source code for dk.netarkivet.common.distribute.HTTPRemoteFileRegistry.java

Source

/* $Id$
 * $Revision$
 * $Author$
 * $Date$
 *
 * The Netarchive Suite - Software to harvest and preserve websites
 * Copyright 2004-2012 The Royal Danish Library, the Danish State and
 * University Library, the National Library of France and the Austrian
 * National Library.
 *
 * 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package dk.netarkivet.common.distribute;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.handler.AbstractHandler;

import dk.netarkivet.common.exceptions.ArgumentNotValid;
import dk.netarkivet.common.exceptions.IOFailure;
import dk.netarkivet.common.utils.CleanupHook;
import dk.netarkivet.common.utils.CleanupIF;
import dk.netarkivet.common.utils.FileUtils;
import dk.netarkivet.common.utils.Settings;
import dk.netarkivet.common.utils.SystemUtils;

/**
 * This is a registry for HTTP remote file, meant for serving registered files
 * to remote hosts.
 * The embedded webserver handling remote files for HTTPRemoteFile
 * point-to-point communication. Optimised to use direct transfer on local
 * machine.
 */
public class HTTPRemoteFileRegistry implements CleanupIF {
    /** The unique instance. */
    protected static HTTPRemoteFileRegistry instance;

    /** Protocol for URLs. */
    private static final String PROTOCOL = "http";

    /** Port number for generating URLs. Read from settings. */
    protected final int port;

    /** Name of this host. Used for generating URLs. */
    private final String localHostName;

    /** Files to serve. */
    private final Map<URL, FileInfo> registeredFiles;

    /** Instance to create random URLs. */
    private final Random random;

    /** Postfix to add to an URL to get cleanup URL.
     * We are not using query string, because it behaves strangely in
     * HttpServletRequest. */
    private static final String UNREGISTER_URL_POSTFIX = "/unregister";

    /** Logger for this class. */
    private final Log log = LogFactory.getLog(getClass());
    /** The embedded webserver. */
    protected Server server;
    /** The shutdown hook. */
    private CleanupHook cleanupHook;

    /** Initialise the registry. This includes registering an HTTP server for
     * getting the files from this machine.
     * @throws IOFailure if it cannot be initialised.
     */
    protected HTTPRemoteFileRegistry() {
        port = Settings.getInt(HTTPRemoteFile.HTTPREMOTEFILE_PORT_NUMBER);
        localHostName = SystemUtils.getLocalHostName();
        registeredFiles = Collections.synchronizedMap(new HashMap<URL, FileInfo>());
        random = new Random();
        startServer();
        cleanupHook = new CleanupHook(this);
        Runtime.getRuntime().addShutdownHook(cleanupHook);
    }

    /** Start the server, including a handler that responds with registered
     * files, removes registered files on request, and gives 404 otherwise.
     * @throws IOFailure if it cannot be initialised.
     */
    protected void startServer() {
        server = new Server();
        SocketConnector connector = new SocketConnector();
        connector.setPort(port);
        server.addConnector(connector);
        server.addHandler(new HTTPRemoteFileRegistryHandler());
        try {
            server.start();
        } catch (Exception e) {
            throw new IOFailure("Cannot start HTTPRemoteFile registry on port " + port, e);
        }
    }

    /**
     * Get the protocol part of URLs, that is HTTP.
     * @return "http", the protocol.
     */
    protected String getProtocol() {
        return PROTOCOL;
    }

    /** Get the unique instance.
     * @return The unique instance.
     */
    public static synchronized HTTPRemoteFileRegistry getInstance() {
        if (instance == null) {
            instance = new HTTPRemoteFileRegistry();
        }
        return instance;
    }

    /**
     * Register a file for serving to an endpoint.
     * @param file The file to register.
     * @param deletable Whether it should be deleted on cleanup.
     * @return The URL it will be served as. It will be uniquely generated.
     * @throws ArgumentNotValid on null or unreadable file.
     * @throws IOFailure on any trouble registerring the file
     */
    public URL registerFile(File file, boolean deletable) {
        ArgumentNotValid.checkNotNull(file, "File file");
        if (!file.isFile() && file.canRead()) {
            throw new ArgumentNotValid("File '" + file + "' is not a readable file");
        }
        String path;
        URL url;
        //ensure we get a random and unique URL.
        do {
            path = "/" + Integer.toHexString(random.nextInt());
            try {
                url = new URL(getProtocol(), localHostName, port, path);
            } catch (MalformedURLException e) {
                throw new IOFailure("Unable to create URL for file '" + file + "'." + " '" + getProtocol() + "', '"
                        + localHostName + "', '" + port + "', '" + path + "''", e);
            }
        } while (registeredFiles.containsKey(url));
        registeredFiles.put(url, new FileInfo(file, deletable));
        log.debug("Registered file '" + file.getPath() + "' with URL '" + url + "'");
        return url;
    }

    /**
     * Get the url for cleaning up after a remote file registered under some
     * URL.
     * @param url some URL
     * 
     * @return the cleanup url.
     * @throws MalformedURLException If unable to construct the cleanup url
     */
    URL getCleanupUrl(URL url) throws MalformedURLException {
        return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath() + UNREGISTER_URL_POSTFIX);
    }

    /** Open a connection to an URL in a registry.
     * @param url The URL to open connection to.
     * @throws IOException If unable to open the connection.
     * @return a connection to an URL in a registry
     */
    protected URLConnection openConnection(URL url) throws IOException {
        return url.openConnection();
    }

    /** Pair of information registered. */
    private class FileInfo {
        /** The file. */
        final File file;
        /** Whether it should be deleted on cleanup. */
        final boolean deletable;

        /** Initialise pair.
         *
         * @param file The file.
         * @param deletable Whether it should be deleted on cleanup.
         */
        FileInfo(File file, boolean deletable) {
            this.file = file;
            this.deletable = deletable;
        }
    }

    /** Stops the server and nulls the instance. */
    public void cleanup() {
        synchronized (HTTPRemoteFileRegistry.class) {
            try {
                server.stop();
            } catch (Exception e) {
                log.warn("Unable to stop HTTPRemoteFile registry");
            }
            try {
                Runtime.getRuntime().removeShutdownHook(cleanupHook);
            } catch (Exception e) {
                //ignore
            }
            instance = null;
        }
    }

    /** A handler for the registry.
     *
     * It has three ways to behave:
     * Serve registered files, return 404 on unknown files, and unregister
     * registered files, depending on the URL.
     */
    protected class HTTPRemoteFileRegistryHandler extends AbstractHandler {
        /**
         * A method for handling Jetty requests.
         *
         * @see AbstractHandler#handle(String, HttpServletRequest,
         * HttpServletResponse, int)
         *
         * @param string Unused domain.
         * @param httpServletRequest request object.
         * @param httpServletResponse the response to write to.
         * @param i Unused state.
         * @throws IOException On trouble in communication.
         * @throws ServletException On servlet trouble.
         */
        public void handle(String string, HttpServletRequest httpServletRequest,
                HttpServletResponse httpServletResponse, int i) throws IOException, ServletException {
            // since this is a jetty handle method, we know it is a Jetty
            // request object.
            Request request = ((Request) httpServletRequest);
            String urlString = httpServletRequest.getRequestURL().toString();
            if (urlString.endsWith(UNREGISTER_URL_POSTFIX)) {
                URL url = new URL(urlString.substring(0, urlString.length() - UNREGISTER_URL_POSTFIX.length()));
                FileInfo fileInfo = registeredFiles.remove(url);
                if (fileInfo != null && fileInfo.deletable && fileInfo.file.exists()) {
                    FileUtils.remove(fileInfo.file);
                    log.debug("Removed file '" + fileInfo.file.getPath() + "' with URL '" + url + "'");
                }
                httpServletResponse.setStatus(200);
                request.setHandled(true);
            } else {
                URL url = new URL(urlString);
                FileInfo fileInfo = registeredFiles.get(url);
                if (fileInfo != null) {
                    httpServletResponse.setStatus(200);
                    FileUtils.writeFileToStream(fileInfo.file, httpServletResponse.getOutputStream());
                    request.setHandled(true);
                    log.debug("Served file '" + fileInfo.file.getPath() + "' with URL '" + url + "'");
                } else {
                    httpServletResponse.sendError(404);
                    log.debug("File not found for URL '" + url + "'");
                }
            }
        }
    }
}