package com.ibm.nmon.chart.definition;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.ibm.nmon.data.definition.*;
import com.ibm.nmon.data.matcher.*;
import com.ibm.nmon.data.transform.name.*;
import com.ibm.nmon.parser.BasicXMLParser;
import com.ibm.nmon.analysis.Statistic;
public final class ChartDefinitionParser extends BasicXMLParser {
private final List<BaseChartDefinition> charts = new java.util.ArrayList<BaseChartDefinition>();
private BaseChartDefinition currentChart;
private HostMatcher hostMatcher;
private NameTransformer hostTransformer;
private TypeMatcher typeMatcher;
private NameTransformer typeTransformer;
// allow multiple field matchers; endDataElement() will handle as appropriate
private final List<FieldMatcher> fieldMatchers = new java.util.ArrayList<FieldMatcher>(3);
// but, allow only a single RegexNameTransformer for all fields
private final Map<String, SimpleNameTransformer> fieldTransformers = new java.util.HashMap<String, SimpleNameTransformer>(
3);
private RegexNameTransformer regexFieldRegexTransformer;
private boolean inData;
private Statistic currentStat;
private final Set<Statistic> markers = new java.util.HashSet<Statistic>(3);
private boolean useSecondaryYAxis;
public ChartDefinitionParser() {
reset();
}
public List<BaseChartDefinition> parseCharts(String filename) throws IOException {
long start = System.nanoTime();
try {
parse(filename);
if (logger.isDebugEnabled()) {
logger.debug("parse complete for file '{}' in {}ms", filename, (System.nanoTime() - start) / 1000000.0d);
}
if (charts.isEmpty()) {
throw new IOException("chart definition file '" + filename
+ "' does not appear to have any charts defined");
}
return java.util.Collections.unmodifiableList(new java.util.ArrayList<BaseChartDefinition>(charts));
}
finally {
reset();
}
}
public List<BaseChartDefinition> parseCharts(InputStream in) throws IOException {
long start = System.nanoTime();
reset();
parse(in);
if (logger.isDebugEnabled()) {
logger.debug("Parse complete for input stream '{}' in {}ms", in, (System.nanoTime() - start) / 1000000.0d);
}
if (charts.isEmpty()) {
throw new IOException("chart definition input stream '" + in
+ "' does not appear to have any charts defined");
}
try {
return java.util.Collections.unmodifiableList(new java.util.ArrayList<BaseChartDefinition>(charts));
}
finally {
reset();
}
}
@Override
protected void startElement(String element, String unparsedAttributes) {
if ("linechart".equals(element)) {
createLineChart(parseAttributes(unparsedAttributes));
}
else if ("intervalchart".equals(element)) {
createIntervalChart(parseAttributes(unparsedAttributes));
}
else if ("barchart".equals(element)) {
createBarChart(parseAttributes(unparsedAttributes));
}
else if ("histogram".equals(element)) {
createHistogram(parseAttributes(unparsedAttributes));
}
else if ("yAxis".equals(element)) {
if (currentChart instanceof YAxisChartDefinition) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
((YAxisChartDefinition) currentChart).setUsePercentYAxis(Boolean.valueOf(attributes.get("asPercent")));
((YAxisChartDefinition) currentChart).setYAxisLabel(attributes.get("label"));
}
else {
logger.warn("ignoring " + "<yAxis>" + " element for chart "
+ (currentChart == null ? currentChart : currentChart.getShortName()) + " without a Y axis"
+ " at line {}", getLineNumber());
}
}
else if ("yAxis2".equals(element)) {
if (currentChart instanceof YAxisChartDefinition) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
if (attributes.get("asPercent") != null) {
logger.warn("ignoring " + "asPercent" + " attribute for " + "<yAxis2>" + " element for chart '"
+ (currentChart == null ? currentChart : currentChart.getShortName()) + '\''
+ " at line {}" + "; secondary axes do not support percentages", getLineNumber());
}
if (currentChart instanceof BarChartDefinition && ((BarChartDefinition) currentChart).isStacked()) {
logger.warn("ignoring " + "<yAxis2>" + " element for chart "
+ (currentChart == null ? currentChart : currentChart.getShortName()) + " at line {}"
+ "; stacked bar charts do not support secondary axes", getLineNumber());
}
else {
((YAxisChartDefinition) currentChart).setSecondaryYAxisLabel(attributes.get("label"));
((YAxisChartDefinition) currentChart).setHasSecondaryYAxis(true);
}
}
else {
logger.warn("ignoring " + "<yAxis2>" + " element for chart "
+ (currentChart == null ? currentChart : currentChart.getShortName()) + " without a Y axis"
+ " at line {}", getLineNumber());
}
}
else if ("xAxis".equals(element)) {
if (currentChart instanceof LineChartDefinition) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
((LineChartDefinition) currentChart).setXAxisLabel(attributes.get("label"));
}
else if (currentChart instanceof BarChartDefinition) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
((BarChartDefinition) currentChart).setCategoryAxisLabel(attributes.get("label"));
}
else if (currentChart instanceof HistogramChartDefinition) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
((HistogramChartDefinition) currentChart).setXAxisLabel(attributes.get("label"));
String minString = attributes.get("min");
String maxString = attributes.get("max");
if (((minString != null) && (maxString == null)) || ((maxString != null) && (minString == null))) {
logger.warn("ignoring " + "<histogram>" + " attributes 'min' and 'max' "
+ "must both be specified " + " at line {}" + ", if a defined range is desired",
getLineNumber());
}
else {
try {
((HistogramChartDefinition) currentChart).setXAxisRange(new org.jfree.data.Range(Integer
.parseInt(minString), Integer.parseInt(maxString)));
}
catch (NumberFormatException nfe) {
logger.warn("ignoring " + "<histogram>" + " attributes 'min' and 'max' "
+ "with values '{}' and '{}'" + " at line {}" + ", are not a valid numbers", minString,
maxString, getLineNumber());
}
catch (IllegalArgumentException iae) {
logger.warn("ignoring " + "<histogram>" + " attributes 'min' and 'max' "
+ "with values '{}' and '{}'" + " at line {}" + ", invalid range", minString,
maxString, getLineNumber());
}
}
}
else {
logger.warn("ignoring " + "<xAxis>" + " element for chart "
+ (currentChart == null ? currentChart : currentChart.getShortName()) + " without an X axis"
+ " at line {}", getLineNumber());
}
}
else if ("data".equals(element)) {
if (currentChart == null) {
logger.warn("ignoring " + "<data>" + " element outside of a chart");
inData = false;
skip = true;
}
else {
inData = true;
skip = false;
Map<String, String> attributes = parseAttributes(unparsedAttributes);
String stat = attributes.get("stat");
if (stat != null) {
try {
currentStat = Statistic.valueOf(stat);
}
catch (IllegalArgumentException iae) {
logger.warn("ignoring " + "invalid " + "'stat'" + " attribute {} at line {}", stat,
getLineNumber());
}
}
useSecondaryYAxis = Boolean.parseBoolean(attributes.get("useYAxis2"));
}
}
else if ("host".equals(element)) {
parseHost(parseAttributes(unparsedAttributes));
}
else if ("type".equals(element)) {
parseType(parseAttributes(unparsedAttributes));
}
else if ("field".equals(element)) {
parseField(parseAttributes(unparsedAttributes));
}
else if ("fieldAlias".equals(element)) {
parseFieldAlias(parseAttributes(unparsedAttributes));
}
else if ("marker".equals(element)) {
Map<String, String> attributes = parseAttributes(unparsedAttributes);
String stat = attributes.get("stat");
if (stat != null) {
try {
markers.add(Statistic.valueOf(stat));
}
catch (IllegalArgumentException iae) {
logger.warn("ignoring " + "invalid " + "'marker'" + " attribute {} at line {}", stat,
getLineNumber());
}
}
}
else if ("charts".equals(element)) {
// do nothing but also do not log a spurious warning
}
else {
logger.warn("unknown element {} at line {}", element, getLineNumber());
}
}
@Override
protected void endElement(String element) {
if (currentChart == null) {
if (!"charts".equals(element)) {
logger.warn("ignoring" + " element </{}> at line {}; current chart is not defined", element,
getLineNumber());
}
// else no chart should be defined when ending the root </charts> element
return;
}
if ("linechart".equals(element)) {
charts.add(currentChart);
currentChart = null;
}
else if ("intervalchart".equals(element)) {
charts.add(currentChart);
currentChart = null;
}
else if ("barchart".equals(element)) {
charts.add(currentChart);
currentChart = null;
}
else if ("histogram".equals(element)) {
charts.add(currentChart);
currentChart = null;
}
else if ("data".equals(element)) {
if (!skip) {
endDataElement();
}
resetData();
}
}
private void createLineChart(Map<String, String> attributes) {
if (currentChart != null) {
logger.warn("ignoring " + "<linechart>" + " element inside another chart definition" + " at line {}",
getLineNumber());
skip = true;
return;
}
String title = attributes.get("name");
if ((title == null) || "".equals(title)) {
logger.warn("ignoring " + "<linechart>" + " element with no name" + " at line {}", getLineNumber());
skip = true;
return;
}
String shortName = attributes.get("shortName");
boolean stacked = Boolean.valueOf(attributes.get("stacked"));
currentChart = new LineChartDefinition(shortName == null ? title : shortName, title, stacked);
if (attributes.get("subtitledBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("subtitledBy"));
if (mode != null) {
currentChart.setSubtitleNamingMode(mode);
}
}
if (attributes.get("linesNamedBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("linesNamedBy"));
((LineChartDefinition) currentChart).setLineNamingMode(mode);
}
parseSize("<linechart>", attributes);
logger.debug("parsed line chart {}", currentChart.getShortName());
}
private void createIntervalChart(Map<String, String> attributes) {
if (currentChart != null) {
logger.warn("ignoring " + "<intervalchart>" + " element inside another chart definition" + " at line {}",
getLineNumber());
skip = true;
}
String title = attributes.get("name");
if ((title == null) || "".equals(title)) {
logger.warn("ignoring " + "<intervalchart>" + " element with no name" + " at line {}", getLineNumber());
skip = true;
return;
}
String shortName = attributes.get("shortName");
currentChart = new IntervalChartDefinition(shortName == null ? title : shortName, title);
if (attributes.get("subtitledBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("subtitledBy"));
if (mode != null) {
currentChart.setSubtitleNamingMode(mode);
}
}
if (attributes.get("linesNamedBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("linesNamedBy"));
((LineChartDefinition) currentChart).setLineNamingMode(mode);
}
parseSize("<intervalchart>", attributes);
logger.debug("parsed interval chart {}", currentChart.getShortName());
}
private void createBarChart(Map<String, String> attributes) {
if (currentChart != null) {
logger.warn("ignoring " + "<barchart>" + " element inside another chart definition" + " at line {}",
getLineNumber());
skip = true;
return;
}
String title = attributes.get("name");
if ((title == null) || "".equals(title)) {
logger.warn("ignoring " + "<barchart>" + " element with no name" + " at line {}", getLineNumber());
skip = true;
return;
}
String shortName = attributes.get("shortName");
boolean stacked = Boolean.valueOf(attributes.get("stacked"));
boolean subtractionNeeded = Boolean.valueOf(attributes.get("subtractionNeeded"));
currentChart = new BarChartDefinition(shortName == null ? title : shortName, title, stacked, subtractionNeeded);
if (attributes.get("subtitledBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("subtitledBy"));
if (mode != null) {
currentChart.setSubtitleNamingMode(mode);
}
}
if (attributes.get("barsNamedBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("barsNamedBy"));
((BarChartDefinition) currentChart).setBarNamingMode(mode);
}
if (attributes.get("categoriesNamedBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("categoriesNamedBy"));
((BarChartDefinition) currentChart).setCategoryNamingMode(mode);
}
parseSize("<barchart>", attributes);
logger.debug("parsed bar chart {}", currentChart.getShortName());
}
private void createHistogram(Map<String, String> attributes) {
if (currentChart != null) {
logger.warn("ignoring " + "<histogram>" + " element inside another chart definition" + " at line {}",
getLineNumber());
skip = true;
return;
}
String title = attributes.get("name");
if ((title == null) || "".equals(title)) {
logger.warn("ignoring " + "<histogram>" + " element with no name" + " at line {}", getLineNumber());
skip = true;
return;
}
String shortName = attributes.get("shortName");
currentChart = new HistogramChartDefinition(shortName == null ? title : shortName, title);
if (attributes.get("barsNamedBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("barsNamedBy"));
((HistogramChartDefinition) currentChart).setHistogramNamingMode(mode);
}
if (attributes.get("subtitledBy") != null) {
NamingMode mode = NamingMode.valueOf(attributes.get("subtitledBy"));
if (mode != null) {
currentChart.setSubtitleNamingMode(mode);
}
}
String temp = attributes.get("bins");
if (temp != null) {
try {
((HistogramChartDefinition) currentChart).setBins(Integer.parseInt(temp));
}
catch (NumberFormatException nfe) {
logger.warn("ignoring " + "<histogram>" + " attribute 'bins' with value {}" + " at line {}"
+ ", it is not a valid number", temp, getLineNumber());
}
}
temp = attributes.get("showMarkers");
// null => default, which is to show markers
if ((temp != null) && !Boolean.valueOf(attributes.get("showMarkers"))) {
((HistogramChartDefinition) currentChart).setMarkers(new Statistic[0]);
}
parseSize("<histogram>", attributes);
logger.debug("parsed histogram chart {}", currentChart.getShortName());
}
private void parseHost(Map<String, String> attributes) {
if (!inData) {
logger.warn("ignoring " + "<host>" + " element outside of <data>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
return;
}
if (hostMatcher == null) {
String name = attributes.get("name");
if (name != null) {
if (HostMatcher.ALL.toString().equals(name)) {
hostMatcher = HostMatcher.ALL;
}
else {
hostMatcher = new ExactHostMatcher(name);
}
}
else {
String regex = attributes.get("regex");
if (regex != null) {
hostMatcher = new RegexHostMatcher(regex);
}
else {
String os = attributes.get("os");
if (os != null) {
hostMatcher = new OSMatcher(os);
}
else {
logger.error("either 'name', 'regex' or 'os'" + " must be defined for " + "<host>"
+ " at line {}" + "; <data> will be skipped", getLineNumber());
skip = true;
}
}
}
hostTransformer = createTransformer(attributes, true, hostTransformer);
}
else {
logger.warn("ignoring " + "extra " + "<host>" + " element" + " at line {}", getLineNumber());
}
}
private void parseType(Map<String, String> attributes) {
if (!inData) {
logger.warn("ignoring " + "<type>" + " element outside of <data>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
return;
}
if (typeMatcher == null) {
String name = attributes.get("name");
if (name != null) {
if ("$PROCESSES".equals(name)) {
typeMatcher = ProcessMatcher.INSTANCE;
}
else if (TypeMatcher.ALL.toString().equals(name)) {
typeMatcher = TypeMatcher.ALL;
}
else {
typeMatcher = new ExactTypeMatcher(name);
}
}
else {
String regex = attributes.get("regex");
if (regex != null) {
typeMatcher = new RegexTypeMatcher(regex);
}
else {
logger.error("either 'name' or 'regex'" + " must be defined for " + "<type>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
}
}
typeTransformer = createTransformer(attributes, true, typeTransformer);
}
else {
logger.warn("ignoring " + "extra " + "<type>" + " element" + " at line {}", getLineNumber());
}
}
private void parseField(Map<String, String> attributes) {
if (!inData) {
logger.warn("ignoring " + "<field>" + " element outside of <data>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
return;
}
String name = attributes.get("name");
if (name != null) {
if (FieldMatcher.ALL.toString().equals(name)) {
fieldMatchers.add(FieldMatcher.ALL);
}
else {
fieldMatchers.add(new ExactFieldMatcher(name));
}
NameTransformer transformer = createTransformer(attributes, true, null);
if (transformer != null) {
fieldTransformers.put(name, (SimpleNameTransformer) transformer);
}
}
else {
String regex = attributes.get("regex");
if (regex != null) {
fieldMatchers.add(new RegexFieldMatcher(regex));
// if the field is being matched by regex, there can be only a single, global
// regex field transform
NameTransformer transformer = createTransformer(attributes, false, regexFieldRegexTransformer);
if (transformer != null) {
regexFieldRegexTransformer = (RegexNameTransformer) transformer;
}
}
else {
logger.error("either 'name' or 'regex'" + " must be defined for " + "<field>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
}
}
}
private void parseFieldAlias(Map<String, String> attributes) {
if (!inData) {
logger.warn("ignoring " + "<fieldAlias>" + " element outside of <data>" + " at line {}"
+ "; <data> will be skipped", getLineNumber());
skip = true;
return;
}
String name = attributes.get("name");
if (name == null) {
logger.warn("'name'" + " must be defined for " + "<fieldAlias>" + " at line {}", getLineNumber());
return;
}
else {
String value = attributes.get("value");
if (value == null) {
logger.warn("'value'" + " must be defined for " + "<fieldAlias>" + " at line {}", getLineNumber());
}
else {
fieldTransformers.put(name, new SimpleNameTransformer(value));
}
}
}
private void parseSize(String elementName, Map<String, String> attributes) {
if (attributes.get("width") != null) {
String temp = attributes.get("width");
try {
currentChart.setWidth(Integer.parseInt(temp));
}
catch (NumberFormatException nfe) {
logger.warn("ignoring " + elementName + " attribute " + "'width'" + " with value '{}'" + " at line {}"
+ ", is not a valid number", temp, getLineNumber());
}
}
if (attributes.get("height") != null) {
String temp = attributes.get("height");
try {
currentChart.setHeight(Integer.parseInt(temp));
}
catch (NumberFormatException nfe) {
logger.warn("ignoring " + elementName + " attribute " + "'height'" + " with value '{}'" + " at line {}"
+ ", is not a valid number", temp, getLineNumber());
}
}
}
private void endDataElement() {
if (typeMatcher == null) {
logger.warn("ignoring " + "<data>" + " element without a <type>" + "at line {}" + " <type> is required",
getLineNumber());
return;
}
// as a convenience to users, allow multiple <field> elements
// but, as an optimization, roll up all the ExactFieldMatchers into a SetFieldMatcher since
// there has to be a DataDefinition per matcher
List<FieldMatcher> consolidatedMatchers = new java.util.ArrayList<FieldMatcher>(fieldMatchers.size());
List<String> names = new java.util.ArrayList<String>(fieldMatchers.size());
for (FieldMatcher matcher : fieldMatchers) {
if (matcher.getClass().equals(ExactFieldMatcher.class)) {
String name = ((ExactFieldMatcher) matcher).getField();
if ("$DISKS".equals(name)) {
consolidatedMatchers.add(DiskMatcher.INSTANCE);
}
else if ("$PARTITIONS".equals(name)) {
consolidatedMatchers.add(PartitionMatcher.INSTANCE);
}
// else if ("$ALL".equals(name)) covered by having the names list be empty
else {
names.add(name);
}
}
else {
consolidatedMatchers.add(matcher);
}
}
if ((names.size() == 0) && (consolidatedMatchers.size() == 0)) {
consolidatedMatchers.add(FieldMatcher.ALL);
}
else if (names.size() == 1) {
consolidatedMatchers.add(new ExactFieldMatcher(names.get(0)));
}
else if (names.size() > 1) {
consolidatedMatchers.add(new SetFieldMatcher(names.toArray(new String[names.size()])));
}
// for each matcher create new DataDefinition
for (FieldMatcher fieldMatcher : consolidatedMatchers) {
DefaultDataDefinition definition = new DefaultDataDefinition(hostMatcher, typeMatcher, fieldMatcher,
currentStat, useSecondaryYAxis);
for (String field : fieldTransformers.keySet()) {
definition.addFieldTransformer(field, fieldTransformers.get(field));
}
if (hostTransformer != null) {
// no <host> element => hostMatcher and hostTransformer will be null, no need for
// hostMatcher null check here
if (hostMatcher.getClass().equals(ExactHostMatcher.class)) {
definition.addHostnameTransformer(((ExactHostMatcher) hostMatcher).getHostname(), hostTransformer);
}
else {
definition.addHostnameTransformer(DefaultDataDefinition.DEFAULT_NAME_TRANSFORMER_KEY,
hostTransformer);
}
}
if (typeTransformer != null) {
// no <type> element => typeMatcher and typeTransformer will be null, no need for
// typeMatcher null check here
if (typeMatcher.getClass().equals(ExactTypeMatcher.class)) {
definition.addTypeTransformer(((ExactTypeMatcher) typeMatcher).getType(), typeTransformer);
}
else {
definition.addTypeTransformer(DefaultDataDefinition.DEFAULT_NAME_TRANSFORMER_KEY, typeTransformer);
}
}
if (regexFieldRegexTransformer != null) {
definition.addFieldTransformer(DefaultDataDefinition.DEFAULT_NAME_TRANSFORMER_KEY,
regexFieldRegexTransformer);
}
currentChart.addData(definition);
if (currentChart instanceof HistogramChartDefinition) {
HistogramChartDefinition histogramChart = ((HistogramChartDefinition) currentChart);
// count will be zero if 'showMarkers' is set to false
// ignore parsed markers if that is the case
if ((histogramChart.getMarkerCount() != 0) && (markers.size() > 0)) {
histogramChart.setMarkers(markers.toArray(new Statistic[markers.size()]));
}
}
logger.debug("added {} to chart {}", definition, currentChart.getShortName());
}
}
private NameTransformer createTransformer(Map<String, String> attributes, boolean aliasAllowed,
NameTransformer existing) {
String alias = attributes.get("alias");
if (alias != null) {
if (aliasAllowed) {
return new SimpleNameTransformer(alias);
}
else {
logger.warn("ignoring invalid 'alias' attribute" + " at line {}", getLineNumber());
return existing;
}
}
else {
String regex = attributes.get("aliasRegex");
if (regex == null) {
// try to reuse existing regex
regex = attributes.get("regex");
}
if (regex != null) {
if (existing != null) {
logger.warn(
"an existing regex substitution has already been defined, ignoring additional substitutions"
+ " at line {}", getLineNumber());
return existing;
}
else {
String group = attributes.get("aliasByGroup");
if (group == null) {
String replacement = attributes.get("aliasByReplacement");
if ((replacement != null) && !"".equals(replacement)) {
return new RegexNameTransformer(regex, replacement);
}
else {
return new RegexNameTransformer(regex);
}
}
else {
try {
return new RegexNameTransformer(regex, Integer.parseInt(group));
}
catch (NumberFormatException nfe) {
logger.warn("'aliasByGroup' must be a number" + " at line {}", getLineNumber());
}
return existing;
}
}
}
else {
return existing;
}
}
}
@Override
protected void reset() {
super.reset();
charts.clear();
currentChart = null;
resetData();
}
private void resetData() {
hostMatcher = null;
hostTransformer = null;
typeMatcher = null;
typeTransformer = null;
fieldMatchers.clear();
fieldTransformers.clear();
regexFieldRegexTransformer = null;
inData = false;
skip = false;
currentStat = Statistic.AVERAGE;
useSecondaryYAxis = false;
}
}