com.netflix.zuul.scriptManager.FilterScriptManagerServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.zuul.scriptManager.FilterScriptManagerServlet.java

Source

/*
 * Copyright 2013 Netflix, Inc.
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */
package com.netflix.zuul.scriptManager;

import com.netflix.zuul.util.JsonUtility;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;

import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

/**
 * Servlet for uploading/downloading/managing scripts.
 * <p/>
 * <ul>
 * <li>Upload scripts to the registry for a given endpoint.</li>
 * <li>Download scripts from the registry</li>
 * <li>List all revisions of scripts for a given endpoint.</li>
 * <li>Mark a particular script revision as active for production.</li>
 * </ul>
 */
@ThreadSafe
public class FilterScriptManagerServlet extends HttpServlet {

    private static final long serialVersionUID = -1L;
    private static final Logger logger = LoggerFactory.getLogger(FilterScriptManagerServlet.class);

    /* actions that we permit as an immutable Set */
    private static final Set<String> VALID_GET_ACTIONS = Collections
            .unmodifiableSet(new HashSet<String>(Arrays.asList(new String[] { "LIST", "DOWNLOAD" })));
    private static final Set<String> VALID_PUT_ACTIONS = Collections.unmodifiableSet(new HashSet<String>(
            Arrays.asList(new String[] { "UPLOAD", "ACTIVATE", "DEACTIVATE", "RUN", "CANARY" })));

    /* DAO for performing CRUD operations with scripts */
    private ZuulFilterDAO scriptDAO;

    /* Controller for executing scripts in development/test */

    /**
     * Default constructor that instantiates default dependencies (ie. the ones that are functional as opposed to those for testing).
     */
    public FilterScriptManagerServlet() {
        this(new ZuulFilterDAOCassandra(ZuulFilterDAOCassandra.getCassKeyspace()));

    }

    /**
     * Construct with dependency injection for unit-testing (will never be invoked in production since servlets can't have constructors)
     *
     * @param scriptDAO
     */
    private FilterScriptManagerServlet(ZuulFilterDAO scriptDAO) {
        super();
        this.scriptDAO = scriptDAO;

    }

    /**
     * GET a script or list of scripts.
     * <p/>
     * Action: LIST
     * <p/>
     * Description: List of all script revisions for the given endpoint URI or list all endpoints if endpoint URI not given.
     * <ul>
     * <li>Request Parameter "endpoint": URI</li>
     * </ul>
     * <p/>
     * Action: DOWNLOAD
     * <p/>
     * Description: Download the text or zip file of scripts for a given endpoint URI + revision.
     * <ul>
     * <li>Request Parameter "endpoint": URI</li>
     * <li>Request Parameter "revision": int of revision to download</li>
     * </ul>
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // retrieve arguments and validate
        String action = request.getParameter("action");
        /* validate the action and method */
        if (!isValidAction(request, response)) {
            return;
        }

        // perform action
        if ("LIST".equals(action)) {
            handleListAction(request, response);
        } else if ("DOWNLOAD".equals(action)) {
            handleDownloadAction(request, response);
        }
    }

    /**
     * PUT a script
     * <p/>
     * Action: UPLOAD
     * <p/>
     * Description: Upload a new script text or zip file for a given endpoint URI.
     * <ul>
     * <li>Request Parameter "endpoint": URI</li>
     * <li>Request Parameter "userAuthenticationRequired": true/false</li>
     * <li>POST Body: text or zip file with multiple text files</li>
     * </ul>
     * <p/>
     * Action: ACTIVATE
     * <p/>
     * Description: Activate a script to become the default to execute for a given endpoint URI + revision.
     * <ul>
     * <li>Request Parameter "endpoint": URI</li>
     * <li>Request Parameter "revision": int of revision to activate</li>
     * </ul>
     */
    @Override
    protected void doPut(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // retrieve arguments and validate
        String action = request.getParameter("action");
        /* validate the action and method */
        if (!isValidAction(request, response)) {
            return;
        }

        // perform action
        if ("UPLOAD".equals(action)) {
            handleUploadAction(request, response);
        } else if ("ACTIVATE".equals(action)) {
            handleActivateAction(request, response);
        } else if ("CANARY".equals(action)) {
            handleCanaryAction(request, response);
        } else if ("DEACTIVATE".equals(action)) {
            handledeActivateAction(request, response);
        }

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doPut(request, response);
    }

    @Override
    protected void doDelete(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        setUsageError(405, response);
        return;
    }

    private void handleListAction(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String filter_id = request.getParameter("filter_id");
        if (filter_id == null) {
            // get list of all endpoints
            List<String> filterIDs = scriptDAO.getAllFilterIDs();
            Map<String, Object> json = new LinkedHashMap<String, Object>();
            json.put("filters", filterIDs);
            response.getWriter().write(JsonUtility.jsonFromMap(json));
        } else {
            List<FilterInfo> scripts;
            if (Boolean.parseBoolean(request.getParameter("active"))) {
                // get list of all scripts for this endpoint
                FilterInfo activeEndpoint = scriptDAO.getActiveFilterInfoForFilter(filter_id);
                scripts = activeEndpoint == null ? Collections.EMPTY_LIST
                        : Collections.singletonList(activeEndpoint);
            } else {
                // get list of all scripts for this endpoint
                scripts = scriptDAO.getZuulFiltersForFilterId(filter_id);
            }
            if (scripts.size() == 0) {
                setUsageError(404, "ERROR: No scripts found for endpoint: " + filter_id, response);
            } else {
                // output JSON
                Map<String, Object> json = new LinkedHashMap<String, Object>();
                json.put("filter_id", filter_id);
                List<Map<String, Object>> scriptsJson = new ArrayList<Map<String, Object>>();
                for (FilterInfo script : scripts) {
                    Map<String, Object> scriptJson = createEndpointScriptJSON(script);
                    scriptsJson.add(scriptJson);
                }

                json.put("filters", scriptsJson);

                response.getWriter().write(JsonUtility.jsonFromMap(json));
            }
        }
    }

    private void handleDownloadAction(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String filter_id = request.getParameter("filter_id");
        if (filter_id == null) {
            // return error, endpoint is required
            setUsageError(404, "ERROR: No endpoint defined.", response);
        } else {
            String revision = request.getParameter("revision");
            FilterInfo script = null;
            if (revision == null) {
                // get latest
                script = scriptDAO.getLatestFilterInfoForFilter(filter_id);
            } else {
                int revisionNumber = -1;
                try {
                    revisionNumber = Integer.parseInt(revision);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: revision must be an integer.", response);
                    return;
                }
                // get the specific revision
                script = scriptDAO.getFilterInfoForFilter(filter_id, revisionNumber);
            }

            // now output script
            if (script == null) {
                setUsageError(404, "ERROR: No scripts found.", response);
            } else {
                if (script.getFilterCode() == null) {
                    // this shouldn't occur but I want to handle it if it does
                    logger.error("Found FilterInfo object without scripts. Length==0. Request: "
                            + request.getPathInfo());
                    setUsageError(500, "ERROR: script files not found", response);
                } else {
                    // output the single script
                    response.setContentType("text/plain");
                    response.getWriter().write(script.getFilterCode());
                }
            }
        }
    }

    private void handleCanaryAction(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String filter_id = request.getParameter("filter_id");
        if (filter_id == null) {
            // return error, endpoint is required
            setUsageError(404, "ERROR: No endpoint defined.", response);
        } else {
            String revision = request.getParameter("revision");
            if (revision == null) {
                setUsageError(404, "ERROR: No revision defined.", response);
            } else {
                int revisionNumber = -1;
                try {
                    revisionNumber = Integer.parseInt(revision);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: revision must be an integer.", response);
                    return;
                }
                FilterInfo filterInfo = scriptDAO.setCanaryFilter(filter_id, revisionNumber);
                //                Map<String, Object> scriptJson = createEndpointScriptJSON(filterInfo);
                //                response.getWriter().write(JsonUtility.jsonFromMap(scriptJson));
                response.sendRedirect("filterLoader.jsp");

            }
        }

    }

    private void handledeActivateAction(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String filter_id = request.getParameter("filter_id");
        if (filter_id == null) {
            // return error, endpoint is required
            setUsageError(404, "ERROR: No endpoint defined.", response);
        } else {
            String revision = request.getParameter("revision");
            if (revision == null) {
                setUsageError(404, "ERROR: No revision defined.", response);
            } else {
                int revisionNumber = -1;
                try {
                    revisionNumber = Integer.parseInt(revision);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: revision must be an integer.", response);
                    return;
                }
                try {
                    FilterInfo filterInfo = scriptDAO.deActivateFilter(filter_id, revisionNumber);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: " + e.getMessage(), response);
                    return;
                }
                response.sendRedirect("filterLoader.jsp");

                //                Map<String, Object> scriptJson = createEndpointScriptJSON(filterInfo);
                //              response.getWriter().write(JsonUtility.jsonFromMap(scriptJson));
            }
        }

    }

    private void handleActivateAction(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String filter_id = request.getParameter("filter_id");
        if (filter_id == null) {
            // return error, endpoint is required
            setUsageError(404, "ERROR: No endpoint defined.", response);
        } else {
            String revision = request.getParameter("revision");
            if (revision == null) {
                setUsageError(404, "ERROR: No revision defined.", response);
            } else {
                int revisionNumber = -1;
                try {
                    revisionNumber = Integer.parseInt(revision);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: revision must be an integer.", response);
                    return;
                }
                try {
                    FilterInfo filterInfo = scriptDAO.setFilterActive(filter_id, revisionNumber);
                } catch (Exception e) {
                    setUsageError(400, "ERROR: " + e.getMessage(), response);
                    return;
                }
                response.sendRedirect("filterLoader.jsp");

                //                Map<String, Object> scriptJson = createEndpointScriptJSON(filterInfo);
                //              response.getWriter().write(JsonUtility.jsonFromMap(scriptJson));
            }
        }

    }

    private void handleUploadAction(HttpServletRequest request, HttpServletResponse response) throws IOException {

        String filter = handlePostBody(request, response);

        if (filter != null) {
            FilterInfo filterInfo = null;
            try {
                filterInfo = FilterVerifier.getInstance().verifyFilter(filter);
            } catch (IllegalAccessException e) {
                logger.error(e.getMessage(), e);
                setUsageError(500, "ERROR: Unable to process uploaded data. " + e.getMessage(), response);
            } catch (InstantiationException e) {
                logger.error(e.getMessage(), e);
                setUsageError(500, "ERROR: Bad Filter. " + e.getMessage(), response);
            }
            filterInfo = scriptDAO.addFilter(filter, filterInfo.getFilterType(), filterInfo.getFilterName(),
                    filterInfo.getFilterDisablePropertyName(), filterInfo.getFilterOrder());
            if (filterInfo == null) {
                setUsageError(500, "ERROR: Unable to process uploaded data.", response);
                return;
            }
            response.sendRedirect("filterLoader.jsp");

            //            Map<String, Object> scriptJson = createEndpointScriptJSON(filterInfo);
            //            response.getWriter().write(JsonUtility.jsonFromMap(scriptJson));
        }
    }

    private String handlePostBody(HttpServletRequest request, HttpServletResponse response) throws IOException {

        FileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        org.apache.commons.fileupload.FileItemIterator it = null;
        try {
            it = upload.getItemIterator(request);

            while (it.hasNext()) {
                FileItemStream stream = it.next();
                InputStream input = stream.openStream();

                // NOTE: we are going to pull the entire stream into memory
                // this will NOT work if we have huge scripts, but we expect these to be measured in KBs, not MBs or larger
                byte[] uploadedBytes = getBytesFromInputStream(input);
                input.close();

                if (uploadedBytes.length == 0) {
                    setUsageError(400, "ERROR: Body contained no data.", response);
                    return null;
                }

                return new String(uploadedBytes);
            }
        } catch (FileUploadException e) {
            throw new IOException(e.getMessage());
        }
        return null;
    }

    private byte[] getBytesFromInputStream(InputStream input) throws IOException {
        int v = 0;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while ((v = input.read()) != -1) {
            bos.write(v);
        }
        bos.close();
        return bos.toByteArray();
    }

    private Map<String, Object> createEndpointScriptJSON(FilterInfo script) {
        Map<String, Object> scriptJson = new LinkedHashMap<String, Object>();
        scriptJson.put("filter_id", script.getFilterID());
        scriptJson.put("filter_name", script.getFilterName());
        scriptJson.put("filter_type", script.getFilterType());
        scriptJson.put("revision", script.getRevision());
        scriptJson.put("active", script.isActive());
        scriptJson.put("creationDate", script.getCreationDate());
        scriptJson.put("canary", script.isCanary());
        return scriptJson;
    }

    /**
     * Determine if the incoming action + method is a correct combination. If not, output the usage docs and set an error code on the response.
     *
     * @param request
     * @param response
     * @return true if valid, false if not
     */
    private static boolean isValidAction(HttpServletRequest request, HttpServletResponse response) {
        String action = request.getParameter("action");
        if (action != null) {
            action = action.trim().toUpperCase();
            /* test for GET actions */
            if (VALID_GET_ACTIONS.contains(action)) {
                if (!request.getMethod().equals("GET")) {
                    // valid action, wrong method
                    setUsageError(405, "ERROR: Invalid HTTP method for action type.", response);
                    return false;
                }
                // valid action and method
                return true;
            }

            if (VALID_PUT_ACTIONS.contains(action)) {
                if (!(request.getMethod().equals("PUT") || request.getMethod().equals("POST"))) {
                    // valid action, wrong method
                    setUsageError(405, "ERROR: Invalid HTTP method for action type.", response);
                    return false;
                }
                // valid action and method
                return true;
            }

            // wrong action
            setUsageError(400, "ERROR: Unknown action type.", response);
            return false;
        } else {
            setUsageError(400, "ERROR: Invalid arguments.", response);
            return false;
        }
    }

    /**
     * Set an error code and print out the usage docs to the response with a preceding error message
     *
     * @param statusCode
     * @param response
     */
    private static void setUsageError(int statusCode, String message, HttpServletResponse response) {
        response.setStatus(statusCode);
        try {
            Writer w = response.getWriter();
            if (message != null) {
                w.write(message + "\n\n");
            }
            w.write(getUsageDoc());
        } catch (Exception e) {
            logger.error("Failed to output usage error.", e);
            // won't throw exception because this is not critical, logging the error is enough
        }
    }

    /**
     * Set an error code and print out the usage docs to the response.
     *
     * @param statusCode
     * @param response
     */
    private static void setUsageError(int statusCode, HttpServletResponse response) {
        setUsageError(statusCode, null, response);
    }

    /**
     * Usage documentation to be output when a URL is malformed.
     *
     * @return
     */
    private static String getUsageDoc() {
        StringBuilder s = new StringBuilder();
        s.append("Usage: /scriptManager?action=<ACTION_TYPE>&<ARGS>").append("\n");
        s.append("       Actions:").append("\n");
        s.append("          LIST: List all endpoints with scripts or all scripts for a given endpoint.")
                .append("\n");
        s.append("              Arguments:").append("\n");
        s.append(
                "                  endpoint: [Optional (Default: All endpoints)] The endpoint of script revisions to list.")
                .append("\n");
        s.append("              Examples:").append("\n");
        s.append("                GET /scriptManager?action=LIST").append("\n");
        s.append("                GET /scriptManager?action=LIST&endpoint=/ps3/home").append("\n");
        s.append("\n");

        s.append("          DOWNLOAD: Download a given script.").append("\n");
        s.append("              Arguments:").append("\n");
        s.append("                  endpoint: [Required] The endpoint of script to download.").append("\n");
        s.append("                  revision: [Optional (Default: last revision)] The revision to download.")
                .append("\n");
        s.append("              Examples:").append("\n");
        s.append("                GET /scriptManager?action=DOWNLOAD&endpoint=/ps3/home").append("\n");
        s.append("                GET /scriptManager?action=DOWNLOAD&endpoint=/ps3/home&revision=23").append("\n");
        s.append("\n");

        s.append("          UPLOAD: Upload a script for a given endpoint.").append("\n");
        s.append("              Arguments:").append("\n");
        s.append(
                "                  endpoint: [Required] The endpoint to associated the script with. If it doesn't exist it will be created.")
                .append("\n");
        s.append(
                "                  userAuthenticationRequired: [Optional (Default: true)] Whether the script requires an authenticated user to execute.")
                .append("\n");
        s.append("              Example:").append("\n");
        s.append("                POST /scriptManager?action=UPLOAD&endpoint=/ps3/home").append("\n");
        s.append(
                "                POST /scriptManager?action=UPLOAD&endpoint=/ps3/home&userAuthenticationRequired=false")
                .append("\n");
        s.append("\n");

        s.append("          ACTIVATE: Mark a particular script revision as active for production.").append("\n");
        s.append("              Arguments:").append("\n");
        s.append(
                "                  endpoint: [Required] The endpoint for which a script revision should be activated.")
                .append("\n");
        s.append("                  revision: [Required] The script revision to activate.").append("\n");
        s.append("              Example:").append("\n");
        s.append("                PUT /scriptManager?action=ACTIVATE&endpoint=/ps3/home&revision=22").append("\n");
        return s.toString();
    }

    public static class UnitTest {
        @Mock
        HttpServletRequest request;
        @Mock
        HttpServletResponse response;
        @Mock
        PrintWriter responseWriter;
        @Mock
        ServletOutputStream outputStream;

        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
            try {
                when(response.getWriter()).thenReturn(responseWriter);
                when(response.getOutputStream()).thenReturn(outputStream);
            } catch (IOException e) {
                throw new RuntimeException("failed to initialize mock", e);
            }
        }

        private FilterScriptManagerServlet getEndpointScriptManagerImplementation(ZuulFilterDAO dao) {
            return new FilterScriptManagerServlet(dao);
        }

        /**
         * Not found response for a "list scripts" request.
         *
         * @throws Exception
         */
        @Test
        public void testListScriptNotFound() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?action=LIST&filter_id=name2:type");
            when(request.getParameter("action")).thenReturn("LIST");
            when(request.getParameter("filter_id")).thenReturn("name:type");
            when(request.getMethod()).thenReturn("GET");

            /* setup mock DAO */
            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            List<FilterInfo> emptyResponse = Collections.emptyList();
            when(dao.getZuulFiltersForFilterId(anyString())).thenReturn(emptyResponse);

            /* construct servlet */
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            // verify that we get a 404 when this endpoint isn't found
            verify(response).setStatus(404);
        }

        /**
         * List scripts for an endpoint.
         *
         * @throws Exception
         */
        @Test
        public void testListScripts() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?action=LIST&filter_id=name1:type1");
            when(request.getParameter("action")).thenReturn("LIST");
            when(request.getParameter("filter_id")).thenReturn("name1:type");
            when(request.getMethod()).thenReturn("GET");

            /* setup mock DAO */

            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);

            Calendar now = Calendar.getInstance();

            List<FilterInfo> scriptsForEndpoint = new ArrayList<FilterInfo>();
            scriptsForEndpoint.add(new FilterInfo("name1:type", "code", "type", "name", "disable", "order", "app"));
            scriptsForEndpoint.add(new FilterInfo("name2:type", "code", "type", "name", "disable", "order", "app"));
            scriptsForEndpoint.add(new FilterInfo("name3:type", "code", "type", "name", "disable", "order", "app"));
            when(dao.getZuulFiltersForFilterId(anyString())).thenReturn(scriptsForEndpoint);

            /* construct servlet */
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());

            /* build up the JSON we expect */
            Map<String, Object> expectedJson = new LinkedHashMap<String, Object>();
            expectedJson.put("filter_id", "name1:type");
            List<Map<String, Object>> scriptsJson = new ArrayList<Map<String, Object>>();
            for (FilterInfo script : scriptsForEndpoint) {
                Map<String, Object> scriptJson = createExpectedJsonMap(script);
                scriptsJson.add(scriptJson);
            }
            expectedJson.put("filters", scriptsJson);

            /* verify that we received the expected JSON */
            String expectedJsonString = JsonUtility.jsonFromMap(expectedJson);
            System.out.println("Expected JSON: \n" + expectedJsonString);
            verify(responseWriter).write(expectedJsonString);
        }

        /**
         * List all endpoints
         *
         * @throws Exception
         */
        @Test
        public void testListEndpoints() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?action=LIST");
            when(request.getParameter("action")).thenReturn("LIST");
            when(request.getMethod()).thenReturn("GET");

            /* setup mock DAO */
            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            List<String> filters = new ArrayList<String>();
            filters.add("name1:type");
            filters.add("name2:type");
            filters.add("name3:type");
            filters.add("name4:type");
            when(dao.getAllFilterIDs()).thenReturn(filters);

            /* construct servlet */
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* build up the JSON we expect */
            Map<String, Object> expectedJson = new LinkedHashMap<String, Object>();
            expectedJson.put("filters", filters);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());

            /* verify that we received the expected JSON */
            String expectedJsonString = JsonUtility.jsonFromMap(expectedJson);
            System.out.println("Expected JSON: \n" + expectedJsonString);
            verify(responseWriter).write(expectedJsonString);
        }

        @Test
        public void testErrorWithUsageIfIncorrectMethod() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?action=LIST&filter=unknown:type");
            when(request.getParameter("action")).thenReturn("LIST");
            // send POST instead of GET so we should get an error
            when(request.getMethod()).thenReturn("POST");

            FilterScriptManagerServlet servlet = new FilterScriptManagerServlet();
            servlet.service(request, response);

            // a 405 because POST with those arguments is invalid
            verify(response).setStatus(405);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: Invalid HTTP method for action type.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testErrorWithUsageIfIncorrectArguments1() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?");
            when(request.getMethod()).thenReturn("GET");

            FilterScriptManagerServlet servlet = new FilterScriptManagerServlet();
            servlet.service(request, response);

            System.out.println("--------------------------------------------------------");
            System.out.println(getUsageDoc());
            System.out.println("--------------------------------------------------------");

            // a 400 because the resource exists, but arguments are incorrect
            verify(response).setStatus(400);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: Invalid arguments.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testErrorWithUsageIfIncorrectArguments2() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?action=UNKNOWN");
            when(request.getParameter("action")).thenReturn("UNKNOWN");
            when(request.getMethod()).thenReturn("GET");

            FilterScriptManagerServlet servlet = new FilterScriptManagerServlet();
            servlet.service(request, response);

            // a 400 because the resource exists, but arguments are incorrect
            verify(response).setStatus(400);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: Unknown action type.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testDoDeleteReturnsError() throws Exception {
            /* setup mock request to return URI */
            when(request.getPathInfo()).thenReturn("?");
            // send POST instead of GET so we should get an error
            when(request.getMethod()).thenReturn("DELETE");

            FilterScriptManagerServlet servlet = new FilterScriptManagerServlet();
            servlet.service(request, response);

            // a 405 because POST with those arguments is invalid
            verify(response).setStatus(405);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testDownloadSingleScriptRevision() throws Exception {
            /* setup mock */
            String filter_id = "name:type";
            String action = "DOWNLOAD";
            when(request.getPathInfo()).thenReturn("?action=" + action + "&filter_id=" + filter_id + "&revision=2");
            when(request.getMethod()).thenReturn("GET");
            when(request.getParameter("action")).thenReturn(action);
            when(request.getParameter("filter_id")).thenReturn(filter_id);
            when(request.getParameter("revision")).thenReturn("2");

            /* setup mock DAO */
            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterInfo script = mock(FilterInfo.class);
            when(dao.getFilterInfoForFilter(filter_id, 2)).thenReturn(script);
            String code = "code";

            when(script.getFilterCode()).thenReturn(code);

            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());

            // verify mime-type
            verify(response).setContentType("text/plain");

            // verify the script is written to the response
            verify(responseWriter).write("code");
        }

        @Test
        public void testDownloadSingleScriptLatest() throws Exception {
            /* setup mock */
            String filter_id = "name:type";
            String action = "DOWNLOAD";
            when(request.getPathInfo()).thenReturn("?action=" + action + "&filter_id=" + filter_id);
            when(request.getMethod()).thenReturn("GET");
            when(request.getParameter("action")).thenReturn(action);
            when(request.getParameter("filter_id")).thenReturn(filter_id);

            /* setup mock DAO */
            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterInfo script = mock(FilterInfo.class);
            when(dao.getLatestFilterInfoForFilter(filter_id)).thenReturn(script);
            when(dao.getFilterInfoForFilter(filter_id, 2)).thenReturn(script);
            when(script.getFilterCode()).thenReturn("code");

            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());

            // verify mime-type
            verify(response).setContentType("text/plain");

            // verify the script is written to the response
            verify(responseWriter).write("code");
        }

        @Test
        public void testDownloadSingleScriptPlusErrorHandlerLatest() throws Exception {
            /* setup mock */
            String filter_id = "name:type";
            String action = "DOWNLOAD";
            when(request.getPathInfo()).thenReturn("?action=" + action + "&filter_id=" + filter_id);
            when(request.getMethod()).thenReturn("GET");
            when(request.getParameter("action")).thenReturn(action);
            when(request.getParameter("filter_id")).thenReturn(filter_id);

            /* setup mock DAO */
            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterInfo script = mock(FilterInfo.class);
            when(dao.getLatestFilterInfoForFilter(filter_id)).thenReturn(script);
            when(dao.getFilterInfoForFilter(filter_id, 2)).thenReturn(script);
            when(script.getFilterCode()).thenReturn("code");

            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());

            // the writer should not be touched since we need binary, not text
            verify(responseWriter).write(anyString());
        }

        @Test
        public void testDownloadEndpointNotFound() throws Exception {
            /* setup mock */
            String action = "DOWNLOAD";
            when(request.getPathInfo()).thenReturn("?action=" + action);
            when(request.getMethod()).thenReturn("GET");
            when(request.getParameter("action")).thenReturn(action);

            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            // a 404 because incorrect arguments were given (so the resource 'does not exist')
            verify(response).setStatus(404);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: No endpoint defined.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testActivateRevision() throws Exception {
            /* setup mock */
            String endpoint = "name:type";
            String action = "ACTIVATE";
            when(request.getPathInfo()).thenReturn("?action=" + action + "&filter_id=" + endpoint + "&revision=2");
            when(request.getMethod()).thenReturn("POST");
            when(request.getParameter("action")).thenReturn(action);
            when(request.getParameter("filter_id")).thenReturn(endpoint);
            when(request.getParameter("revision")).thenReturn("2");

            /* setup mock DAO */
            ZuulFilterDAOCassandra dao = mock(ZuulFilterDAOCassandra.class);

            /* setup the mock script that will be uploaded */
            FilterInfo script = mockEndpointScript();

            // userAuthenticationRequired should default to true
            when(dao.setFilterActive(endpoint, 2)).thenReturn(script);

            // execute the servlet
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            /* verify the default status is used */
            verify(response, never()).setStatus(anyInt());
            verify(dao).setFilterActive(endpoint, 2);

            Map<String, Object> expectedJson = createExpectedJsonMap(script);

        }

        @Test
        public void testActivateEndpointNotFound() throws Exception {
            /* setup mock */
            String action = "ACTIVATE";
            when(request.getPathInfo()).thenReturn("?action=" + action);
            when(request.getMethod()).thenReturn("POST");
            when(request.getParameter("action")).thenReturn(action);

            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            // a 404 because incorrect arguments were given (so the resource 'does not exist')
            verify(response).setStatus(404);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: No endpoint defined.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        @Test
        public void testActivateRevisionNotFound() throws Exception {
            /* setup mock */
            String endpoint = "/ps3/{userID}/home";
            String action = "ACTIVATE";
            when(request.getPathInfo()).thenReturn("?action=" + action + "&filter_id=" + endpoint);
            when(request.getMethod()).thenReturn("POST");
            when(request.getParameter("action")).thenReturn(action);
            when(request.getParameter("filter_id")).thenReturn(endpoint);

            ZuulFilterDAO dao = mock(ZuulFilterDAOCassandra.class);
            FilterScriptManagerServlet servlet = getEndpointScriptManagerImplementation(dao);
            servlet.service(request, response);

            // a 404 because incorrect arguments were given (so the resource 'does not exist')
            verify(response).setStatus(404);
            // test that the usage docs were output
            verify(response).getWriter();
            verify(responseWriter).write("ERROR: No revision defined.\n\n");
            verify(responseWriter).write(getUsageDoc());
        }

        private Map<String, Object> createExpectedJsonMap(FilterInfo script) {
            Map<String, Object> expectedJson = new LinkedHashMap<String, Object>();
            expectedJson.put("filter_id", script.getFilterID());
            expectedJson.put("filter_name", script.getFilterName());
            expectedJson.put("filter_type", script.getFilterType());
            expectedJson.put("revision", script.getRevision());
            expectedJson.put("active", script.isActive());
            expectedJson.put("creationDate", script.getCreationDate());
            expectedJson.put("canary", script.isCanary());
            return expectedJson;
        }

        private FilterInfo mockEndpointScript() {
            Calendar now = Calendar.getInstance();
            FilterInfo script = mock(FilterInfo.class);
            when(script.getCreationDate()).thenReturn(now.getTime());
            when(script.getRevision()).thenReturn(1);
            when(script.isActive()).thenReturn(false);
            when(script.isCanary()).thenReturn(false);
            return script;
        }

        private Integer[] getStringAsIntegerArray(String scriptBody) {
            char[] chars = scriptBody.toCharArray();
            Integer[] bytes = new Integer[chars.length + 1];
            for (int i = 0; i < chars.length; i++) {
                bytes[i] = Integer.valueOf(chars[i]);
            }
            bytes[bytes.length - 1] = -1;
            return bytes;
        }
    }
}