All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bigdata.counters.query.URLQueryModel Maven / Gradle / Ivy

/*

Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     [email protected]

This program 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; version 2 of the License.

This program 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/
/*
 * Created on May 26, 2009
 */

package com.bigdata.counters.query;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.bigdata.counters.History;
import com.bigdata.counters.ICounterSet;
import com.bigdata.counters.PeriodEnum;
import com.bigdata.counters.httpd.CounterSetHTTPD;
import com.bigdata.service.Event;
import com.bigdata.service.IEventReportingService;
import com.bigdata.service.IService;
import com.bigdata.util.CaseInsensitiveStringComparator;
import com.bigdata.util.httpd.NanoHTTPD;

/**
 * The model for a URL used to query an {@link ICounterSelector}.
 * 
 * @author Bryan Thompson
 * @version $Id$
 */
public class URLQueryModel {

    private static transient final Logger log = Logger.getLogger(URLQueryModel.class);
    
    /**
     * Name of the URL query parameter specifying the starting path for the page
     * view.
     */
    public static final String PATH = "path";

    /**
     * Depth to be displayed from the given path -or- ZERO (0) to display
     * all levels.
     */
    public static final String DEPTH = "depth";
    
    /**
     * @see BLZG-1318
     */
    public static final String DEFAULT_DEPTH = "0";
    
    /**
     * URL query parameter whose value is the type of report to generate.
     * The default is {@link ReportEnum#hierarchy}.
     * 
     * @see ReportEnum
     */
    public static final String REPORT = "report";
    
    /**
     * The ordered labels to be assigned to the category columns in a
     * {@link ReportEnum#pivot} report. The order of the names in the URL
     * query parameters MUST correspond with the order of the capturing
     * groups in the {@link #REGEX}.
     */
    public static final String CATEGORY = "category";
    
    /**
     * Name of the URL query parameter specifying whether the optional
     * correlated view for counter histories will be displayed.
     * 

* Note: This is a shorthand for specifying {@link #REPORT} as * {@value ReportEnum#correlated}. */ public static final String CORRELATED = "correlated"; /** * Name of the URL query parameter specifying one or more strings for * the filter to be applied to the counter paths. */ public static final String FILTER = "filter"; /** * Name of the URL query parameter specifying one or more regular * expression for the filter to be applied to the counter paths. Any * capturing groups in this regular expression will be used to generate * the column title when examining correlated counters in a table view. * If there are no capturing groups then the counter name is used as the * default title. */ public static final String REGEX = "regex"; /** * Name of the URL query parameter specifying that the format for the first * column of the history counter table view. This column is the timestamp * associated with the counter but it can be reported in a variety of ways. * The possible values for this option are specified by * {@link TimestampFormatEnum}. * * @see TimestampFormatEnum * * @todo add support for elapsed period units since the fromTime, since a * specified time, or since the federation up time. */ public static final String TIMESTAMP_FORMAT = "timestampFormat"; /** * The reporting period to be displayed. When not specified, all periods * will be reported. The value may be any {@link PeriodEnum}. */ public static final String PERIOD = "period"; /** * Optional override of the MIME type from a URL query parameter. */ public static final String MIMETYPE = "mimeType"; /** * Parameter recognized as the name of the local file on which to render the * counters (this option is supported only by utility classes run from a * command line, not by the httpd interface). */ public static final String FILE = "file"; /** * A collection of event filters. Each filter is a regular expression. * The key is the {@link Event} {@link Field} to which the filter will * be applied. The events filters are specified using URL query * parameters having the general form: events.column=regex. * For example, * *

     * events.majorEventType = AsynchronousOverflow
     * 
* * would select just the asynchronous overflow events and * *
     * events.hostname=blade12.*
     * 
* * would select events reported for blade12. */ public final HashMap eventFilters = new HashMap(); /** * The eventOrderBy=fld URL query parameters specifies * the sequence in which events should be grouped. The value of the * query parameter is an ordered list of the names of {@link Event} * {@link Field}s. For example: * *
     * eventOrderBy=majorEventType & eventOrderOrderBy=hostname
     * 
* * would group the events first by the major event type and then by the * hostname. All events for the same {@link Event#majorEventType} and * the same {@link Event#hostname} would appear on the same Y value. *

* If no value is specified for this URL query parameter then the * default is as if {@link Event#hostname} was specified. */ static final String EVENT_ORDER_BY = "eventOrderBy"; /** * The order in which the events will be grouped. * * @see #EVENT_ORDER_BY */ public final Field[] eventOrderBy; /** * The URI from the request. */ final public String uri; /** * The parameters from the request (as parsed from URL query parameters). */ final public LinkedHashMap> params; // /** // * The request headers. // */ // final public Map headers; /** * The reconstructed request URL. */ private final String requestURL; /** * The value of the {@link #PATH} query parameter. */ final public String path; /** * The value of the {@link #DEPTH} query parameter. */ final public int depth; /** * The kind of report to generate. * * @see #REPORT * @see ReportEnum */ final public ReportEnum reportType; /** * @see #TIMESTAMP_FORMAT * @see TimestampFormatEnum */ final public TimestampFormatEnum timestampFormat; /** * The ordered labels to be assigned to the category columns in a * {@link ReportEnum#pivot} report (optional). The order of the names in * the URL query parameters MUST correspond with the order of the * capturing groups in the {@link #REGEX}. * * @see #CATEGORY */ final public String[] category; /** * The inclusive lower bound in milliseconds of the timestamp for the * counters or events to be selected. */ final public long fromTime; /** * The exclusive upper bound in milliseconds of the timestamp for the * counters or events to be selected. */ final public long toTime; /** * The reporting period to be used. When null all periods * will be reported. When specified, only that period is reported. */ final public PeriodEnum period; /** * The {@link Pattern} compiled from the {@link #FILTER} query * parameters and null iff there are no {@link #FILTER} * query parameters. */ final public Pattern pattern; /** * The events iff they are available from the service. * * @see IEventReportingService */ final public IEventReportingService eventReportingService; /** * true iff we need to output the scripts to support * flot. */ final public boolean flot; /** * Used to format double and float counter values. */ public final DecimalFormat decimalFormat; /** * Used to format counter values that can be inferred to be a percentage. */ public final NumberFormat percentFormat; /** * Used to format integer and long counter values. */ public final NumberFormat integerFormat; /** * Used to format the units of time when expressed as elapsed units since * the first sample of a {@link History}. */ public final DecimalFormat unitsFormat; /** * Used to format the timestamp fields (From:, To:, and the last column) and * the epoch for flot. This is set dynamically based on the * {@link #TIMESTAMP_FORMAT} and the {@link #PERIOD}. Flot always requires * epoch numbering, so it does not use this field. */ public final Format dateFormat; /** * Optional override of the MIME type from a URL query parameter. * * @see MIMETYPE */ public final String mimeType; /** * The name of a local file on which to write the data (this option is * supported only by local utility classes, not by the httpd interface). * * @see #FILE */ final public File file; @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(URLQueryModel.class.getName()); sb.append("{uri=" + uri); sb.append(",params=" + params); sb.append(",path=" + path); sb.append(",depth=" + depth); sb.append(",reportType=" + reportType); sb.append(",mimeType=" + mimeType); sb.append(",pattern=" + pattern); sb.append(",category=" + (category == null ? "N/A" : Arrays.toString(category))); sb.append(",period=" + period); sb.append(",[fromTime=" + fromTime); sb.append(",toTime=" + toTime + "]"); sb.append(",flot=" + flot); if (eventOrderBy != null) { sb.append(",eventOrderBy=["); boolean first = true; for (Field f : eventOrderBy) { if (!first) sb.append(","); sb.append(f.getName()); first = false; } sb.append("]"); } if (eventFilters != null && !eventFilters.isEmpty()) { sb.append(",eventFilters{"); boolean first = true; for (Map.Entry e : eventFilters.entrySet()) { if (!first) sb.append(","); sb.append(e.getKey().getName()); sb.append("="); sb.append(e.getValue()); first = false; } sb.append("}"); } sb.append("}"); return sb.toString(); } /** * Factory for performance counter integration. * * @param service * The service object IFF one was specified when * {@link CounterSetHTTPD} was started. * @param uri * Percent-decoded URI without parameters, for example * "/index.cgi" * @param parms * Parsed, percent decoded parameters from URI and, in case of * POST, data. The keys are the parameter names. Each value is a * {@link Vector} of {@link String}s containing the bindings for * the named parameter. The order of the URL parameters is * preserved by the insertion order of the {@link LinkedHashMap} * and the elements of the {@link Vector} values. * @param header * Header entries, percent decoded */ public static URLQueryModel getInstance(// final IService service,// final String uri,// final LinkedHashMap> params,// final Map headers// ) { /* * Re-create the request URL, including the protocol, host, port, and * path but not any query parameters. */ final StringBuilder sb = new StringBuilder(); // protocol (known from the container). sb.append("http://"); // host and port sb.append(headers.get("host")); // path (including the leading '/') sb.append(uri); final String requestURL = sb.toString(); return new URLQueryModel(service, uri, params, requestURL); } /** * Factory for Servlet API integration. * * @param service * The service object IFF one was specified when * {@link CounterSetHTTPD} was started. If this implements the * {@link IEventReportingService} interface, then events can also * be requested. * * @param req * The request. * @param resp * The response. */ public static URLQueryModel getInstance(// final IService service, final HttpServletRequest req, final HttpServletResponse resp ) throws UnsupportedEncodingException { final String uri = URLDecoder.decode(req.getRequestURI(), "UTF-8"); final LinkedHashMap> params = new LinkedHashMap>(); // @SuppressWarnings("unchecked") final Enumeration enames = req.getParameterNames(); while (enames.hasMoreElements()) { final String name = enames.nextElement(); final String[] values = req.getParameterValues(name); final Vector value = new Vector(); for (String v : values) { value.add(v); } params.put(name, value); } final String requestURL = req.getRequestURL().toString(); return new URLQueryModel(service, uri, params, requestURL); } /** * Create a {@link URLQueryModel} from a URL. This is useful when serving * historical performance counter data out of a file. * * @param url * The URL. * * @return The {@link URLQueryModel} * * @throws UnsupportedEncodingException */ static public URLQueryModel getInstance(final URL url) throws UnsupportedEncodingException { // Extract the URL query parameters. final LinkedHashMap> params = NanoHTTPD .decodeParams(url.getQuery(), new LinkedHashMap>()); // add any relevant headers final Map headers = new TreeMap( new CaseInsensitiveStringComparator()); headers.put("host", url.getHost() + ":" + url.getPort()); return URLQueryModel.getInstance(null/* service */, url.toString(), params, headers); } private URLQueryModel(final IService service, final String uri, final LinkedHashMap> params, final String requestURL) { if (uri == null) throw new IllegalArgumentException(); if (params == null) throw new IllegalArgumentException(); if (requestURL == null) throw new IllegalArgumentException(); this.uri = uri; this.params = params; // this.headers = headers; this.requestURL = requestURL; this.path = getProperty(params, PATH, ICounterSet.pathSeparator); if (log.isInfoEnabled()) log.info(PATH + "=" + path); this.depth = Integer.parseInt(getProperty(params, DEPTH, DEFAULT_DEPTH)); if (log.isInfoEnabled()) log.info(DEPTH + "=" + depth); if (depth < 0) throw new IllegalArgumentException("depth must be GTE ZERO(0)"); /* * FIXME fromTime and toTime are not yet being parsed. They should * be interpreted so as to allow somewhat flexible specification and * should be applied to both performance counter views and event * views. */ fromTime = 0L; toTime = Long.MAX_VALUE; // assemble the optional filter. this.pattern = QueryUtil.getPattern(// params.get(FILTER),// params.get(REGEX)// ); if (service != null && service instanceof IEventReportingService) { // events are available. eventReportingService = ((IEventReportingService) service); } else { // events are not available. eventReportingService = null; } if (params.containsKey(REPORT) && params.containsKey(CORRELATED)) { throw new IllegalArgumentException("Please use either '" + CORRELATED + "' or '" + REPORT + "'"); } if(params.containsKey(REPORT)) { this.reportType = ReportEnum.valueOf(getProperty( params, REPORT, ReportEnum.hierarchy.toString())); if (log.isInfoEnabled()) log.info(REPORT + "=" + reportType); } else { final boolean correlated = Boolean.parseBoolean(getProperty( params, CORRELATED, "false")); if (log.isInfoEnabled()) log.info(CORRELATED + "=" + correlated); this.reportType = correlated ? ReportEnum.correlated : ReportEnum.hierarchy; } if (eventReportingService != null) { final Iterator>> itr = params .entrySet().iterator(); while(itr.hasNext()) { final Map.Entry> entry = itr.next(); final String name = entry.getKey(); if (!name.startsWith("events.")) continue; final int pos = name.indexOf('.'); if (pos == -1) { throw new IllegalArgumentException( "Missing event column name: " + name); } // the name of the event column. final String col = name.substring(pos + 1, name.length()); final Field fld; try { fld = Event.class.getField(col); } catch(NoSuchFieldException ex) { throw new IllegalArgumentException("Unknown event field: "+col); } final Vector patterns = entry.getValue(); if (patterns.size() == 0) continue; if (patterns.size() > 1) throw new IllegalArgumentException( "Only one pattern per field: " + name); /* * compile the pattern * * Note: Throws PatternSyntaxException if the pattern can * not be compiled. */ final Pattern pattern = Pattern.compile(patterns.firstElement()); eventFilters.put(fld, pattern); } if (log.isInfoEnabled()) { final StringBuilder sb = new StringBuilder(); for (Field f : eventFilters.keySet()) { sb.append(f.getName() + "=" + eventFilters.get(f)); } log.info("eventFilters={" + sb + "}"); } } // eventOrderBy { final Vector v = params.get(EVENT_ORDER_BY); if (v == null) { /* * Use a default for eventOrderBy. */ try { eventOrderBy = new Field[] { Event.class .getField("hostname") }; } catch (Throwable t) { throw new RuntimeException(t); } } else { final Vector fields = new Vector(); for (String s : v) { try { fields.add(Event.class.getField(s)); } catch (Throwable t) { throw new RuntimeException(t); } } eventOrderBy = fields.toArray(new Field[0]); } if (log.isInfoEnabled()) log.info(EVENT_ORDER_BY + "=" + Arrays.toString(eventOrderBy)); } switch (reportType) { case events: if (eventReportingService == null) { /* * Throw exception since the report type requires events but * they are not available. */ throw new IllegalStateException("Events are not available."); } flot = true; break; default: flot = false; break; } this.category = params.containsKey(CATEGORY) ? params.get(CATEGORY) .toArray(new String[0]) : null; if (log.isInfoEnabled() && category != null) log.info(CATEGORY + "=" + Arrays.toString(category)); this.timestampFormat = TimestampFormatEnum.valueOf(getProperty( params, TIMESTAMP_FORMAT, TimestampFormatEnum.dateTime.toString())); if (log.isInfoEnabled()) log.info(TIMESTAMP_FORMAT + "=" + timestampFormat); this.period = PeriodEnum.valueOf(getProperty(params, PERIOD, PeriodEnum.Minutes.toString()/* defaultValue */)); if (log.isInfoEnabled()) log.info(PERIOD + "=" + period); /* * @todo this should be specified by a URL query parameter and * passed into the IRenderer instances. */ // this.decimalFormat = new DecimalFormat("0.###E0"); this.decimalFormat = new DecimalFormat("##0.#####E0"); // decimalFormat.setGroupingUsed(true); // // decimalFormat.setMinimumFractionDigits(3); // // decimalFormat.setMaximumFractionDigits(6); // // decimalFormat.setDecimalSeparatorAlwaysShown(true); this.percentFormat = NumberFormat.getPercentInstance(); this.integerFormat = NumberFormat.getIntegerInstance(); integerFormat.setGroupingUsed(true); this.unitsFormat = new DecimalFormat("0.#"); /* * Figure out how we will format the timestamp (From:, To:, and the last * column). */ switch(timestampFormat) { case dateTime: /* * Note: I have decided to go with the long format (date + time) * since runs often span days and the time along is not enough * information. */ dateFormat = DateFormat.getDateTimeInstance( DateFormat.MEDIUM/* date */, DateFormat.MEDIUM/* time */); // switch (period) { // case Minutes: // dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT); // break; // case Hours: // dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM); // break; // case Days: // dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); // break; // default: // throw new UnsupportedOperationException(period.toString()); // } break; case epoch: { // milliseconds since the epoch final NumberFormat f = NumberFormat.getIntegerInstance(); f.setGroupingUsed(false); f.setMinimumFractionDigits(0); dateFormat = f; break; } default: throw new UnsupportedOperationException(timestampFormat.toString()); } this.mimeType = (params.containsKey(MIMETYPE) ? getProperty(params, MIMETYPE, null) : null); this.file = (params.containsKey(FILE) ? new File(getProperty(params, FILE, null)) : null); if (log.isInfoEnabled()) log.info(FILE + "=" + file); } /** * Return the first value for the named property. * * @param params * The request parameters. * @param property * The name of the property * @param defaultValue * The default value (optional). * * @return The first value for the named property and the defaultValue * if there named property was not present in the request. * * @todo move to a request object? */ static protected String getProperty( final Map> params, final String property, final String defaultValue) { if (params == null) throw new IllegalArgumentException(); if (property == null) throw new IllegalArgumentException(); final Vector vals = params.get(property); if (vals == null) return defaultValue; return vals.get(0); } /** * Re-create the request URL, including the protocol, host, port, and * path but not any query parameters. */ public StringBuilder getRequestURL() { return new StringBuilder(requestURL); } /** * Re-create the request URL. * * @param override * Overridden query parameters (optional). * * @todo move to request object? */ public String getRequestURL(final URLQueryParam[] override) { // Note: Used throughput to preserve the parameter order. final LinkedHashMap> p; if(override == null) { p = params; } else { p = new LinkedHashMap>(params); for(URLQueryParam x : override) { p.put(x.name, x.values); } } final StringBuilder sb = getRequestURL(); sb.append("?path=" + encodeURL(getProperty(p, PATH, ICounterSet.pathSeparator))); final Iterator>> itr = p .entrySet().iterator(); while(itr.hasNext()) { final Map.Entry> entry = itr.next(); final String name = entry.getKey(); if (name.equals(PATH)) { // already handled. continue; } final Collection vals = entry.getValue(); for (String s : vals) { sb.append("&" + encodeURL(name) + "=" + encodeURL(s)); } } return sb.toString(); } static protected String encodeURL(final String url) { final String charset = "UTF-8"; try { return URLEncoder.encode(url, charset); } catch (UnsupportedEncodingException e) { log.error("Could not encode: charset=" + charset + ", url=" + url); return url; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy