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}