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}