Java tutorial
/* * Copyright 2011 Greg Haines * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.greghaines.jesque.web.controller; import static net.greghaines.jesque.utils.ResqueConstants.COLON; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import net.greghaines.jesque.Config; import net.greghaines.jesque.meta.KeyInfo; import net.greghaines.jesque.meta.KeyType; import net.greghaines.jesque.meta.QueueInfo; import net.greghaines.jesque.meta.WorkerInfo; import net.greghaines.jesque.meta.dao.FailureDAO; import net.greghaines.jesque.meta.dao.KeysDAO; import net.greghaines.jesque.meta.dao.QueueInfoDAO; import net.greghaines.jesque.meta.dao.WorkerInfoDAO; import net.greghaines.jesque.utils.JesqueUtils; import net.greghaines.jesque.utils.ResqueDateFormatThreadLocal; import net.greghaines.jesque.utils.VersionUtils; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.ModelAndView; import redis.clients.jedis.exceptions.JedisConnectionException; @Controller("jesqueController") public class JesqueController { private static final List<String> tabs = Arrays.asList("Overview", "Working", "Failed", "Queues", "Workers", "Stats"); private static final List<String> statsSubTabs = Arrays.asList("resque", "redis", "keys"); private static final Pattern whitespacePattern = Pattern.compile("\\s+"); @Resource private Config config; @Resource private FailureDAO failureDAO; @Resource private KeysDAO keysDAO; @Resource private QueueInfoDAO queueInfoDAO; @Resource private WorkerInfoDAO workerInfoDAO; private String redisURI; @PostConstruct public void buildRedisURI() { this.redisURI = this.config.getURI(); } @ExceptionHandler @ResponseStatus(SERVICE_UNAVAILABLE) public ModelAndView connectError(final JedisConnectionException exception) { return errorModelAndView("error", exception, SERVICE_UNAVAILABLE); } @ExceptionHandler @ResponseStatus(INTERNAL_SERVER_ERROR) public ModelAndView genericError(final Exception exception) { return errorModelAndView("error", exception, INTERNAL_SERVER_ERROR); } private static ModelAndView errorModelAndView(final String viewName, final Throwable t, final HttpStatus status) { final ModelAndView model = new ModelAndView(viewName); model.addObject("errorCode", status.value()); model.addObject("errorName", toNiceCase(status.name())); model.addObject("errorType", t.getClass().getName()); model.addObject("errorMessage", t.getMessage()); model.addObject("stackTrace", JesqueUtils.createBacktrace(t).toArray(new String[0])); return model; } private static String toNiceCase(final String orig) { final String[] tmpStrs = whitespacePattern.split(orig.replace('_', ' ').trim()); final StringBuilder sb = new StringBuilder(orig.length()); String prefix = ""; for (final String tmpStr : tmpStrs) { if (tmpStr.length() > 0) { sb.append(prefix).append(tmpStr.substring(0, 1).toUpperCase()) .append(tmpStr.substring(1).toLowerCase()); } prefix = " "; } return sb.toString(); } @RequestMapping(value = "/index", method = GET) public String index() { return "redirect:/overview"; } @RequestMapping(value = "/failed", method = GET) public String failed(@RequestParam(value = "start", defaultValue = "0") final long offset, @RequestParam(value = "count", defaultValue = "20") final long count, final Model model) { addHeaderAttributes(model, "Failed", null, null); model.addAttribute("start", offset); model.addAttribute("count", count); model.addAttribute("fullFailureCount", this.failureDAO.getCount()); model.addAttribute("failures", this.failureDAO.getFailures(offset, count)); return "failed"; } @RequestMapping(value = "/failed/clear", method = POST) public String failedClear() { this.failureDAO.clear(); return "redirect:/failed"; } @RequestMapping(value = "/failed/remove/{index}", method = GET) public String failedRemove(@PathVariable("index") final long index) { this.failureDAO.remove(index); return "redirect:/failed"; } @RequestMapping(value = "/failed/requeue/{index}", method = GET) public String failedRequeue(@PathVariable("index") final long index) { this.failureDAO.requeue(index); return "redirect:/failed"; } @RequestMapping(value = "/failed/requeue/{index}", method = GET, headers = "X-Requested-With=XMLHttpRequest") public void failedRequeueXHR(@PathVariable("index") final long index, final HttpServletResponse resp) throws IOException { final Date retriedAt = this.failureDAO.requeue(index); final PrintWriter pw = resp.getWriter(); pw.print((retriedAt == null) ? "ERROR" : ResqueDateFormatThreadLocal.getInstance().format(retriedAt)); pw.flush(); pw.close(); } @RequestMapping(value = "/overview", method = GET) public String overview(final Model model) { addHeaderAttributes(model, "Overview", null, null); addQueuesAttributes(model); addWorkingAttributes(model); addPollController(model, "overview", false); return "overview"; } @RequestMapping(value = "/overview.poll", method = GET) public String overviewPoll(final Model model) { addQueuesAttributes(model); addWorkingAttributes(model); addPollController(model, "overview", true); return "overview"; } @RequestMapping(value = "/queues", method = GET) public String queues(final Model model) { addHeaderAttributes(model, "Queues", null, null); addQueuesAttributes(model); return "queues"; } @RequestMapping(value = "/queues/{queueName}", method = GET) public String queues(@PathVariable("queueName") final String queueName, @RequestParam(value = "start", defaultValue = "0") final long offset, @RequestParam(value = "count", defaultValue = "20") final long count, final Model model) { addHeaderAttributes(model, "Queues", this.queueInfoDAO.getQueueNames(), queueName); model.addAttribute("start", offset); model.addAttribute("count", count); final QueueInfo queueInfo = this.queueInfoDAO.getQueueInfo(queueName, offset, count); if (queueInfo == null) { model.addAttribute("queueName", queueName); } else { model.addAttribute("queue", queueInfo); } return "queues-detail"; } @RequestMapping(value = "/queues/{queueName}/remove", method = POST) public String queues(@PathVariable("queueName") final String queueName) { this.queueInfoDAO.removeQueue(queueName); return "redirect:/queues"; } @RequestMapping(value = "/stats", method = GET) public String stats(final Model model) { return "redirect:/stats/resque"; } @RequestMapping(value = "/stats/{statType}", method = GET) public String stats(@PathVariable("statType") final String statType, final Model model) { if ("resque".equals(statType)) { addHeaderAttributes(model, "Stats", statsSubTabs, "resque"); model.addAttribute("title", "Resque Client connected to " + this.redisURI); model.addAttribute("stats", createResqueStats()); } else if ("redis".equals(statType)) { addHeaderAttributes(model, "Stats", statsSubTabs, "redis"); model.addAttribute("title", this.redisURI); model.addAttribute("stats", this.keysDAO.getRedisInfo()); } else if ("keys".equals(statType)) { addHeaderAttributes(model, "Stats", statsSubTabs, "keys"); model.addAttribute("title", "Keys owned by Resque Client connected to " + this.redisURI); model.addAttribute("subTitle", "(All keys are actually prefixed with \"" + this.config.getNamespace() + COLON + "\")"); model.addAttribute("keys", this.keysDAO.getKeyInfos()); } return "stats"; } @RequestMapping(value = "/stats.txt", method = GET) public void statsTxt(final HttpServletResponse resp) throws IOException { final Map<String, Object> resqueStats = createResqueStats(); final List<QueueInfo> queueInfos = this.queueInfoDAO.getQueueInfos(); resp.setContentType("text/html"); final PrintWriter pw = resp.getWriter(); pw.println("resque.pending=" + resqueStats.get("pending")); pw.println("resque.processed=" + resqueStats.get("processed")); pw.println("resque.failed=" + resqueStats.get("failed")); pw.println("resque.workers=" + resqueStats.get("workers")); pw.println("resque.working=" + resqueStats.get("working")); for (final QueueInfo queueInfo : queueInfos) { pw.printf("queues.%s=%d%n", queueInfo.getName(), queueInfo.getSize()); } pw.flush(); pw.close(); } private Map<String, Object> createResqueStats() { final Map<String, Object> resqueStats = new LinkedHashMap<String, Object>(); resqueStats.put("environment", "development"); resqueStats.put("failed", this.failureDAO.getCount()); resqueStats.put("pending", this.queueInfoDAO.getPendingCount()); resqueStats.put("processed", this.queueInfoDAO.getProcessedCount()); resqueStats.put("queues", this.queueInfoDAO.getQueueNames().size()); resqueStats.put("servers", "[\"" + this.redisURI + "\"]"); resqueStats.put("workers", this.workerInfoDAO.getWorkerCount()); resqueStats.put("working", this.workerInfoDAO.getActiveWorkerCount()); resqueStats.put("paused", this.workerInfoDAO.getPausedWorkerCount()); return resqueStats; } @RequestMapping(value = "/stats/keys/{key}", method = GET) public String statsKey(@PathVariable("key") final String key, @RequestParam(value = "start", defaultValue = "0") final int offset, @RequestParam(value = "count", defaultValue = "20") final int count, final Model model) { addHeaderAttributes(model, "Stats", statsSubTabs, "keys"); model.addAttribute("start", offset); model.addAttribute("count", count); final KeyInfo keyInfo = this.keysDAO.getKeyInfo(this.config.getNamespace() + COLON + key, offset, count); if (keyInfo == null) { model.addAttribute("keyName", key); } else { model.addAttribute("key", keyInfo); } return (keyInfo == null || KeyType.STRING.equals(keyInfo.getType())) ? "key-string" : "key-sets"; } @RequestMapping(value = "/workers", method = GET) public String workers(final Model model) { addHeaderAttributes(model, "Workers", null, null); return addWorkersAttributes(model, false); } @RequestMapping(value = "/workers.poll", method = GET) public String workersPoll(final Model model) { return addWorkersAttributes(model, true); } @RequestMapping(value = "/workers/{workerName}", method = GET) public String workers(@PathVariable("workerName") final String workerName, final Model model) { final WorkerValues wv = addWorkersAttributes(workerName, model, false); addHeaderAttributes(model, "Workers", wv.getSubTabs(), wv.getActiveSubTab()); return wv.getViewName(); } @RequestMapping(value = "/workers/{workerName}.poll", method = GET) public String workersPoll(@PathVariable("workerName") final String workerName, final Model model) { return addWorkersAttributes(workerName, model, true).getViewName(); } @RequestMapping(value = "/working", method = GET) public String working(final Model model) { addHeaderAttributes(model, "Working", null, null); addWorkingAttributes(model); return "working"; } @RequestMapping(value = "/working/{workerName}", method = GET) public String working(@PathVariable("workerName") final String workerName, final Model model) { addHeaderAttributes(model, "Working", null, null); model.addAttribute("worker", this.workerInfoDAO.getWorker(workerName)); return "working-detail"; } private String addWorkersAttributes(final Model model, final boolean poll) { final Map<String, List<WorkerInfo>> hostMap = this.workerInfoDAO.getWorkerHostMap(); final String viewName; if (hostMap.size() == 1) { model.addAttribute("workers", combineWorkerInfos(hostMap)); addPollController(model, "workers", poll); viewName = "workers"; } else { model.addAttribute("hostMap", hostMap); model.addAttribute("totalWorkerCount", totalWorkerInfoCount(hostMap)); viewName = "workers-hosts"; } return viewName; } private WorkerValues addWorkersAttributes(final String workerName, final Model model, final boolean poll) { final WorkerInfo workerInfo = this.workerInfoDAO.getWorker(workerName); final Map<String, List<WorkerInfo>> hostMap = this.workerInfoDAO.getWorkerHostMap(); final String activeSubTab; final String viewName; if (workerInfo != null) { // Display workers detail activeSubTab = workerInfo.getHost(); viewName = "workers-detail"; model.addAttribute("worker", workerInfo); } else if (!hostMap.containsKey(workerName) && !"all".equalsIgnoreCase(workerName)) { // Unknown worker name activeSubTab = null; viewName = "workers-detail"; } else { // Display a list of workers viewName = "workers"; addPollController(model, workerName, poll); if ("all".equalsIgnoreCase(workerName)) { activeSubTab = null; model.addAttribute("workers", combineWorkerInfos(hostMap)); } else { activeSubTab = workerName; model.addAttribute("workers", hostMap.get(workerName)); } } final List<String> subTabs = (hostMap.size() > 1) ? new ArrayList<String>(hostMap.keySet()) : null; return new WorkerValues(activeSubTab, viewName, subTabs); } private void addWorkingAttributes(final Model model) { model.addAttribute("totalWorkerCount", this.workerInfoDAO.getWorkerCount()); model.addAttribute("working", this.workerInfoDAO.getActiveWorkers()); } private void addQueuesAttributes(final Model model) { model.addAttribute("queues", this.queueInfoDAO.getQueueInfos()); model.addAttribute("totalFailureCount", this.failureDAO.getCount()); } private void addHeaderAttributes(final Model model, final String activeTab, final List<String> subTabs, final String activeSubTab) { model.addAttribute("tabs", tabs); model.addAttribute("activeTab", activeTab); if (subTabs != null) { model.addAttribute("subTabs", subTabs); model.addAttribute("activeSubTab", activeSubTab); } model.addAttribute("namespace", this.config.getNamespace()); model.addAttribute("redisUri", this.redisURI); model.addAttribute("version", VersionUtils.getVersion()); } private static void addPollController(final Model model, final String path, final boolean poll) { final StringBuilder sb = new StringBuilder(64); sb.append("<p class=\"poll\">"); if (poll) { sb.append("Last Updated: ").append(new SimpleDateFormat("HH:mm:ss").format(new Date())); } else { sb.append("<a href=\"").append(path).append(".poll\" rel=\"poll\">Live Poll</a>"); } sb.append("</p>"); model.addAttribute("pollController", sb.toString()); model.addAttribute("poll", poll); } private static List<WorkerInfo> combineWorkerInfos(final Map<?, List<WorkerInfo>> hostMap) { final List<WorkerInfo> allWorkers = new LinkedList<WorkerInfo>(); for (final List<WorkerInfo> hostWorkers : hostMap.values()) { allWorkers.addAll(hostWorkers); } return allWorkers; } private static long totalWorkerInfoCount(final Map<?, List<WorkerInfo>> hostMap) { long count = 0; for (final List<WorkerInfo> workerInfos : hostMap.values()) { count += workerInfos.size(); } return count; } private static final class WorkerValues { private final String activeSubTab; private final String viewName; private final List<String> subTabs; public WorkerValues(final String activeSubTab, final String viewName, final List<String> subTabs) { this.activeSubTab = activeSubTab; this.viewName = viewName; this.subTabs = subTabs; } public String getActiveSubTab() { return this.activeSubTab; } public String getViewName() { return this.viewName; } public List<String> getSubTabs() { return this.subTabs; } } }