001/* 002 * #%L 003 * Netarchivesuite - harvester 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 */ 023 024package dk.netarkivet.viewerproxy.distribute; 025 026import java.io.IOException; 027import java.io.OutputStream; 028import java.net.URI; 029import java.util.HashSet; 030import java.util.Locale; 031import java.util.Set; 032 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import dk.netarkivet.common.exceptions.ArgumentNotValid; 037import dk.netarkivet.common.exceptions.IOFailure; 038import dk.netarkivet.common.utils.StringUtils; 039import dk.netarkivet.viewerproxy.CommandResolver; 040import dk.netarkivet.viewerproxy.Controller; 041import dk.netarkivet.viewerproxy.Request; 042import dk.netarkivet.viewerproxy.Response; 043import dk.netarkivet.viewerproxy.URIResolver; 044 045/** 046 * Wrapper for an URIResolver, which calls the controller methods on given specific URLs, and forwards all others to the 047 * wrapped handler. This allows you to access control methods by giving specific urls to this class. 048 */ 049public class HTTPControllerServer extends CommandResolver { 050 /** 051 * The controller to call methods on. 052 */ 053 private Controller c; 054 /** The log. */ 055 private final Logger log = LoggerFactory.getLogger(HTTPControllerServer.class); 056 057 /** Command for starting url collection. */ 058 static final String START_COMMAND = "/startRecordingURIs"; 059 /** Command for stopping url collection. */ 060 static final String STOP_COMMAND = "/stopRecordingURIs"; 061 /** Command for clearing collected urls. */ 062 static final String CLEAR_COMMAND = "/clearRecordedURIs"; 063 /** Command for getting collected urls. */ 064 static final String GET_RECORDED_URIS_COMMAND = "/getRecordedURIs"; 065 /** Command for changing index. */ 066 static final String CHANGE_INDEX_COMMAND = "/changeIndex"; 067 /** Command for getting status. */ 068 static final String GET_STATUS_COMMAND = "/getStatus"; 069 070 /** 071 * Parameter defining the url to return to after doing start, stop, clear, or changeIndex. 072 */ 073 static final String RETURN_URL_PARAMETER = "returnURL"; 074 /** Parameter for ids of jobs to change index to. May be repeated. */ 075 static final String JOB_ID_PARAMETER = "jobID"; 076 /** Parameter for label of an index. */ 077 static final String INDEX_LABEL_PARAMETER = "label"; 078 /** Parameter for locale to generate status. */ 079 static final String LOCALE_PARAMETER = "locale"; 080 081 /** Http header for location. */ 082 private static final String LOCATION_HEADER = "Location"; 083 /** Http header for content type. */ 084 private static final String CONTENT_TYPE_HEADER = "Content-Type"; 085 /** Http header value for content type text. */ 086 private static final String TEXT_PLAIN_MIMETYPE = "text/plain; charset=UTF-8"; 087 088 /** Http response code for redirect. */ 089 private static final int REDIRECT_RESPONSE_CODE = 303; 090 /** Http response code for OK. */ 091 private static final int OK_RESPONSE_CODE = 200; 092 093 /** 094 * Make a new HTTPControllerServer, which calls commands on the given controller, and forwards all other requests to 095 * the given URIResolver. 096 * 097 * @param c The controller which handles commands given in command URLs. 098 * @param ur The URIResolver to handle all other uris. 099 * @throws ArgumentNotValid if either argument is null. 100 */ 101 public HTTPControllerServer(Controller c, URIResolver ur) { 102 super(ur); 103 ArgumentNotValid.checkNotNull(c, "Controller c"); 104 this.c = c; 105 } 106 107 /** 108 * Handles parsing of the URL and delegating to relevant methods. The commands are of the form 109 * http://<<localhostname>>/<<command>>?<<param>>=<<value>>* Known commands are the following: start - params: 110 * returnURL - effect: start url collection return to returnURL stop - params: returnURL - effect: stop url 111 * collection return to returnURL clear - params: returnURL - effect: clear url collection return to returnURL 112 * getRecordedURIs - params: none - effect: write url collection to response changeIndex - params: jobID*, - effect: 113 * generate index for jobs, returnURL return to returnURL getStatus - params: locale - effect: write status to 114 * response. 115 * 116 * @param request The request to check 117 * @param response The response to give command results to if it is a command. If the request is one of these 118 * commands, the response code is set to 303 if page is redirected to return url; 200 if command url returns data; 119 * otherwise whatever is returned by the wrapped resolver 120 * @return Whether this was a command URL 121 */ 122 protected boolean executeCommand(Request request, Response response) { 123 // If the url is for this host (potential command) 124 if (isCommandHostRequest(request)) { 125 log.debug("Executing command " + request.getURI()); 126 // get path 127 String path = request.getURI().getPath(); 128 if (path.equals(START_COMMAND)) { 129 doStartRecordingURIs(request, response); 130 return true; 131 } 132 if (path.equals(STOP_COMMAND)) { 133 doStopRecordingURIs(request, response); 134 return true; 135 } 136 if (path.equals(CLEAR_COMMAND)) { 137 doClearRecordedURIs(request, response); 138 return true; 139 } 140 if (path.equals(GET_RECORDED_URIS_COMMAND)) { 141 doGetRecordedURIs(request, response); 142 return true; 143 } 144 if (path.equals(CHANGE_INDEX_COMMAND)) { 145 doChangeIndex(request, response); 146 return true; 147 } 148 if (path.equals(GET_STATUS_COMMAND)) { 149 doGetStatus(request, response); 150 return true; 151 } 152 } else { 153 if (request != null) { 154 log.debug("This request is not a CommandHostRequest. Ignoring request for URI {}", request.getURI()); 155 } else { 156 log.debug("This request is not a CommandHostRequest. Ignoring null request"); 157 } 158 } 159 return false; 160 161 } 162 163 /** 164 * Check parameter map for exactly the parameter names given. If any are missing , throws IOFailure naming the 165 * expected parameters. 166 * 167 * @param request The request to check parameters in 168 * @param parameterNames The parameters to check for. 169 * @throws IOFailure on missing parameters. 170 */ 171 private void checkParameters(Request request, String... parameterNames) { 172 for (String parameter : parameterNames) { 173 if (!request.getParameterMap().containsKey(parameter)) { 174 throw new IOFailure("Bad request: '" + request.getURI() + "':\n" + "Wrong parameters. Expected: " 175 + StringUtils.conjoin(",", parameterNames)); 176 } 177 } 178 } 179 180 /** 181 * Helper method to handle start command. 182 * 183 * @param request The HTTP request we are working on 184 * @param response Response to handle result 185 */ 186 private void doStartRecordingURIs(Request request, Response response) { 187 setReturnResponseFromParameter(response, request); 188 c.startRecordingURIs(); 189 } 190 191 /** 192 * Helper method to handle stop command. 193 * 194 * @param request The HTTP request we are working on 195 * @param response Response to handle result 196 */ 197 private void doStopRecordingURIs(Request request, Response response) { 198 setReturnResponseFromParameter(response, request); 199 c.stopRecordingURIs(); 200 } 201 202 /** 203 * Helper method to handle clear command. 204 * 205 * @param request The HTTP request we are working on 206 * @param response Response to handle result 207 */ 208 private void doClearRecordedURIs(Request request, Response response) { 209 setReturnResponseFromParameter(response, request); 210 c.clearRecordedURIs(); 211 } 212 213 /** 214 * Helper method to handle getRecordedURIs command. 215 * 216 * @param request The HTTP request we are working on 217 * @param response Response to handle result 218 */ 219 private void doGetRecordedURIs(Request request, Response response) { 220 checkParameters(request); 221 Set<URI> uris = c.getRecordedURIs(); 222 response.addHeaderField(CONTENT_TYPE_HEADER, TEXT_PLAIN_MIMETYPE); 223 OutputStream os = response.getOutputStream(); 224 try { 225 for (URI recordedUri : uris) { 226 os.write(recordedUri.toString().getBytes()); 227 os.write('\n'); 228 } 229 } catch (IOException e) { 230 throw new IOFailure("Error trying to write missing " + "uris to http response!", e); 231 } 232 response.setStatus(OK_RESPONSE_CODE); 233 } 234 235 /** 236 * Helper method to handle changeIndex command. 237 * 238 * @param request The HTTP request we are working on 239 * @param response Response to handle result 240 */ 241 private void doChangeIndex(Request request, Response response) { 242 checkParameters(request, JOB_ID_PARAMETER); 243 setReturnResponseFromParameter(response, request); 244 String[] jobIDStrings = request.getParameterMap().get(JOB_ID_PARAMETER); 245 Set<Long> jobIDs = new HashSet<Long>(); 246 for (String jobIDString : jobIDStrings) { 247 try { 248 jobIDs.add(Long.parseLong(jobIDString)); 249 } catch (NumberFormatException e) { 250 log.debug("Ignoring illegal job ID in change index " + "command for uri '" + request.getURI() + "'", e); 251 } 252 } 253 String label = getParameter(request, INDEX_LABEL_PARAMETER); 254 c.changeIndex(jobIDs, label); 255 } 256 257 /** 258 * Helper method to handle getStatus command. 259 * 260 * @param request The HTTP request we are working on 261 * @param response Response to handle result 262 */ 263 private void doGetStatus(Request request, Response response) { 264 String localeString = getParameter(request, LOCALE_PARAMETER); 265 response.addHeaderField(CONTENT_TYPE_HEADER, TEXT_PLAIN_MIMETYPE); 266 OutputStream os = response.getOutputStream(); 267 try { 268 os.write(c.getStatus(new Locale(localeString)).getBytes()); 269 } catch (IOException e) { 270 throw new IOFailure("Error trying to write status " + "to http response!", e); 271 } 272 response.setStatus(OK_RESPONSE_CODE); 273 } 274 275 /** 276 * Set up the appropriate headers and return code for doing a redirect to the URL given by the returnURL parameter. 277 * 278 * @param response The response to set to be a redirect 279 * @param request The request to read the returnURL parameter from 280 */ 281 private void setReturnResponseFromParameter(Response response, Request request) { 282 String returnURL = getParameter(request, RETURN_URL_PARAMETER); 283 response.addHeaderField(LOCATION_HEADER, returnURL); 284 response.setStatus(REDIRECT_RESPONSE_CODE); 285 } 286 287 /** 288 * Get a single parameter out of a request. 289 * 290 * @param request A request to look up parameters in. 291 * @param parameterName The name of the parameter to look up. 292 * @return The value of one instance of the parameter in the request. If more than one instance exists, an arbitrary 293 * one is picked. 294 * @throws IOFailure if the parameter is not given. 295 */ 296 private String getParameter(Request request, String parameterName) { 297 checkParameters(request, parameterName); 298 String localeString = request.getParameterMap().get(parameterName)[0]; 299 return localeString; 300 } 301}