001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.distribute;
024
025import java.io.File;
026import java.io.IOException;
027import java.net.MalformedURLException;
028import java.net.URL;
029import java.net.URLConnection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Map;
033import java.util.Random;
034
035import javax.servlet.ServletException;
036import javax.servlet.http.HttpServletRequest;
037import javax.servlet.http.HttpServletResponse;
038
039import org.mortbay.jetty.Request;
040import org.mortbay.jetty.Server;
041
042import org.mortbay.jetty.bio.SocketConnector;
043import org.mortbay.jetty.handler.AbstractHandler;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import dk.netarkivet.common.exceptions.ArgumentNotValid;
048import dk.netarkivet.common.exceptions.IOFailure;
049import dk.netarkivet.common.utils.CleanupHook;
050import dk.netarkivet.common.utils.CleanupIF;
051import dk.netarkivet.common.utils.FileUtils;
052import dk.netarkivet.common.utils.Settings;
053import dk.netarkivet.common.utils.SystemUtils;
054
055/**
056 * This is a registry for HTTP remote file, meant for serving registered files to remote hosts. The embedded webserver
057 * handling remote files for HTTPRemoteFile point-to-point communication. Optimised to use direct transfer on local
058 * machine.
059 */
060public class HTTPRemoteFileRegistry implements CleanupIF {
061
062    /** Logger for this class. */
063    private static final Logger log = LoggerFactory.getLogger(HTTPRemoteFileRegistry.class);
064
065    /** The unique instance. */
066    protected static HTTPRemoteFileRegistry instance;
067
068    /** Protocol for URLs. */
069    private static final String PROTOCOL = "http";
070
071    /** Port number for generating URLs. Read from settings. */
072    protected final int port;
073
074    /** Name of this host. Used for generating URLs. */
075    private final String localHostName;
076
077    /** Files to serve. */
078    private final Map<URL, FileInfo> registeredFiles;
079
080    /** Instance to create random URLs. */
081    private final Random random;
082
083    /**
084     * Postfix to add to an URL to get cleanup URL. We are not using query string, because it behaves strangely in
085     * HttpServletRequest.
086     */
087    private static final String UNREGISTER_URL_POSTFIX = "/unregister";
088
089    /** The embedded webserver. */
090    protected Server server;
091    /** The shutdown hook. */
092    private CleanupHook cleanupHook;
093
094    /**
095     * Initialise the registry. This includes registering an HTTP server for getting the files from this machine.
096     *
097     * @throws IOFailure if it cannot be initialised.
098     */
099    protected HTTPRemoteFileRegistry() {
100        port = Settings.getInt(HTTPRemoteFile.HTTPREMOTEFILE_PORT_NUMBER);
101        localHostName = SystemUtils.getLocalHostName();
102        registeredFiles = Collections.synchronizedMap(new HashMap<URL, FileInfo>());
103        random = new Random();
104        startServer();
105        cleanupHook = new CleanupHook(this);
106        Runtime.getRuntime().addShutdownHook(cleanupHook);
107    }
108
109    /**
110     * Start the server, including a handler that responds with registered files, removes registered files on request,
111     * and gives 404 otherwise.
112     *
113     * @throws IOFailure if it cannot be initialised.
114     */
115    protected void startServer() {
116        server = new Server();
117        //ServerConnector connector = new ServerConnector(server);
118        SocketConnector connector = new SocketConnector();
119        connector.setPort(port);
120        server.addConnector(connector);
121        server.setHandler(new HTTPRemoteFileRegistryHandler());
122        try {
123            server.start();
124        } catch (Exception e) {
125            throw new IOFailure("Cannot start HTTPRemoteFile registry on port " + port, e);
126        }
127    }
128
129    /**
130     * Get the protocol part of URLs, that is HTTP.
131     *
132     * @return "http", the protocol.
133     */
134    protected String getProtocol() {
135        return PROTOCOL;
136    }
137
138    /**
139     * Get the unique instance.
140     *
141     * @return The unique instance.
142     */
143    public static synchronized HTTPRemoteFileRegistry getInstance() {
144        if (instance == null) {
145            instance = new HTTPRemoteFileRegistry();
146        }
147        return instance;
148    }
149
150    /**
151     * Register a file for serving to an endpoint.
152     *
153     * @param file The file to register.
154     * @param deletable Whether it should be deleted on cleanup.
155     * @return The URL it will be served as. It will be uniquely generated.
156     * @throws ArgumentNotValid on null or unreadable file.
157     * @throws IOFailure on any trouble registerring the file
158     */
159    public URL registerFile(File file, boolean deletable) {
160        ArgumentNotValid.checkNotNull(file, "File file");
161        if (!file.isFile() && file.canRead()) {
162            throw new ArgumentNotValid("File '" + file + "' is not a readable file");
163        }
164        String path;
165        URL url;
166        // ensure we get a random and unique URL.
167        do {
168            path = "/" + Integer.toHexString(random.nextInt());
169            try {
170                url = new URL(getProtocol(), localHostName, port, path);
171            } catch (MalformedURLException e) {
172                throw new IOFailure("Unable to create URL for file '" + file + "'." + " '" + getProtocol() + "', '"
173                        + localHostName + "', '" + port + "', '" + path + "''", e);
174            }
175        } while (registeredFiles.containsKey(url));
176        registeredFiles.put(url, new FileInfo(file, deletable));
177        log.debug("Registered file '{}' with URL '{}'", file.getPath(), url);
178        return url;
179    }
180
181    /**
182     * Get the url for cleaning up after a remote file registered under some URL.
183     *
184     * @param url some URL
185     * @return the cleanup url.
186     * @throws MalformedURLException If unable to construct the cleanup url
187     */
188    URL getCleanupUrl(URL url) throws MalformedURLException {
189        return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath() + UNREGISTER_URL_POSTFIX);
190    }
191
192    /**
193     * Open a connection to an URL in a registry.
194     *
195     * @param url The URL to open connection to.
196     * @return a connection to an URL in a registry
197     * @throws IOException If unable to open the connection.
198     */
199    protected URLConnection openConnection(URL url) throws IOException {
200        return url.openConnection();
201    }
202
203    /** Pair of information registered. */
204    private class FileInfo {
205        /** The file. */
206        final File file;
207        /** Whether it should be deleted on cleanup. */
208        final boolean deletable;
209
210        /**
211         * Initialise pair.
212         *
213         * @param file The file.
214         * @param deletable Whether it should be deleted on cleanup.
215         */
216        FileInfo(File file, boolean deletable) {
217            this.file = file;
218            this.deletable = deletable;
219        }
220    }
221
222    /** Stops the server and nulls the instance. */
223    public void cleanup() {
224        synchronized (HTTPRemoteFileRegistry.class) {
225            try {
226                server.stop();
227            } catch (Exception e) {
228                log.warn("Unable to stop HTTPRemoteFile registry");
229            }
230            try {
231                Runtime.getRuntime().removeShutdownHook(cleanupHook);
232            } catch (Exception e) {
233                // ignore
234            }
235            instance = null;
236        }
237    }
238
239    /**
240     * A handler for the registry.
241     * <p>
242     * It has three ways to behave: Serve registered files, return 404 on unknown files, and unregister registered
243     * files, depending on the URL.
244     */
245    protected class HTTPRemoteFileRegistryHandler extends AbstractHandler {
246        /**
247         * A method for handling Jetty requests.
248         *
249         * @param string Unused domain.
250         * @param httpServletRequest request object.
251         * @param httpServletResponse the response to write to.
252         * @throws IOException On trouble in communication.
253         * @throws ServletException On servlet trouble.
254         * @see AbstractHandler#handle(String, org.eclipse.jetty.server.Request, HttpServletRequest,
255         * HttpServletResponse), HttpServletResponse, int)
256         */
257        @Override
258        public void handle(String string, 
259                HttpServletRequest httpServletRequest,
260                HttpServletResponse httpServletResponse, int i) throws IOException, ServletException {
261            // since this is a jetty handle method, we know it is a Jetty
262            // request object.
263            Request request = ((Request) httpServletRequest);
264            String urlString = httpServletRequest.getRequestURL().toString();
265            if (urlString.endsWith(UNREGISTER_URL_POSTFIX)) {
266                URL url = new URL(urlString.substring(0, urlString.length() - UNREGISTER_URL_POSTFIX.length()));
267                FileInfo fileInfo = registeredFiles.remove(url);
268                if (fileInfo != null && fileInfo.deletable && fileInfo.file.exists()) {
269                    FileUtils.remove(fileInfo.file);
270                    log.debug("Removed file '{}' with URL '{}'", fileInfo.file.getPath(), url);
271                }
272                httpServletResponse.setStatus(200);
273                request.setHandled(true);
274            } else {
275                URL url = new URL(urlString);
276                FileInfo fileInfo = registeredFiles.get(url);
277                if (fileInfo != null) {
278                    httpServletResponse.setStatus(200);
279                    FileUtils.writeFileToStream(fileInfo.file, httpServletResponse.getOutputStream());
280                    request.setHandled(true);
281                    log.debug("Served file '{}' with URL '{}'", fileInfo.file.getPath(), url);
282                } else {
283                    httpServletResponse.sendError(404);
284                    log.debug("File not found for URL '{}'", url);
285                }
286            }
287        }
288    }
289
290}