/*
* 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 > 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;
}
}