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