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