au.edu.uq.cmm.paul.servlet.WebUIController.java Source code

Java tutorial

Introduction

Here is the source code for au.edu.uq.cmm.paul.servlet.WebUIController.java

Source

/*
* Copyright 2012-2013, CMM, University of Queensland.
*
* This file is part of Paul.
*
* Paul is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Paul is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Paul. If not, see <http://www.gnu.org/licenses/>.
*/

package au.edu.uq.cmm.paul.servlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.realm.GenericPrincipal;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Hours;
import org.joda.time.Minutes;
import org.joda.time.Months;
import org.joda.time.Weeks;
import org.joda.time.Years;
import org.joda.time.base.BaseSingleFieldPeriod;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.ServletContextAware;

import au.edu.uq.cmm.aclslib.config.BuildInfo;
import au.edu.uq.cmm.aclslib.config.ConfigurationException;
import au.edu.uq.cmm.aclslib.config.FacilityConfig;
import au.edu.uq.cmm.aclslib.proxy.AclsAuthenticationException;
import au.edu.uq.cmm.aclslib.proxy.AclsInUseException;
import au.edu.uq.cmm.aclslib.service.Service.State;
import au.edu.uq.cmm.eccles.EcclesProxyConfiguration;
import au.edu.uq.cmm.eccles.FacilitySession;
import au.edu.uq.cmm.eccles.UserDetails;
import au.edu.uq.cmm.eccles.UserDetailsException;
import au.edu.uq.cmm.eccles.UserDetailsManager;
import au.edu.uq.cmm.paul.Paul;
import au.edu.uq.cmm.paul.PaulConfiguration;
import au.edu.uq.cmm.paul.grabber.Analyser;
import au.edu.uq.cmm.paul.grabber.DatafileMetadata;
import au.edu.uq.cmm.paul.grabber.DatasetGrabber;
import au.edu.uq.cmm.paul.grabber.DatasetMetadata;
import au.edu.uq.cmm.paul.queue.AtomFeed;
import au.edu.uq.cmm.paul.queue.QueueFileException;
import au.edu.uq.cmm.paul.queue.QueueFileManager;
import au.edu.uq.cmm.paul.queue.QueueManager;
import au.edu.uq.cmm.paul.queue.QueueManager.DateRange;
import au.edu.uq.cmm.paul.queue.QueueManager.Removal;
import au.edu.uq.cmm.paul.queue.QueueManager.Slice;
import au.edu.uq.cmm.paul.status.Facility;
import au.edu.uq.cmm.paul.status.FacilityStatus;
import au.edu.uq.cmm.paul.status.FacilityStatusManager;
import au.edu.uq.cmm.paul.watcher.FileWatcher;

/**
 * The MVC controller for Paul's web UI.  This supports the status and configuration
 * pages and also implements GET access to the files in the queue area.
 * 
 * @author scrawley
 */
@Controller
public class WebUIController implements ServletContextAware {
    private static final Logger LOG = LoggerFactory.getLogger(WebUIController.class);

    private static DateTimeFormatter[] FORMATS = new DateTimeFormatter[] { ISODateTimeFormat.dateHourMinuteSecond(),
            ISODateTimeFormat.localTimeParser(), ISODateTimeFormat.localDateOptionalTimeParser(),
            ISODateTimeFormat.dateTimeParser() };

    @Autowired(required = true)
    Paul services;

    private ArrayList<BuildInfo> buildInfo;

    private ArrayList<BuildInfo> loadBuildInfo(ServletContext servletContext) {
        final ArrayList<BuildInfo> res = new ArrayList<>();
        res.add(BuildInfo.readBuildInfo("au.edu.uq.cmm", "aclslib"));
        res.add(BuildInfo.readBuildInfo("au.edu.uq.cmm", "eccles"));
        try {
            Path start = FileSystems.getDefault().getPath(servletContext.getRealPath("/META-INF/maven"));
            Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (file.getFileName().toString().equals("pom.properties")) {
                        try (InputStream is = new FileInputStream(file.toFile())) {
                            res.add(BuildInfo.readBuildInfo(is));
                        }
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException ex) {
            LOG.error("Problem loading build info");
        }
        return res;
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        LOG.debug("Setting the timezone (" + TimeZone.getDefault().getID() + ") in the servlet context");
        servletContext.setAttribute("javax.servlet.jsp.jstl.fmt.timeZone", TimeZone.getDefault());
        buildInfo = loadBuildInfo(servletContext);
    }

    @RequestMapping(value = "/control", method = RequestMethod.GET)
    public String control(Model model) {
        addStateAndStatus(model);
        return "control";
    }

    @RequestMapping(value = "/control", method = RequestMethod.POST)
    public String controlAction(Model model, HttpServletRequest request) {
        processStatusChange("watcher", request.getParameter("watcher"));
        processStatusChange("atomFeed", request.getParameter("atomFeed"));
        addStateAndStatus(model);
        return "control";
    }

    private void processStatusChange(String serviceName, String param) {
        if (param != null) {
            services.processStatusChange(serviceName, param);
        }
    }

    private void addStateAndStatus(Model model) {
        State ws = getFileWatcher().getState();
        model.addAttribute("watcherState", ws);
        model.addAttribute("watcherStatus", Status.forState(ws));
        State as = getAtomFeed().getState();
        model.addAttribute("atomFeedState", as);
        model.addAttribute("atomFeedStatus", Status.forState(as));
        model.addAttribute("resetRequired", getLatestConfig() != getConfig());
    }

    @RequestMapping(value = "/versions", method = RequestMethod.GET)
    public String versions(Model model) {
        model.addAttribute("buildInfo", buildInfo);
        return "versions";
    }

    @RequestMapping(value = "/sessions", method = RequestMethod.GET)
    public String status(Model model) {
        model.addAttribute("sessions", getFacilityStatusManager().getLatestSessions());
        return "sessions";
    }

    @RequestMapping(value = "/sessions", method = RequestMethod.POST, params = { "endSession" })
    public String endSession(Model model, @RequestParam String sessionUuid, HttpServletResponse response,
            HttpServletRequest request) throws IOException, AclsAuthenticationException {
        getFacilityStatusManager().logoutSession(sessionUuid);
        response.sendRedirect(response.encodeRedirectURL(request.getContextPath() + "/sessions"));
        return null;
    }

    @RequestMapping(value = "/facilities", method = RequestMethod.GET)
    public String facilities(Model model) {
        Collection<FacilityConfig> facilities = getFacilities();
        for (FacilityConfig fc : facilities) {
            getFacilityStatusManager().attachStatus((Facility) fc);
        }
        model.addAttribute("facilities", facilities);
        return "facilities";
    }

    @RequestMapping(value = "/facilities", method = RequestMethod.GET, params = "newForm")
    public String newFacilityForm(Model model, HttpServletRequest request) {
        model.addAttribute("facility", new Facility()); // (just for the defaults ...)
        model.addAttribute("edit", true);
        model.addAttribute("create", true);
        model.addAttribute("message", "Please fill in the form and click 'Save New Facility'");
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        return "facility";
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.GET)
    public String facilityConfig(@PathVariable String facilityName, Model model, HttpServletRequest request,
            @RequestParam(required = false) String edit) throws ConfigurationException {
        Facility facility = lookupFacilityByName(facilityName);
        model.addAttribute("facility", facility);
        if (edit != null) {
            model.addAttribute("edit", true);
            model.addAttribute("message", "Please fill in the form and click 'Save Facility Changes'");
        }
        return "facility";
    }

    @RequestMapping(value = "/facilities", method = RequestMethod.POST, params = { "create" })
    public String createFacilityConfig(Model model, HttpServletRequest request) throws ConfigurationException {
        ValidationResult<Facility> res = getConfigManager().createFacility(request.getParameterMap());
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        if (!res.isValid()) {
            model.addAttribute("edit", true);
            model.addAttribute("create", true);
            model.addAttribute("facility", res.getTarget());
            model.addAttribute("diags", res.getDiags());
            model.addAttribute("message", "Please correct the errors and try again");
            return "facility";
        } else {
            model.addAttribute("message", "Facility configuration created");
            return "ok";
        }
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.POST, params = { "copy" })
    public String copyFacilityConfig(@PathVariable String facilityName, Model model, HttpServletRequest request)
            throws ConfigurationException {
        Facility facility = lookupFacilityByName(facilityName);
        facility.setFacilityName(null);
        facility.setId(null);
        model.addAttribute("edit", true);
        model.addAttribute("create", true);
        model.addAttribute("facility", facility);
        model.addAttribute("diags", Collections.emptyMap());
        model.addAttribute("message",
                "Fill in the new facility name, " + "edit the other details and click 'Save New Facility'");
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        return "facility";
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.POST, params = { "update" })
    public String updateFacilityConfig(@PathVariable String facilityName, Model model, HttpServletRequest request)
            throws ConfigurationException {
        ValidationResult<Facility> res = getConfigManager().updateFacility(facilityName, request.getParameterMap());
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        if (!res.isValid()) {
            model.addAttribute("edit", true);
            model.addAttribute("facility", res.getTarget());
            model.addAttribute("diags", res.getDiags());
            model.addAttribute("message", "Please correct the errors and try again");
            return "facility";
        } else {
            model.addAttribute("message", "Facility configuration updated");
            return "ok";
        }
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.POST, params = { "delete" })
    public String deleteFacilityConfig(@PathVariable String facilityName, Model model, HttpServletRequest request,
            @RequestParam(required = false) String confirmed) {
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        model.addAttribute("facilityName", facilityName);
        if (confirmed == null) {
            return "facilityDeleteConfirmation";
        }
        Facility facility = lookupFacilityByName(facilityName);
        if (facility == null) {
            model.addAttribute("message", "Can't find facility configuration for '" + facilityName + "'");
            return "failed";
        }
        getFileWatcher().stopFileWatching(facility);
        getConfigManager().deleteFacility(facilityName);
        model.addAttribute("message", "Facility configuration deleted");
        return "ok";
    }

    @RequestMapping(value = "/sessions/{facilityName:.+}")
    public String facilitySessions(@PathVariable String facilityName, Model model) throws ConfigurationException {
        model.addAttribute("sessions", getFacilityStatusManager().sessionsForFacility(facilityName));
        model.addAttribute("facilityName", facilityName);
        return "facilitySessions";
    }

    @RequestMapping(value = "/queueDiagnostics/{facilityName:.+}", method = RequestMethod.GET)
    public String queueDiagnostics(@PathVariable String facilityName, Model model,
            @RequestParam(required = false) String hwmTimestamp,
            @RequestParam(required = false) String lwmTimestamp, @RequestParam(required = false) String checkHashes)
            throws ConfigurationException {
        return doCollectDiagnostics(facilityName, model, hwmTimestamp, lwmTimestamp, toBoolean(checkHashes));
    }

    private String doCollectDiagnostics(String facilityName, Model model, String hwmTimestamp, String lwmTimestamp,
            boolean checkHashes) {
        Facility facility = lookupFacilityByName(facilityName);
        FacilityStatus status = getFacilityStatusManager().getStatus(facility);
        DateRange range = getQueueManager().getQueueDateRange(facility);
        model.addAttribute("facilityName", facilityName);
        model.addAttribute("status", status);
        Date hwm = status.getGrabberHWMTimestamp();
        if (!tidy(hwmTimestamp).isEmpty()) {
            DateTime tmp = parseTimestamp(hwmTimestamp);
            if (tmp != null) {
                hwm = tmp.toDate();
            }
        }
        Date lwm = status.getGrabberLWMTimestamp();
        if (!tidy(lwmTimestamp).isEmpty()) {
            DateTime tmp = parseTimestamp(lwmTimestamp);
            if (tmp != null) {
                lwm = tmp.toDate();
            }
        }
        model.addAttribute("lwmTimestamp", lwm);
        model.addAttribute("hwmTimestamp", hwm);
        model.addAttribute("intertidal", true);
        model.addAttribute("checkHashes", checkHashes);
        model.addAttribute("analysis", new Analyser(services, facility).analyse(lwm, hwm, range, checkHashes));
        return "catchupControl";
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", params = { "reanalyse" }, method = RequestMethod.POST)
    public String reanalyse(@PathVariable String facilityName, Model model,
            @RequestParam(required = false) String lwmTimestamp,
            @RequestParam(required = false) String hwmTimestamp, @RequestParam(required = false) String checkHashes)
            throws ConfigurationException {
        return doCollectDiagnostics(facilityName, model, hwmTimestamp, lwmTimestamp, toBoolean(checkHashes));
    }

    private boolean toBoolean(String option) {
        return (option != null && option.equals("true"));
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", params = {
            "setIntertidal" }, method = RequestMethod.POST)
    public String setFacilityHWM(@PathVariable String facilityName, Model model, HttpServletRequest request,
            @RequestParam String lwmTimestamp, @RequestParam String hwmTimestamp) throws ConfigurationException {
        model.addAttribute("returnTo", inferReturnTo(request, "/facilities"));
        Facility facility = lookupFacilityByName(facilityName);
        FacilityStatus status = getFacilityStatusManager().getStatus(facility);
        if (status.getStatus() == FacilityStatusManager.Status.ON) {
            model.addAttribute("message", "Cannot change LWM and HWM while the Grabber is running.");
            return "failed";
        }
        Date oldLWM = status.getGrabberLWMTimestamp();
        Date oldHWM = status.getGrabberHWMTimestamp();
        Date lwm = parseDate(lwmTimestamp);
        Date hwm = parseDate(hwmTimestamp);
        Date now = new Date();
        if (lwm != null && hwm != null && !lwm.after(hwm) && hwm.before(now)) {
            getFacilityStatusManager().updateIntertidalTimestamp(facility, lwm, hwm);
            model.addAttribute("message", "Changed LWM / HWM for '" + facilityName + "' from " + oldLWM + " / "
                    + oldHWM + " to " + lwm + " / " + hwm);
            return "ok";
        } else {
            if (lwm == null) {
                model.addAttribute("message", "Invalid LWM timestamp");
            } else if (hwm == null) {
                model.addAttribute("message", "Invalid HWM timestamp");
            } else if (lwm.after(hwm)) {
                model.addAttribute("message", "Inconsistent timestamps: LWM &gt; HWM");
            } else if (!hwm.before(now)) {
                model.addAttribute("message", "Inconsistent timestamps: HWM in the future");
            }
            return "failed";
        }
    }

    private Date parseDate(String str) {
        str = tidy(str);
        if (!str.isEmpty()) {
            DateTime tmp = parseTimestamp(str);
            if (tmp != null) {
                return tmp.toDate();
            }
        }
        return null;
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.POST, params = { "start" })
    public String startWatcher(@PathVariable String facilityName, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        Facility facility = lookupFacilityByName(facilityName);
        if (facility != null) {
            getFileWatcher().startFileWatching(facility);
        }
        response.sendRedirect(inferReturnTo(request, "/facilities"));
        return null;
    }

    @RequestMapping(value = "/facilities/{facilityName:.+}", method = RequestMethod.POST, params = { "stop" })
    public String stopWatcher(@PathVariable String facilityName, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        Facility facility = lookupFacilityByName(facilityName);
        if (facility != null) {
            getFileWatcher().stopFileWatching(facility);
        }
        response.sendRedirect(inferReturnTo(request, "/facilities"));
        return null;
    }

    @RequestMapping(value = "/mirage", method = RequestMethod.GET)
    public String mirage(Model model, HttpServletResponse response) throws IOException {
        response.sendRedirect(getConfig().getPrimaryRepositoryUrl());
        return null;
    }

    @RequestMapping(value = "/acls", method = RequestMethod.GET)
    public String acls(Model model, HttpServletResponse response) throws IOException {
        response.sendRedirect(getConfig().getAclsUrl());
        return null;
    }

    @RequestMapping(value = "/facilitySelect", method = RequestMethod.GET)
    public String facilitySelector(Model model, HttpServletRequest request, @RequestParam String next,
            @RequestParam(required = false) String slice) {
        model.addAttribute("next", next);
        model.addAttribute("slice", inferSlice(slice));
        model.addAttribute("returnTo", inferReturnTo(request));
        model.addAttribute("facilities", getFacilities());
        return "facilitySelect";
    }

    @RequestMapping(value = "/facilitySelect", method = RequestMethod.POST)
    public String facilitySelect(Model model, @RequestParam String next,
            @RequestParam(required = false) String slice, @RequestParam(required = false) String zz,
            HttpServletRequest request, HttpServletResponse response,
            @RequestParam(required = false) String facilityName) throws UnsupportedEncodingException, IOException {
        if (facilityName == null) {
            model.addAttribute("slice", inferSlice(slice));
            model.addAttribute("returnTo", inferReturnTo(request));
            model.addAttribute("facilities", getFacilities());
            model.addAttribute("message", "Select a facility from the pulldown");
            model.addAttribute("next", next);
            return "facilitySelect";
        } else {
            response.sendRedirect(request.getContextPath() + "/" + next + "?facilityName="
                    + URLEncoder.encode(facilityName, "UTF-8") + "&slice=" + slice + "&returnTo="
                    + inferReturnTo(request));
            return null;
        }
    }

    @RequestMapping(value = "/facilityLogout")
    public String facilityLogout(Model model, HttpServletRequest request, @RequestParam String facilityName) {
        model.addAttribute("returnTo", inferReturnTo(request));
        GenericPrincipal principal = (GenericPrincipal) request.getUserPrincipal();
        if (principal == null || !principal.hasRole("ROLE_ACLS_USER")) {
            model.addAttribute("message", "I don't know your ACLS userName");
            return "failed";
        }
        String userName = principal.getName();
        FacilityStatusManager fsm = getFacilityStatusManager();
        FacilitySession session = fsm.getSession(lookupFacilityByName(facilityName), System.currentTimeMillis());
        if (session == null || !session.getUserName().equals(userName)) {
            model.addAttribute("message", "You are not logged in on '" + facilityName + "'");
            return "failed";
        }
        try {
            fsm.logoutSession(session.getSessionUuid());
            model.addAttribute("message", "Your session has been logged out");
            return "ok";
        } catch (AclsAuthenticationException ex) {
            LOG.error("Session logout failed", ex);
            model.addAttribute("message", "Session logout failed due to an internal error");
            return "failed";
        }
    }

    @RequestMapping(value = "/facilityLogin")
    public String facilityLogin(@RequestParam String facilityName,
            @RequestParam(required = false) String startSession,
            @RequestParam(required = false) String endOldSession, @RequestParam(required = false) String userName,
            @RequestParam(required = false) String account, Model model, HttpServletResponse response,
            HttpServletRequest request) throws IOException {
        FacilityStatusManager fsm = getFacilityStatusManager();
        facilityName = tidy(facilityName);
        model.addAttribute("facilityName", facilityName);
        model.addAttribute("facilities", getFacilities());
        model.addAttribute("returnTo", inferReturnTo(request));

        userName = tidy(userName);
        String password = tidy(request.getParameter("password"));
        if (startSession == null) {
            GenericPrincipal principal = (GenericPrincipal) request.getUserPrincipal();
            if (principal != null && principal.hasRole("ROLE_ACLS_USER") && principal.getPassword() != null
                    && !principal.getPassword().isEmpty()) {
                userName = principal.getName();
                password = principal.getPassword();
            }
        }
        model.addAttribute("userName", userName);
        model.addAttribute("password", password);
        if (userName.isEmpty() || password.isEmpty()) {
            // Phase 1 - user must fill in user name and password
            model.addAttribute("message", "Fill in the username and password fields");
            return "facilityLogin";
        }
        try {
            if (account == null) {
                // Phase 2 - validate user credentials and get accounts list
                List<String> accounts = null;
                if (endOldSession != null) {
                    LOG.debug("Attempting old session logout");
                    fsm.logoutFacility(facilityName);
                    LOG.debug("Logout succeeded");
                }
                LOG.debug("Attempting login");
                accounts = fsm.login(facilityName, userName, password);
                LOG.debug("Login succeeded");
                // If there is only one account, select immediately.
                if (accounts != null) {
                    if (accounts.size() == 1) {
                        fsm.selectAccount(facilityName, userName, accounts.get(0));
                        LOG.debug("Account selection succeeded");
                        ;
                        return "facilityLoggedIn";
                    } else {
                        model.addAttribute("accounts", accounts);
                        model.addAttribute("message", "Select an account to complete the login");
                    }
                }
            } else {
                // Phase 3 - after user has selected an account
                fsm.selectAccount(facilityName, userName, account);
                LOG.debug("Account selection succeeded");
                return "facilityLoggedIn";
            }
        } catch (AclsAuthenticationException ex) {
            model.addAttribute("message", "Login failed: " + ex.getMessage());
        } catch (AclsInUseException ex) {
            model.addAttribute("message", "Instrument " + ex.getFacilityName()
                    + " is currently logged in under the name of " + ex.getUserName());
            model.addAttribute("inUse", true);
        }
        return "facilityLogin";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(Model model) {
        return "login";
    }

    @RequestMapping(value = "/loginFailed")
    public String loginFailed(Model model) {
        return "loginFailed";
    }

    @RequestMapping(value = "/loggedIn", method = RequestMethod.GET)
    public String loggedIn(Model model) {
        return "loggedIn";
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public String logout(Model model) {
        return "logout";
    }

    @RequestMapping(value = "/admin", method = RequestMethod.GET)
    public String admin(Model model) {
        return "admin";
    }

    @RequestMapping(value = "/noAccess", method = RequestMethod.GET)
    public String noAccess(Model model) {
        return "noAccess";
    }

    @RequestMapping(value = "/unavailable", method = RequestMethod.GET)
    public String unavailable(Model model) {
        return "unavailable";
    }

    @RequestMapping(value = "/config", method = RequestMethod.GET)
    public String config(Model model) {
        model.addAttribute("config", getLatestConfig());
        model.addAttribute("proxyConfig", getLatestProxyConfig());
        return "config";
    }

    @RequestMapping(value = "/config", method = RequestMethod.POST, params = { "reset" })
    public String configReset(Model model, HttpServletRequest request,
            @RequestParam(required = false) String confirmed) {
        model.addAttribute("returnTo", inferReturnTo(request, "config"));
        if (confirmed == null) {
            return "resetConfirmation";
        } else {
            getConfigManager().resetConfigurations();
            model.addAttribute("message",
                    "Configuration reset succeeded.  " + "Please restart the webapp to use the updated configs");
            return "ok";
        }
    }

    @RequestMapping(value = "/queue/held", method = RequestMethod.GET)
    public String held(Model model) {
        model.addAttribute("queue", getQueueManager().getSnapshot(Slice.HELD));
        return "held";
    }

    @RequestMapping(value = "/queue/ingestible", method = RequestMethod.GET)
    public String queue(Model model) {
        model.addAttribute("queue", getQueueManager().getSnapshot(Slice.INGESTIBLE));
        return "queue";
    }

    @RequestMapping(value = "/claimDatasets", method = RequestMethod.GET)
    public String showClaimDatasets(Model model, HttpServletRequest request, HttpServletResponse response,
            @RequestParam String facilityName) throws IOException {
        model.addAttribute("facilityName", facilityName);
        model.addAttribute("returnTo", inferReturnTo(request));
        model.addAttribute("datasets", getQueueManager().getSnapshot(Slice.HELD, facilityName, true));
        return "claimDatasets";
    }

    @RequestMapping(value = "/claimDatasets", method = RequestMethod.POST, params = { "claim" })
    public String claimDatasets(Model model, HttpServletRequest request, HttpServletResponse response,
            @RequestParam(required = false) String[] ids, @RequestParam String facilityName)
            throws IOException, QueueFileException, InterruptedException {
        GenericPrincipal principal = (GenericPrincipal) request.getUserPrincipal();
        if (principal == null) {
            LOG.error("No principal ... can't proceed");
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return null;
        }
        model.addAttribute("returnTo", inferReturnTo(request));
        if (ids == null) {
            model.addAttribute("facilityName", facilityName);
            model.addAttribute("datasets", getQueueManager().getSnapshot(Slice.HELD, facilityName, true));
            model.addAttribute("message", "Check the checkboxes for the " + "Datasets you want to claim");
            return "claimDatasets";
        }
        if (!principal.hasRole("ROLE_ACLS_USER")) {
            model.addAttribute("message", "You must be logged in using " + "ACLS credentials to claim files");
            return "failed";
        }
        String userName = principal.getName();
        try {
            int nosChanged = getQueueManager().changeUser(ids, userName, false);
            model.addAttribute("message", verbiage(nosChanged, "dataset", "datasets", "claimed"));
            return "ok";
        } catch (NumberFormatException ex) {
            LOG.debug("Rejected request with bad entry id(s)");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
    }

    @RequestMapping(value = "/manageDatasets", method = RequestMethod.GET)
    public String showManageDatasets(Model model, HttpServletRequest request, HttpServletResponse response,
            @RequestParam(required = false) String slice, @RequestParam String facilityName) throws IOException {
        model.addAttribute("facilityName", facilityName);
        model.addAttribute("returnTo", inferReturnTo(request));
        Slice s = inferSlice(slice);
        model.addAttribute("slice", s);
        model.addAttribute("datasets", getQueueManager().getSnapshot(s, facilityName, true));
        model.addAttribute("userNames", getUserDetailsManager().getUserNames());
        return "manageDatasets";
    }

    private Slice inferSlice(String sliceName) {
        if (sliceName == null) {
            return null;
        } else {
            try {
                return Slice.valueOf(sliceName.toUpperCase());
            } catch (IllegalArgumentException ex) {
                LOG.debug("unrecognized slice - ignoring it");
                return Slice.ALL;
            }
        }
    }

    @RequestMapping(value = "/manageDatasets", method = RequestMethod.POST)
    public String manageDatasets(Model model, HttpServletRequest request, HttpServletResponse response,
            @RequestParam(required = false) String[] ids, @RequestParam(required = false) String userName,
            @RequestParam(required = false) String slice, @RequestParam(required = false) String confirmed,
            @RequestParam String action, @RequestParam(required = false) String facilityName)
            throws IOException, QueueFileException, InterruptedException {
        GenericPrincipal principal = (GenericPrincipal) request.getUserPrincipal();
        if (principal == null) {
            LOG.error("No principal ... can't proceed");
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return null;
        }
        if (!principal.hasRole("ROLE_ADMIN")) {
            model.addAttribute("message", "Only an administrator can manage datasets");
            return "failed";
        }
        Slice s = inferSlice(slice);
        model.addAttribute("facilityName", facilityName);
        model.addAttribute("slice", s);
        model.addAttribute("returnTo", inferReturnTo(request));
        if (action.equals("deleteAll")) {
            return deleteAll(model, request, s, facilityName, true, confirmed);
        } else if (action.equals("archiveAll")) {
            return deleteAll(model, request, s, facilityName, false, confirmed);
        } else if (action.equals("expire")) {
            return expire(model, request, s, facilityName, confirmed);
        }
        QueueManager qm = getQueueManager();
        if (ids == null) {
            return retryManage(model, "Check the checkboxes for the Datasets you want to " + action, qm, s,
                    facilityName);
        }
        try {
            int nosChanged;
            switch (action) {
            case "archive":
                nosChanged = qm.delete(ids, Removal.ARCHIVE);
                model.addAttribute("message", verbiage(nosChanged, "dataset", "datasets", "archived"));
                return "ok";
            case "delete":
                nosChanged = qm.delete(ids, Removal.DELETE);
                model.addAttribute("message", verbiage(nosChanged, "dataset", "datasets", "deleted"));
                return "ok";
            case "assign":
                try {
                    // Check the name is known
                    getUserDetailsManager().lookupUser(userName, false);
                } catch (UserDetailsException ex) {
                    return retryManage(model, "User '" + userName + "' is not known.", qm, s, facilityName);
                }
                nosChanged = qm.changeUser(ids, userName, true);
                model.addAttribute("message", verbiage(nosChanged, "dataset", "datasets", "assigned"));
                return "ok";
            default:
                LOG.debug("Rejected request with unrecognized action");
                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return null;
            }
        } catch (NumberFormatException ex) {
            LOG.debug("Rejected request with bad entry id(s)");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
    }

    private String retryManage(Model model, String message, QueueManager qm, Slice s, String facilityName) {
        model.addAttribute("datasets", qm.getSnapshot(s, facilityName, true));
        model.addAttribute("userNames", getUserDetailsManager().getUserNames());
        model.addAttribute("message", message);
        return "manageDatasets";
    }

    private String verbiage(int count, String singular, String plural, String verbed) {
        if (count == 0) {
            return "No " + plural + " " + verbed;
        } else if (count == 1) {
            return "1 " + singular + " " + verbed;
        } else {
            return count + " " + plural + " " + verbed;
        }
    }

    private String deleteAll(Model model, HttpServletRequest request, Slice slice, String facilityName,
            boolean discard, String confirmed) throws InterruptedException {
        if (confirmed == null) {
            model.addAttribute("discard", discard);
            return "queueDeleteConfirmation";
        }
        Removal removal = discard ? Removal.DELETE : Removal.ARCHIVE;
        int count = getQueueManager().deleteAll(removal, facilityName, slice);
        model.addAttribute("message",
                verbiage(count, "queue entry", "queue entries", discard ? "deleted" : "archived"));
        return "ok";
    }

    private String expire(Model model, HttpServletRequest request, Slice slice, String facilityName,
            String confirmed) throws InterruptedException {
        String mode = request.getParameter("mode");
        String olderThan = request.getParameter("olderThan");
        String age = request.getParameter("age");
        Date cutoff = determineCutoff(model, tidy(olderThan), tidy(age));
        if (cutoff == null || confirmed == null) {
            return "queueExpiryForm";
        }
        Removal removal = mode.equals("discard") ? Removal.DELETE : Removal.ARCHIVE;
        int count = getQueueManager().expireAll(removal, facilityName, slice, cutoff);

        model.addAttribute("message", verbiage(count, "queue entry", "queue entries", "expired"));
        return "ok";
    }

    @RequestMapping(value = "/datasets/{entry:.+}", method = RequestMethod.GET)
    public String queueEntry(@PathVariable String entry, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        DatasetMetadata metadata = findDataset(entry, response);
        if (metadata != null) {
            addFileStatuses(metadata);
            model.addAttribute("entry", metadata);
            model.addAttribute("returnTo", inferReturnTo(request));
            return "dataset";
        } else {
            return null;
        }
    }

    private void addFileStatuses(DatasetMetadata metadata) {
        QueueFileManager qfm = getQueueManager().getFileManager();
        for (DatafileMetadata df : metadata.getDatafiles()) {
            df.setFileStatus(qfm.getFileStatus(new File(df.getCapturedFilePathname())));
        }
    }

    @RequestMapping(value = "/datasets/{entry:.+}", params = { "regrab" }, method = RequestMethod.POST)
    public String regrabPrepare(@PathVariable String entry, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        DatasetMetadata dataset = findDataset(entry, response);
        if (dataset != null) {
            DatasetMetadata grabbedMetadata = new DatasetGrabber(services, dataset).getCandidateDataset();
            model.addAttribute("returnTo", inferReturnTo(request));
            if (grabbedMetadata != null) {
                addFileStatuses(dataset);
                addFileStatuses(grabbedMetadata);
                grabbedMetadata.updateDatasetHash();
                dataset.updateDatasetHash();
                model.addAttribute("oldEntry", dataset);
                model.addAttribute("newEntry", grabbedMetadata);
                return "regrabConfirmation";
            } else {
                model.addAttribute("message", "None of the original dataset files still exist");
                return "failed";
            }
        } else {
            return null;
        }
    }

    @RequestMapping(value = "/datasets/", params = { "grab" }, method = RequestMethod.POST)
    public String grab(Model model, @RequestParam String pathnameBase, @RequestParam String facilityName,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, InterruptedException, QueueFileException {
        model.addAttribute("returnTo", inferReturnTo(request));
        Facility facility = lookupFacilityByName(facilityName);
        DatasetGrabber dsr = new DatasetGrabber(services, new File(pathnameBase), facility);
        DatasetMetadata grabbedDataset = dsr.grabDataset();
        if (grabbedDataset != null) {
            grabbedDataset.updateDatasetHash();
            model.addAttribute("message", "Dataset grab succeeded");
            return "ok";
        } else {
            model.addAttribute("message", "Dataset grab failed");
            return "failed";
        }
    }

    @RequestMapping(value = "/datasets/{entry:.+}", params = { "regrabNew" }, method = RequestMethod.POST)
    public String regrab(@PathVariable String entry, Model model, @RequestParam String hash,
            @RequestParam String regrabNew, HttpServletRequest request, HttpServletResponse response)
            throws IOException, InterruptedException, QueueFileException {
        DatasetMetadata dataset = findDataset(entry, response);
        if (dataset != null) {
            model.addAttribute("returnTo", inferReturnTo(request));
            DatasetGrabber dsr = new DatasetGrabber(services, dataset);
            DatasetMetadata grabbedDataset = dsr.regrabDataset(regrabNew.equalsIgnoreCase("yes"));
            if (grabbedDataset == null) {
                model.addAttribute("message", "Dataset regrab failed - see logs");
                return "failed";
            }
            grabbedDataset.updateDatasetHash();
            if (!hash.equals(grabbedDataset.getCombinedDatafileHash())) {
                LOG.debug("supplied hash is " + hash);
                LOG.debug("actual hash is   " + grabbedDataset.getCombinedDatafileHash());
                model.addAttribute("message", "Dataset files were apparently changed");
                return "failed";
            } else {
                dsr.commitRegrabbedDataset(dataset, grabbedDataset, regrabNew.equalsIgnoreCase("yes"));
                model.addAttribute("message", "Dataset regrab succeeded");
                return "ok";
            }
        } else {
            return null;
        }
    }

    @RequestMapping(value = "/datasets/{entry:.+}", params = { "delete" }, method = RequestMethod.POST)
    public String delete(@PathVariable String entry, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException, InterruptedException {
        return doDelete(entry, model, request, response, true);
    }

    @RequestMapping(value = "/datasets/{entry:.+}", params = { "archive" }, method = RequestMethod.POST)
    public String archive(@PathVariable String entry, Model model, HttpServletRequest request,
            HttpServletResponse response) throws IOException, InterruptedException {
        return doDelete(entry, model, request, response, false);
    }

    private String doDelete(String entry, Model model, HttpServletRequest request, HttpServletResponse response,
            boolean discard) throws IOException, InterruptedException {
        DatasetMetadata dataset = findDataset(entry, response);
        if (dataset != null) {
            model.addAttribute("returnTo", inferReturnTo(request));
            QueueManager qm = getQueueManager();
            Removal removal = discard ? Removal.DELETE : Removal.ARCHIVE;
            int nosDeleted = qm.delete(new String[] { entry }, removal);
            if (nosDeleted == 0) {
                model.addAttribute("message", "Could not find that dataset");
                return "failed";
            } else {
                model.addAttribute("message", "Dataset #" + entry + (discard ? " deleted" : " archived"));
                return "ok";
            }
        } else {
            return null;
        }
    }

    private DatasetMetadata findDataset(String entry, HttpServletResponse response) throws IOException {
        long id;
        try {
            id = Long.parseLong(entry);
        } catch (NumberFormatException ex) {
            LOG.debug("Rejected request with bad entry id");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
        DatasetMetadata dataset = getQueueManager().fetchDataset(id);
        if (dataset == null) {
            LOG.debug("Rejected request for unknown entry");
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return null;
        }
        return dataset;
    }

    @RequestMapping(value = "/files/{fileName:.+}", method = RequestMethod.GET)
    public String file(@PathVariable String fileName, Model model, HttpServletResponse response)
            throws IOException {
        LOG.debug("Request to fetch file " + fileName);
        // This aims to prevent requests from reading files outside of the queue directory.
        // FIXME - this assumes that the directory for the queue is flat ...
        if (fileName.contains("/") || fileName.equals("..")) {
            LOG.debug("Rejected request for security reasons");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
        File file = new File(getConfig().getCaptureDirectory(), fileName);
        DatafileMetadata metadata = fetchMetadata(file);
        if (metadata == null) {
            LOG.debug("No metadata for file " + fileName);
        } else {
            LOG.debug("Found metadata for file " + fileName);
        }
        model.addAttribute("file", file);
        model.addAttribute("contentType", metadata == null ? "application/octet-stream" : metadata.getMimeType());
        return "fileView";
    }

    @RequestMapping(value = "/users", method = RequestMethod.GET)
    public String users(Model model) {
        model.addAttribute("userNames", getUserDetailsManager().getUserNames());
        return "users";
    }

    @RequestMapping(value = "/users", params = { "add" }, method = RequestMethod.POST)
    public String userAdd(@RequestParam() String userName, Model model) {
        UserDetailsManager um = getUserDetailsManager();
        try {
            um.addUser(new UserDetails(userName));
            model.addAttribute("message", "User '" + userName + "' added");
        } catch (UserDetailsException ex) {
            model.addAttribute("message", ex.getMessage());
        }
        model.addAttribute("userNames", um.getUserNames());
        return "users";
    }

    @RequestMapping(value = "/users", params = { "remove" }, method = RequestMethod.POST)
    public String userRemove(@RequestParam() String userName, Model model) {
        UserDetailsManager um = getUserDetailsManager();
        try {
            um.removeUser(userName);
            model.addAttribute("message", "User '" + userName + "' removed");
        } catch (UserDetailsException ex) {
            model.addAttribute("message", ex.getMessage());
        }
        model.addAttribute("userNames", um.getUserNames());
        return "users";
    }

    @RequestMapping(value = "/users/{userName:.+}", method = RequestMethod.GET)
    public String user(@PathVariable String userName, Model model, HttpServletResponse response)
            throws IOException {
        try {
            UserDetails userDetails = getUserDetailsManager().lookupUser(userName, true);
            model.addAttribute("user", userDetails);
        } catch (UserDetailsException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
        return "user";
    }

    private String tidy(String str) {
        return str == null ? "" : str.trim();
    }

    private String inferReturnTo(HttpServletRequest request) {
        return inferReturnTo(request, "");
    }

    private String inferReturnTo(HttpServletRequest request, String dflt) {
        String param = request.getParameter("returnTo");
        if (param == null) {
            param = dflt;
        } else {
            param = param.trim();
        }
        if (param.startsWith(request.getContextPath())) {
            return param;
        } else if (param.startsWith("/")) {
            return request.getContextPath() + param;
        } else {
            return request.getContextPath() + "/" + param;
        }
    }

    private DatafileMetadata fetchMetadata(File file) {
        EntityManager entityManager = createEntityManager();
        try {
            TypedQuery<DatafileMetadata> query = entityManager.createQuery(
                    "from DatafileMetadata d where d.capturedFilePathname = :pathName", DatafileMetadata.class);
            query.setParameter("pathName", file.getAbsolutePath());
            return query.getSingleResult();
        } catch (NoResultException ex) {
            return null;
        } finally {
            entityManager.close();
        }
    }

    private FileWatcher getFileWatcher() {
        return services.getFileWatcher();
    }

    private FacilityStatusManager getFacilityStatusManager() {
        return services.getFacilityStatusManager();
    }

    private QueueManager getQueueManager() {
        return services.getQueueManager();
    }

    private UserDetailsManager getUserDetailsManager() {
        return services.getUserDetailsManager();
    }

    private AtomFeed getAtomFeed() {
        return services.getAtomFeed();
    }

    private ConfigurationManager getConfigManager() {
        return services.getConfigManager();
    }

    private PaulConfiguration getLatestConfig() {
        return getConfigManager().getLatestConfig();
    }

    private PaulConfiguration getConfig() {
        return getConfigManager().getActiveConfig();
    }

    private EcclesProxyConfiguration getLatestProxyConfig() {
        return getConfigManager().getLatestProxyConfig();
    }

    private Facility lookupFacilityByName(String facilityName) {
        return (Facility) services.getFacilityMapper().lookup(null, facilityName, null);
    }

    private Collection<FacilityConfig> getFacilities() {
        return services.getFacilityMapper().allFacilities();
    }

    private EntityManager createEntityManager() {
        return services.getEntityManagerFactory().createEntityManager();
    }

    private Date determineCutoff(Model model, String olderThan, String age) {

        if (olderThan.isEmpty() && age.isEmpty()) {
            model.addAttribute("message", "Either an expiry date or an age must be supplied");
            return null;
        }
        String[] parts = age.split("\\s", 2);
        DateTime cutoff;
        if (olderThan.isEmpty()) {
            int value;
            try {
                value = Integer.parseInt(parts[0]);
            } catch (NumberFormatException ex) {
                model.addAttribute("message", "Age quantity is not an integer");
                return null;
            }
            BaseSingleFieldPeriod p;
            switch (parts.length == 1 ? "" : parts[1]) {
            case "minute":
            case "minutes":
                p = Minutes.minutes(value);
                break;
            case "hour":
            case "hours":
                p = Hours.hours(value);
                break;
            case "day":
            case "days":
                p = Days.days(value);
                break;
            case "week":
            case "weeks":
                p = Weeks.weeks(value);
                break;
            case "month":
            case "months":
                p = Months.months(value);
                break;
            case "year":
            case "years":
                p = Years.years(value);
                break;
            default:
                model.addAttribute("message", "Unrecognized age time-unit");
                return null;
            }
            cutoff = DateTime.now().minus(p);
        } else {
            cutoff = parseTimestamp(olderThan);
            if (cutoff == null) {
                model.addAttribute("message", "Unrecognizable expiry date");
                return null;
            }
        }
        if (cutoff.isAfter(new DateTime())) {
            model.addAttribute("message", "Supplied or computed expiry date is in the future!");
            return null;
        }
        model.addAttribute("computedDate", FORMATS[0].print(cutoff));
        return cutoff.toDate();
    }

    private DateTime parseTimestamp(String stamp) {
        for (DateTimeFormatter format : FORMATS) {
            try {
                return format.parseDateTime(stamp);
            } catch (IllegalArgumentException ex) {
                continue;
            }
        }
        return null;
    }
}