Java tutorial
/** * This file is part of the Paxle project. * Visit http://www.paxle.net for more information. * Copyright 2007-2010 the original author or authors. * * Licensed under the terms of the Common Public License 1.0 ("CPL 1.0"). * Any use, reproduction or distribution of this program constitutes the recipient's acceptance of this agreement. * The full license text is available under http://www.opensource.org/licenses/cpl1.0.txt * or in the file LICENSE.txt in the root directory of the Paxle distribution. * * Unless required by applicable law or agreed to in writing, this software is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ package org.paxle.tools.charts.impl.gui; import java.awt.Color; import java.net.URI; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.imageio.IIOException; import javax.servlet.Servlet; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.velocity.Template; import org.apache.velocity.context.Context; import org.apache.velocity.tools.view.VelocityLayoutServlet; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.data.time.Minute; import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceEvent; import org.osgi.framework.ServiceListener; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentContext; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.osgi.service.monitor.MonitorAdmin; import org.osgi.service.monitor.Monitorable; import org.osgi.service.monitor.MonitoringJob; import org.osgi.service.monitor.StatusVariable; @Component(metatype = false, immediate = true) @Service(Servlet.class) @Properties({ @Property(name = "org.paxle.servlet.path", value = "/chart"), @Property(name = "org.paxle.servlet.doUserAuth", boolValue = false) }) public class ChartServlet extends VelocityLayoutServlet implements EventHandler, ServiceListener { private static final long serialVersionUID = 1L; /** * Memory used by the java runtime */ private static final String TSERIES_MEMORY_USAGE = "java.lang.runtime/memory.used"; /** * Current size of the lucene index * @see org.paxle.se.index.IIndexSearcher#getDocCount() */ public static final String TSERIES_INDEX_SIZE = "org.paxle.lucene-db/docs.known"; /** * Total number of {@link URI}s known to the command-db */ public static final String TSERIES_CMD_SIZE_TOTAL = "org.paxle.data.cmddb/size.total"; /** * Number of {@link URI}s enqueued in the command-db */ public static final String TSERIES_CMD_SIZE_ENQUEUD = "org.paxle.data.cmddb/size.enqueued"; /** * Current PPM of the crawler * @see org.paxle.core.IMWComponent#getPPM() */ public static final String TSERIES_PPM_CRAWLER = "org.paxle.crawler/ppm"; /** * Current PPM of the parser * @see org.paxle.core.IMWComponent#getPPM() */ public static final String TSERIES_PPM_PARSER = "org.paxle.parser/ppm"; /** * Current PPM of the indexer * @see org.paxle.core.IMWComponent#getPPM() */ public static final String TSERIES_PPM_INDEXER = "org.paxle.indexer/ppm"; /** * The {@link Constants#SERVICE_PID} of the {@link Monitorable} providing * informations about the cpu usage of the jvm * * @see #TSERIES_CPU_TOTAL * @see #TSERIES_CPU_USER * @see #TSERIES_CPU_SYSTEM */ public static final String CPU_MONITORABLE_ID = "os.usage.cpu"; /** * Total CPU Usage */ public static final String TSERIES_CPU_TOTAL = CPU_MONITORABLE_ID + "/cpu.usage.total"; /** * User CPU Usage */ public static final String TSERIES_CPU_USER = CPU_MONITORABLE_ID + "/cpu.usage.user"; /** * System CPU Usage */ public static final String TSERIES_CPU_SYSTEM = CPU_MONITORABLE_ID + "/cpu.usage.system"; /** * An arraylist containing all full-path names of all {@link StatusVariable variables} * to monitor */ private static final String[] TSERIES = new String[] { TSERIES_MEMORY_USAGE, TSERIES_INDEX_SIZE, TSERIES_PPM_CRAWLER, TSERIES_PPM_PARSER, TSERIES_PPM_INDEXER, TSERIES_CPU_TOTAL, TSERIES_CPU_USER, TSERIES_CPU_SYSTEM, TSERIES_CMD_SIZE_TOTAL, TSERIES_CMD_SIZE_ENQUEUD }; private HashMap<String, HashSet<String>> variableTree; /** * A map containing the fullpath of a {@link StatusVariable} as key and the it's * {@link StatusVariable#getType() type} as {@link Integer} * * This list is required by {@link #handleEvent(Event)} to parse the received value * * @see #addVariables4Monitor(ServiceReference, HashSet, boolean) * @see #handleEvent(Event) */ private HashMap<String, Integer> typeList; /** * An OSGi bundle-context used to access other services registered to the system */ private BundleContext context; /** * Currently active monitoring job */ private MonitoringJob currentMonitorJob; /** * OSGI Monitor Admin Service */ @Reference private MonitorAdmin monitorService; /** * For logging */ private Log logger = LogFactory.getLog(this.getClass()); /** * A map containing all {@link TimeSeries} that are filled by {@link #handleEvent(Event)} */ private HashMap<String, TimeSeries> seriesMap = new HashMap<String, TimeSeries>(); /** * A map containing all available {@link JFreeChart charts} as their names */ private HashMap<String, JFreeChart> chartMap = new HashMap<String, JFreeChart>(); @Activate protected void activate(BundleContext context) { this.context = context; this.typeList = new HashMap<String, Integer>(); this.variableTree = new HashMap<String, HashSet<String>>(); this.buildVariableTree(); // create charts this.chartMap.put("mem", this.createMemoryChart()); this.chartMap.put("ppm", this.createPPMChart()); this.chartMap.put("index", this.createIndexChart()); this.chartMap.put("system", this.createCPUChart()); // registering servlet as event-handler: required to receive monitoring-events Dictionary<String, Object> properties = new Hashtable<String, Object>(); properties.put(EventConstants.EVENT_TOPIC, new String[] { "org/osgi/service/monitor" }); properties.put(EventConstants.EVENT_FILTER, String.format("(mon.listener.id=%s)", ChartServlet.class.getName())); this.context.registerService(EventHandler.class.getName(), this, properties); try { // detecting already registered monitorables and // determine which of their variables we need to monitor final HashSet<String> variableNames = new HashSet<String>(); final ServiceReference[] services = this.context.getServiceReferences(Monitorable.class.getName(), null); if (services != null) { for (ServiceReference reference : services) { this.addVariables4Monitor(reference, variableNames, true, true); } this.startScheduledJob(variableNames); } // registering this class as service-listener this.context.addServiceListener(this, String.format("(%s=%s)", Constants.OBJECTCLASS, Monitorable.class.getName())); } catch (InvalidSyntaxException e) { // this should not occur this.logger.error(e); } } @Deactivate protected void deactivate() { this.typeList.clear(); this.variableTree.clear(); this.chartMap.clear(); // TODO: } private void buildVariableTree() { for (String fullPath : TSERIES) { int idx = fullPath.indexOf('/'); String monitorableId = fullPath.substring(0, idx); String variableId = fullPath.substring(idx + 1); HashSet<String> variableIds; if (variableTree.containsKey(monitorableId)) { variableIds = variableTree.get(monitorableId); } else { variableIds = new HashSet<String>(); variableTree.put(monitorableId, variableIds); } variableIds.add(variableId); } } private JFreeChart createPPMChart() { /* * INIT TIME-SERIES */ final TimeSeriesCollection dataset = new TimeSeriesCollection(); TimeSeries crawlerPPM = new TimeSeries("Crawler PPM", Minute.class); crawlerPPM.setMaximumItemAge(24 * 60); dataset.addSeries(crawlerPPM); this.seriesMap.put(TSERIES_PPM_CRAWLER, crawlerPPM); TimeSeries parserPPM = new TimeSeries("Parser PPM", Minute.class); parserPPM.setMaximumItemAge(24 * 60); dataset.addSeries(parserPPM); this.seriesMap.put(TSERIES_PPM_PARSER, parserPPM); TimeSeries indexerPPM = new TimeSeries("Indexer PPM", Minute.class); indexerPPM.setMaximumItemAge(24 * 60); dataset.addSeries(indexerPPM); this.seriesMap.put(TSERIES_PPM_INDEXER, indexerPPM); /* * INIT CHART */ JFreeChart chart = ChartFactory.createTimeSeriesChart(null, "Time", "PPM", dataset, true, false, false); // change axis data format ((DateAxis) chart.getXYPlot().getDomainAxis()).setDateFormatOverride(new SimpleDateFormat("HH:mm")); chart.setBackgroundPaint(Color.WHITE); return chart; } private JFreeChart createCPUChart() { final TimeSeriesCollection dataset = new TimeSeriesCollection(); TimeSeries series = null; series = new TimeSeries("Total CPU Usage", Minute.class); series.setMaximumItemAge(24 * 60); dataset.addSeries(series); this.seriesMap.put(TSERIES_CPU_TOTAL, series); series = new TimeSeries("User CPU Usage", Minute.class); series.setMaximumItemAge(24 * 60); dataset.addSeries(series); this.seriesMap.put(TSERIES_CPU_USER, series); series = new TimeSeries("System CPU Usage", Minute.class); series.setMaximumItemAge(24 * 60); dataset.addSeries(series); this.seriesMap.put(TSERIES_CPU_SYSTEM, series); // init chart JFreeChart chart = ChartFactory.createTimeSeriesChart(null, "Time", "Usage [%]", dataset, true, false, false); // change axis data format ((DateAxis) chart.getXYPlot().getDomainAxis()).setDateFormatOverride(new SimpleDateFormat("HH:mm")); chart.getXYPlot().getRangeAxis().setRange(0, 100); chart.getXYPlot().setNoDataMessage("No data available"); chart.setBackgroundPaint(Color.WHITE); return chart; } private JFreeChart createIndexChart() { // init Time-Series TimeSeries indexSizeSeries = new TimeSeries("Index Size", Minute.class); indexSizeSeries.setMaximumItemAge(24 * 60); this.seriesMap.put(TSERIES_INDEX_SIZE, indexSizeSeries); // init chart JFreeChart chart = ChartFactory.createTimeSeriesChart(null, "Time", "#Docs", new TimeSeriesCollection(indexSizeSeries), true, false, false); XYPlot plot = chart.getXYPlot(); final TimeSeriesCollection linksDataset = new TimeSeriesCollection(); TimeSeries totalLinksSeries = new TimeSeries("Total URI", Minute.class); totalLinksSeries.setMaximumItemAge(24 * 60); linksDataset.addSeries(totalLinksSeries); this.seriesMap.put(TSERIES_CMD_SIZE_TOTAL, totalLinksSeries); TimeSeries enqueuedLinksSeries = new TimeSeries("Enqueued URI", Minute.class); enqueuedLinksSeries.setMaximumItemAge(24 * 60); linksDataset.addSeries(enqueuedLinksSeries); this.seriesMap.put(TSERIES_CMD_SIZE_ENQUEUD, enqueuedLinksSeries); NumberAxis axis2 = new NumberAxis("#Links"); axis2.setAutoRangeIncludesZero(false); axis2.setNumberFormatOverride(new DecimalFormat("#,##0")); plot.setRangeAxis(1, axis2); plot.setDataset(1, linksDataset); plot.setRenderer(1, new StandardXYItemRenderer()); plot.mapDatasetToRangeAxis(1, 1); NumberAxis axis1 = (NumberAxis) plot.getRangeAxis(0); axis1.setNumberFormatOverride(new DecimalFormat("#,##0")); // change axis date format ((DateAxis) plot.getDomainAxis()).setDateFormatOverride(new SimpleDateFormat("HH:mm")); chart.setBackgroundPaint(Color.WHITE); return chart; } private JFreeChart createMemoryChart() { // init time series TimeSeries usedmemSeries = new TimeSeries("Used MEM", Minute.class); usedmemSeries.setMaximumItemAge(24 * 60); this.seriesMap.put(TSERIES_MEMORY_USAGE, usedmemSeries); // init collections and chart final TimeSeriesCollection dataset = new TimeSeriesCollection(); dataset.addSeries(usedmemSeries); /* * INIT CHART */ JFreeChart chart = ChartFactory.createTimeSeriesChart(null, "Time", "Memory [MB]", dataset, true, false, false); // change axis data format ((DateAxis) chart.getXYPlot().getDomainAxis()).setDateFormatOverride(new SimpleDateFormat("HH:mm")); long maxMemory = Runtime.getRuntime().maxMemory(); if (maxMemory != Long.MAX_VALUE) { ((NumberAxis) chart.getXYPlot().getRangeAxis()).setRange(0, (double) maxMemory / (double) (1024 * 1024)); } chart.setBackgroundPaint(Color.WHITE); return chart; } @Override protected void doRequest(HttpServletRequest request, HttpServletResponse response) { String chartType = null; try { String display = request.getParameter("display"); chartType = request.getParameter("t"); if ((display == null || display.equals("plain")) && (chartType != null)) { // getting requested graph size String wStrg = request.getParameter("w"); int width = Integer.parseInt(wStrg == null ? "385" : wStrg); String hStrg = request.getParameter("h"); int height = Integer.parseInt(hStrg == null ? "200" : hStrg); // getting the reuested chart JFreeChart chart = this.chartMap.get(chartType); if (chart == null) { response.setStatus(404); return; } // set response type response.setContentType("image/png"); // render image ServletOutputStream out = response.getOutputStream(); ChartUtilities.writeChartAsPNG(out, chart, width, height); out.flush(); } else { // using velocity template super.doRequest(request, response); } } catch (Exception e) { if (e instanceof IIOException && e.getCause() != null && e.getCause().getCause() != null && e.getCause().getCause().getMessage() != null && e.getCause().getCause().getMessage().equalsIgnoreCase("Broken pipe")) { this.logger.debug(String.format("Broken pipe while writing chart '%s'.", chartType)); } else if (e.getClass().getName().equals("org.mortbay.jetty.EofException")) { //c.f. bug #293 this.logger.debug(String.format("Broken connection while writing chart '%s'.", chartType)); } else { this.logger.error(String.format("Unexpected '%s' while writing chart '%s'.", e.getClass().getName(), chartType), e); } } } @Override protected void fillContext(Context context, HttpServletRequest request) { context.put("chartServlet", this); context.put("chartMap", this.chartMap); context.put("chartTypeMap", this.chartMap.keySet()); } /** * Choosing the template to use */ @Override protected Template getTemplate(HttpServletRequest request, HttpServletResponse response) { return this.getTemplate("/resources/templates/ChartServlet.vm"); } /** * @see EventHandler#handleEvent(Event) */ public void handleEvent(Event event) { String pid = (String) event.getProperty("mon.monitorable.pid"); String name = (String) event.getProperty("mon.statusvariable.name"); Object value = event.getProperty("mon.statusvariable.value"); final String fullPath = pid + "/" + name; this.updateValue(fullPath, value); } /** * Function to update charts with the newly received value * @param fullPath the full-name of the changed {@link StatusVariable} * @param value the value of a {@link StatusVariable} received via event */ private void updateValue(String fullPath, Object value) { final TimeSeries series = this.seriesMap.get(fullPath); if (series != null) { Number num = null; /* * According to the MA spec the values * must be delivered as String */ if (value instanceof String) { Integer type = this.typeList.get(fullPath); if (type != null) { switch (type.intValue()) { case StatusVariable.TYPE_FLOAT: num = Float.valueOf((String) value); break; case StatusVariable.TYPE_INTEGER: num = Integer.valueOf((String) value); break; // these types are not supported by the chart-servlet for now /* case StatusVariable.TYPE_BOOLEAN: case StatusVariable.TYPE_STRING: */ default: break; } } } /* * XXX: this is a bug of the apache felix implementation. * As defined in the OSGi spec, the value should be delivered * as String */ else if (value instanceof Number) { num = (Number) value; } else { this.logger.warn(String.format("Unexpected type of monitoring variable '%s': %s", value.getClass().getSimpleName(), fullPath)); } if (num != null) { if (fullPath.equalsIgnoreCase(TSERIES_MEMORY_USAGE)) { /* * the memory-usage value is in KB. * See org.paxle.core.monitorable.provider.impl.RuntimeMemoryMonitoring for details */ num = Integer.valueOf(num.intValue() / 1024); } else if (fullPath.startsWith(CPU_MONITORABLE_ID)) { num = new Double(num.doubleValue() * 100f); } series.addOrUpdate(new Minute(new Date()), num); } } } /** * @see ServiceListener#serviceChanged(ServiceEvent) */ public void serviceChanged(ServiceEvent event) { // getting the service reference ServiceReference reference = event.getServiceReference(); this.serviceChanged(reference, event.getType()); } private void serviceChanged(ServiceReference reference, int eventType) { if (reference == null) return; if (eventType == ServiceEvent.MODIFIED) return; // ignoring unknown services String pid = (String) reference.getProperty(Constants.SERVICE_PID); if (!variableTree.containsKey(pid)) return; // getting currently monitored variables final HashSet<String> currentVariableNames = new HashSet<String>(); if (this.currentMonitorJob != null) { String[] temp = this.currentMonitorJob.getStatusVariableNames(); if (temp != null) { currentVariableNames.addAll(Arrays.asList(temp)); } try { // stopping old monitoring-job this.currentMonitorJob.stop(); } catch (NullPointerException e) { // XXX this is a bug in the MA implementation and should be ignored for now this.logger.debug(e); } this.currentMonitorJob = null; } // getting variables of changed service final HashSet<String> diffVariableNames = new HashSet<String>(); this.addVariables4Monitor(reference, diffVariableNames, eventType == ServiceEvent.REGISTERED, eventType == ServiceEvent.REGISTERED); if (eventType == ServiceEvent.REGISTERED) { // adding new variable currentVariableNames.addAll(diffVariableNames); } else if (eventType == ServiceEvent.UNREGISTERING) { currentVariableNames.removeAll(diffVariableNames); } // restarting monitoring job this.startScheduledJob(currentVariableNames); } /** * Add the full-path of all {@link StatusVariable variables} of the given {@link Monitorable} into the set * @param reference a reference to a {@link Monitorable} * @param variableNames the set where the variable-names should be appended * @param determineType if <code>true</code> the type of the {@link StatusVariable} is determined via call to * {@link StatusVariable#getType()} and stored into {@link #typeList} * @param updateCharts if determineType was enabled and this is <code>true</code> the current value of the {@link StatusVariable} * is used to update the chart via call to {@link #updateValue(String, Object)} */ private void addVariables4Monitor(ServiceReference reference, HashSet<String> variableNames, boolean determineType, boolean updateCharts) { String pid = (String) reference.getProperty(Constants.SERVICE_PID); if (!variableTree.containsKey(pid)) return; for (String name : variableTree.get(pid)) { final String fullPath = pid + "/" + name; // append full-path variableNames.add(fullPath); // getting the type of the status-variable if (determineType) { try { Monitorable mon = (Monitorable) this.context.getService(reference); StatusVariable var = mon.getStatusVariable(name); Integer type = Integer.valueOf(var.getType()); this.typeList.put(fullPath, type); // updating charts if requested if (updateCharts) { String value = null; switch (type.intValue()) { case StatusVariable.TYPE_FLOAT: value = Float.toString(var.getFloat()); break; case StatusVariable.TYPE_INTEGER: value = Integer.toString(var.getInteger()); break; case StatusVariable.TYPE_BOOLEAN: value = Boolean.toString(var.getBoolean()); break; case StatusVariable.TYPE_STRING: value = var.getString(); break; default: break; } this.updateValue(fullPath, value); } } catch (Exception e) { this.logger.error( String.format("Unexpected '%s' while trying to determine type of statusvariable '%s'.", e.getClass().getName(), fullPath), e); } } } } /** * Starting a new monitoring job with the given variables to monitor * @param variableNames full-path of the {@link StatusVariable variables} to monitor */ private void startScheduledJob(Set<String> variableNames) { if (variableNames.size() == 0) return; this.currentMonitorJob = this.monitorService.startScheduledJob(ChartServlet.class.getName(), // listener.id variableNames.toArray(new String[variableNames.size()]), 60, // seconds 0 // Forever ); } @SuppressWarnings("unchecked") public Iterator<RegularTimePeriod> getMergedPeriods(List<TimeSeries> seriesList) { TreeSet<RegularTimePeriod> periods = new TreeSet<RegularTimePeriod>(); for (TimeSeries series : seriesList) { if (series.getItemCount() == 0) continue; periods.addAll(series.getTimePeriods()); } return periods.iterator(); } }