/*
* NOTE: This copyright does *not* cover user programs that use HQ
* program services by normal system calls through the application
* program interfaces provided as part of the Hyperic Plug-in Development
* Kit or the Hyperic Client Development Kit - this is merely considered
* normal use of the program, and does *not* fall under the heading of
* "derived work".
*
* Copyright (C) [2004-2010], Hyperic, Inc.
* This file is part of HQ.
*
* HQ is free software; you can redistribute it and/or modify
* it under the terms version 2 of the GNU General Public License as
* published by the Free Software Foundation. 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.
*/
package org.hyperic.hq.product;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.StringTokenizer;
import java.util.Properties;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.hq.measurement.MeasurementConstants;
import org.hyperic.util.config.ConfigResponse;
import org.hyperic.util.config.ConfigSchema;
import org.hyperic.util.StringUtil;
/**
* this class parses Metrics in the form of:
* jmx-domain:jmx-properties:jmx-attribute:metric-properties
* For example:
* jboss.system:type=ServerInfo,lang=eng:FreeMemory:naming=jnp://localhost:1099,password=xxx
* where:
* jmx-domain = jboss.system
* jmx-properties = type=ServerInfo,lang=eng
* jmx-attribute = FreeMemory
* metric-properties = naming=jnp://localhost:1099,password=xxx
*/
public class Metric {
private static final Log log = LogFactory.getLog(Metric.class);
public static final String ATTR_AVAIL = "Availability";
public static final double AVAIL_UNKNOWN = MeasurementConstants.AVAIL_UNKNOWN;
public static final double AVAIL_UP = MeasurementConstants.AVAIL_UP;
public static final double AVAIL_DOWN = MeasurementConstants.AVAIL_DOWN;
public static final double AVAIL_WARN = MeasurementConstants.AVAIL_WARN;
public static final double AVAIL_PAUSED = MeasurementConstants.AVAIL_PAUSED;
public static final double AVAIL_POWERED_OFF = MeasurementConstants.AVAIL_POWERED_OFF;
private static HashMap cache = new HashMap();
private static final MetricProperties NO_PROPERTIES =
new MetricProperties();
private final Object lock = new Object();
private String template = null;
private String domainName = null;
private String objectName = null;
private String objectPropString = null;
private MetricProperties objectProperties = null;
private String attributeName = null;
private String propString = null;
private MetricProperties props = null;
private String id = null; //for tie-in to logging
private String category = null;
private long interval;
private Metric() {
synchronized (lock) {
interval = -1;
}
}
//we only need to encode these three
private static String getEncoding(char c) {
//poor-mans java.net.URLEncoder
switch (c) {
case ':':
return "%3A";
case '=':
return "%3D";
case ',':
return "%2C";
default:
return null;
}
}
private static char getDecoding(String s, int i) {
char c1 = s.charAt(i+1);
char c2 = s.charAt(i+2);
if ((c1 == '2') && (c2 == 'C')) {
return ',';
}
else if (c1 == '3') {
if (c2 == 'A') {
return ':';
}
else if (c2 == 'D') {
return '=';
}
}
return '0';
}
/**
* HHQ-3246: Some characters need to be escaped with a double backlash
* to preserve their value during the decoding process.
*
* For example, the equals sign = will normally be encoded
* to %3D and decoded back to =
*
* However, if %3D is the desired string, it needs
* to be escaped with the double backslash \\%3D so that %3D
* is the outputted string during the decoding process.
*/
private static String getUnescapedDecoding(String s, int i) {
String escapeIndicator = "\\\\";
String[] specialVals = new String[] {"%3D", "%3A", "%2C"};
String unescapedDecoding = null;
try {
String encodedString = s.substring(i, i+5);
for (int j=0; j<specialVals.length; j++) {
String escapedString = escapeIndicator + specialVals[j];
if (escapedString.equals(encodedString)) {
unescapedDecoding = specialVals[j];
break;
}
}
} catch (IndexOutOfBoundsException iob) {
//
}
return unescapedDecoding;
}
//we only encode/decode property values
//which are input by a user or auto inventory
public static String encode(String val) {
StringBuffer buf = new StringBuffer(val.length());
boolean changed = false;
for (int i=0; i<val.length(); i++) {
char c = val.charAt(i);
String enc = getEncoding(c);
if (enc == null) {
buf.append(c);
}
else {
buf.append(enc);
changed = true;
}
}
return changed ? buf.toString() : val;
}
//java.net.URLDecoder is not forgiving enough.
public static String decode(String val) {
StringBuffer buf = new StringBuffer(val.length());
boolean changed = false;
int len = val.length();
for (int i=0; i<len; i++) {
char c = val.charAt(i);
if ((c == '\\') && ((i+4) < len)) {
String unesc = getUnescapedDecoding(val, i);
if (unesc == null) {
buf.append(c);
} else {
i += 4;
buf.append(unesc);
changed = true;
}
} else {
if ((c == '%') && ((i+2) < len)) {
char dc = getDecoding(val, i);
if (dc != '0') {
i += 2;
c = dc;
changed = true;
}
}
buf.append(c);
}
}
return changed ? buf.toString() : val;
}
/**
* The domain name - corresponding the the ObjectName domain.
*/
public String getDomainName() {
return this.domainName;
}
public void setDomainName(String domain) {
this.domainName = domain;
}
/** The full JMX object name - domain : objectName
*/
public String getObjectName() {
// do not return the decoded version of the object name
// at this point, since sigar will decode it successively
// causing things to break
return this.objectName;
}
/** Set the JMX object name.
*/
public void setObjectName(String objectName) {
this.objectName = objectName;
// Clear out the object properties to force them to be
// recreated on subsequent calls to getObjectProperties()
this.objectProperties = null;
}
/** The attribute name
*/
public String getAttributeName() {
return this.attributeName;
}
public String toString() {
return this.template;
}
static String mask(String val) {
if (val == null) {
return "";
}
StringBuffer sb = new StringBuffer();
for (int i=0; i<val.length(); i++) {
sb.append('*');
}
return sb.toString();
}
private String toDebugString(String orig, Properties props) {
if (props == null && orig == null) {
return orig;
}
StringBuffer ds = new StringBuffer();
StringTokenizer tok = new StringTokenizer(orig, ",");
while (tok.hasMoreTokens()) {
String pair = tok.nextToken();
int ix = pair.indexOf('=');
if (ix == -1) {
ds.append(pair);
continue;
}
String key = pair.substring(0, ix);
String origVal = pair.substring(ix-1, pair.length());
if (ConfigSchema.isSecret(key)) {
String val = (props == null) ? origVal : props.getProperty(key);
ds.append(key).append('=');
ds.append(mask(val));
}
else {
ds.append(pair);
}
if (tok.hasMoreTokens()) {
ds.append(',');
}
}
return ds.toString();
}
public String toDebugString() {
StringBuffer dm = new StringBuffer();
dm.append(this.domainName).append(':');
dm.append(toDebugString(this.objectPropString, this.objectProperties));
dm.append(':').append(this.attributeName);
if (this.propString != null) {
dm.append(':');
dm.append(toDebugString(this.propString, this.props));
}
return dm.toString();
}
public Properties getProperties() {
if (this.props == null) {
if (this.propString == null) {
this.props = NO_PROPERTIES;
}
else {
this.props = parseProperties(this.propString);
}
}
return this.props;
}
public void setPropString(String propString) {
this.propString = propString;
}
public String getPropString() {
return this.propString;
}
public String getObjectProperty(String property)
{
return getObjectProperties().getProperty(property);
}
/** Properties in the local part of the ObjectName.
*/
public Properties getObjectProperties() {
if (this.objectProperties == null) {
this.objectProperties = parseProperties(this.objectPropString);
this.objectProperties.setDefaults(this.props);
}
return this.objectProperties;
}
/** The local part of the ObjectName.
*/
public String getObjectPropString() {
return this.objectPropString;
}
private static MetricProperties parseProperties(String config) {
MetricProperties props;
//common for templates to have the same properties
synchronized (cache) {
props = (MetricProperties)cache.get(config);
if (props != null) {
return props;
}
props = new MetricProperties();
cache.put(config, props);
}
//e.g. PluginLinter parses but does not replace properties
//such as %process.query%
if (config.indexOf(",") == -1) {
if (config.startsWith("%") && config.endsWith("%")) {
return props;
}
}
StringTokenizer st = new StringTokenizer(config, ",");
while (st.hasMoreTokens()) {
String attr = st.nextToken();
if (attr.equals("*")) {
//e.g. used in MBeanServer.queryMBeans
continue;
}
int ix = attr.indexOf('=');
if (ix == -1) {
continue;
}
String key = attr.substring(0, ix);
String val = attr.substring(key.length()+1);
if (val.length() == 0) {
continue;
}
ix = val.length()-1;
if ((val.charAt(0) == '%') &&
(val.charAt(ix) == '%') &&
val.substring(1, ix).equals(key))
{
//value was not replaced
continue;
}
props.setProperty(key, decode(val));
}
return props;
}
public String getId() {
return this.id;
}
public void setId(int type, int id) {
this.id = type + ":" + id;
}
public String getCategory() {
return this.category;
}
public void setCategory(String category) {
this.category = category;
}
public long getInterval() {
synchronized (lock) {
return this.interval;
}
}
public void setInterval(long interval) {
synchronized (lock) {
this.interval = interval;
}
}
public boolean isAvail() {
return MeasurementConstants.CAT_AVAILABILITY.equals(getCategory()) ||
getAttributeName().equals(ATTR_AVAIL);
}
/**
* @param template The metric string to be parsed.
* @return A Metric that can be used by the plugins.
* @exception MetricInvalidException If the metric string is malformed.
*/
public static Metric parse(String template)
throws MetricInvalidException {
if ((template == null) ||
(template.length() == 0)) {
throw new MetricInvalidException();
}
Metric metric;
synchronized (cache) {
metric = (Metric)cache.get(template);
}
if (metric != null) {
return metric;
}
metric = new Metric();
metric.template = template;
//e.g. jboss.system:type=ServerInfo:FreeMemory
StringTokenizer st = new StringTokenizer(template, ":");
try {
metric.domainName = st.nextToken(); //e.g. jboss.system
metric.objectPropString = st.nextToken(); //e.g. type=ServerInfo
//XXX workaround for hqagent "camAgent:availability" templates
if (!st.hasMoreTokens()) {
if (template.endsWith(":")) {
metric.attributeName = ""; //e.g. optional snmp %oid%
}
else {
metric.attributeName = metric.objectPropString;
metric.objectPropString = "DummyKey=DummyVal";
metric.objectName =
metric.domainName + ":" + metric.objectPropString;
}
}
else {
metric.objectName =
template.substring(0,
metric.domainName.length() +
1 +
metric.objectPropString.length());
metric.attributeName = st.nextToken(); //e.g. FreeMemory
}
if (st.hasMoreTokens()) {
//parse the metric properties
int offset = metric.objectName.length() +
metric.attributeName.length() + 2;
//e.g. admin.url=t3://localhost:7001,admin.username=system
metric.propString = template.substring(offset);
}
} catch (Exception e) {
throw new MetricInvalidException(template,e );
}
synchronized (cache) {
cache.put(template, metric);
}
metric.attributeName = decode(metric.attributeName);
return metric;
}
private static String replace(String template,
String key, String val) {
if (val == null) {
return template;
}
return StringUtil.replace(template,
"%" + key + "%",
encode(val));
}
public static String translate(String template,
ConfigResponse config) {
Iterator iter = config.getKeys().iterator();
while (iter.hasNext()) {
String key = (String)iter.next();
String val = config.getValue(key);
template = replace(template, key, val);
}
return template;
}
public static String translate(String template,
Properties props) {
Iterator iter = props.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
String key = (String)entry.getKey();
String val = (String)entry.getValue();
template = replace(template, key, val);
}
return template;
}
public static String configTemplate(String[] props) {
String template = "";
for (int i=0; i<props.length; i++) {
template += props[i] + "=" + "%" + props[i] + "%";
if (i+1 < props.length) {
template += ",";
}
}
return template;
}
private static void list(Metric metric) {
System.out.println("DomainName: '" +
metric.getDomainName() + "'");
System.out.println("ObjectName: '" +
metric.getObjectName() + "'");
System.out.println("AttributeName: '" +
metric.getAttributeName() + "'");
System.out.println("Object Properties: '" +
metric.getObjectPropString() + "'" +
" (" + metric.getObjectProperties().size() + ")");
if (metric.propString == null) {
return;
}
System.out.println("Connection Properties: '" +
metric.getPropString() + "'" +
" (" + metric.getProperties().size() + ")");
}
private static void list(File file) throws Exception {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if ((line.length() == 0) ||
line.startsWith("#"))
{
continue;
}
System.out.println("Template: " + line);
list(Metric.parse(line));
System.out.println("----------------------------");
}
} finally {
reader.close();
}
}
public static void main(String[] args) throws Exception {
long memStart = Runtime.getRuntime().freeMemory();
for (int i=0; i<args.length; i++) {
String template = args[i];
File file = new File(template);
if (file.exists()) {
list(file);
}
else {
list(Metric.parse(template));
}
}
long memEnd = Runtime.getRuntime().freeMemory();
System.out.println("mem diff=" + (memStart-memEnd));
int nMetrics=0, nProps=0;
for (Iterator it=cache.values().iterator(); it.hasNext();) {
Object o = it.next();
if (o instanceof Metric) {
nMetrics++;
}
else {
nProps++;
}
}
System.out.println("cache entries=" + cache.size() +
", metrics=" + nMetrics +
", props=" + nProps);
}
}