/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2007, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.helios.apmrouter.metric; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import javax.management.Attribute; import javax.management.AttributeList; import org.helios.apmrouter.util.URLHelper; /** * <p>Title: MetricURIBuilder</p> * <p>Description: A fluent style builder for a MetricURI.</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.metric.MetricURIBuilder</code></p> */ public class MetricURIBuilder { // <Domain>/<Host>/<Agent>/<Namespace>[:<MetricName>][?<OptionKey1>=<OptionValue1>[...<OptionKeyn>=<OptionValuen>]] /** The subscription's domain */ private final String domain; /** The subscription's host */ private final String host; /** The subscription's agent */ private final String agent; /** The subscription's metric name*/ private String metricName = null; /** The subscription's max depth */ private int maxd = -1; /** The subscription's metric name spaces, defaulting to none */ private List<String> namespaces = new ArrayList<String>(); /** The subscription's metric types, defaulting to all long types */ private Set<MetricType> metricTypes = EnumSet.of(MetricType.DELTA_COUNTER, MetricType.getLongMetricTypes()) ; /** The subscription's sub event types, defaulting to all */ private Set<SubscriptionType> subTypes = EnumSet.allOf(SubscriptionType.class); /** The subscription's event statuses, defaulting to all */ private Set<MetricStatus> statuses = EnumSet.allOf(MetricStatus.class); /** * Builds the metric URI from the current state of this builder * @param useOrdinals If true, enum constants will be rendered using their ordinals, otherwise they will be rendered using their names. * @return the metric URI */ public URI build(boolean useOrdinals) { StringBuilder b = new StringBuilder(); b.append(domain).append("/").append(host).append("/").append(agent); for(String s: namespaces) { b.append("/").append(s); } if(metricName != null) { b.append(":").append(metricName); } b.append("?"); boolean ao = false; if(!metricTypes.isEmpty()) { b.append("&").append(OPT_METRIC_TYPE).append("="); for(MetricType t: metricTypes) { b.append(useOrdinals ? t.ordinal() : t.name()).append(","); ao = true; } if(ao) b.deleteCharAt(b.length()-1); } ao = false; if(!subTypes.isEmpty()) { b.append("&").append(OPT_SUB_TYPE).append("="); for(SubscriptionType t: subTypes) { b.append(useOrdinals ? t.ordinal() : t.name()).append(","); ao = true; } if(ao) b.deleteCharAt(b.length()-1); } ao = false; if(!statuses.isEmpty()) { b.append("&").append(OPT_METRIC_STATUS).append("="); for(MetricStatus t: statuses) { b.append(useOrdinals ? t.ordinal() : t.name()).append(","); ao = true; } if(ao) b.deleteCharAt(b.length()-1); } if(maxd>0) { b.append("&").append(OPT_MAX_DEPTH).append("=").append(maxd); } return URLHelper.toURI(b); } /** * Builds the metric URI from the current state of this builder, rendering enum constants using ordinals * @return the metric URI */ public URI build() { return build(true); } /** * <p>Title: SubscriptionType</p> * <p>Description: Defines the MetricURI events that a MetricURI subscription is interested in.</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.metric.MetricURIBuilder.SubscriptionType</code></p> */ public static enum SubscriptionType { /** Subscribes to metric state change events */ STATE_CHANGE, /** Subscribes to new metric events */ NEW_METRIC, /** Subscribes to a data feed for the metrics */ DATA; } /** * <p>Title: MetricStatus</p> * <p>Description: Defines the possibles statuses of a metric that a subscriber might be interested to know about</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.metric.MetricURIBuilder.MetricStatus</code></p> */ public static enum MetricStatus { /** The entry is active and has had recent inserts */ ACTIVE, /** The entry is stale and has not seen inserts within the stale window */ STALE, /** The entry is offline and has not seen inserts within one time series tier */ OFFLINE; } /** A map of enum constants indexed by the ordinal externally indexed by the enum class */ private static final Map<Class<? extends Enum<?>>, Map<String, ? extends Enum<?>>> ORDMAPS = new HashMap<Class<? extends Enum<?>>, Map<String, ? extends Enum<?>>>(); /** The metric namespace delimiter splitter */ public static final Pattern SLASH_SPLITTER = Pattern.compile("/"); /** The query delimiter splitter */ public static final Pattern AMP_SPLITTER = Pattern.compile("&"); /** The query value comma splitter */ public static final Pattern COMMA_SPLITTER = Pattern.compile(","); /** * Parses the passed string value for comma separated values representing enum constants in the passed type. * Enum constants may be specified as the name, or as the oridinal. * @param type The enum type representing the MetricURI option * @param value The comma separated string value to parse * @return a set of select enum options */ @SuppressWarnings("unchecked") public static <T extends Enum<?>> Set<T> extractEnums(Class<T> type, CharSequence value) { T[] enumValues = type.getEnumConstants(); if(enumValues.length==0) return Collections.emptySet(); Map<String, T> ORD2ENUM = getEnumCache(type); Set<T> set = (Set<T>) EnumSet.noneOf(enumValues[0].getClass()); String[] evalues = COMMA_SPLITTER.split(value); if(evalues.length>0) { for(String evalue : evalues) { evalue = evalue.trim().toUpperCase(); T t = ORD2ENUM.get(evalue); if(t!=null) { set.add(t); continue; } try { t = (T) Enum.valueOf(enumValues[0].getClass(), evalue); set.add(t); continue; } catch (Exception ex) { /* No Op */ } throw new RuntimeException("Unrecognized option value [" + evalue + "] for option type [" + type.getSimpleName() + "]", new Throwable()); } } return set; } /** * Creates an enum lookup cache for the passed enum type * @param type The enum type to prep a cache for */ private static <T extends Enum<?>> Map<String, T> getEnumCache(Class<T> type) { Map<String, T> ORD2ENUM = (Map<String, T>) ORDMAPS.get(type); if(ORD2ENUM==null) { synchronized(ORDMAPS) { ORD2ENUM = (Map<String, T>) ORDMAPS.get(type); if(ORD2ENUM==null) { T[] enumValues = type.getEnumConstants(); ORD2ENUM = new HashMap<String, T>(enumValues.length); for(T t : enumValues) { ORD2ENUM.put("" + t.ordinal(), t); } ORDMAPS.put(type, Collections.unmodifiableMap(ORD2ENUM)); } } } return ORD2ENUM; } // static { // getEnumCache(MetricType.class); // getEnumCache(SubscriptionType.class); // getEnumCache(MetricStatus.class); // log("ORD MAP DUMP:"); // for(Map.Entry<Class<? extends Enum<?>>, Map<String, ? extends Enum<?>>> entry: ORDMAPS.entrySet()) { // StringBuilder b = new StringBuilder("\n\t").append(entry.getKey().getSimpleName()); // for(Map.Entry<String, ? extends Enum<?>> en: entry.getValue().entrySet()) { // b.append("\n\t\t[").append(en.getValue().name()).append(":").append(en.getValue().ordinal()).append("/").append(en.getKey()).append("]"); // } // log(b); // } // // } // private static void log(Object msg) { // System.out.println(msg); // } /** Option key for the max depth of the query */ public static final String OPT_MAX_DEPTH = "maxd"; /** Option key for the metric type filter of the query, parse as comma separated ints */ public static final String OPT_METRIC_TYPE = "type"; /** Option key for the status filter of the query, parse as comma separated ints */ public static final String OPT_METRIC_STATUS = "st"; /** Option key for the subscription type bit mask */ public static final String OPT_SUB_TYPE = "subtype"; /** The query keys in a set for quick validation */ public static final Set<String> QUERY_KEYS = Collections.unmodifiableSortedSet(new TreeSet<String>(Arrays.asList( OPT_MAX_DEPTH, OPT_METRIC_TYPE, OPT_METRIC_STATUS, OPT_SUB_TYPE ))); /** * Creates a new MetricURIBuilder * @param domain The metric's domain * @param host The metric's host * @param agent The metric's agent * @return a new MetricURIBuilder */ public static MetricURIBuilder newBuilder(String domain, String host, String agent) { return new MetricURIBuilder(domain, host, agent); } /** * Creates a new MetricURIBuilder * @param metricUriPath The metric URI that must contain at least the domain, host and agent. * @return a new MetricURIBuilder */ public static MetricURIBuilder newBuilder(CharSequence metricUriPath) { return new MetricURIBuilder(metricUriPath); } /** * Creates a new MetricURIBuilder * @param domain The metric's domain * @param host The metric's host * @param agent The metric's agent */ private MetricURIBuilder(String domain, String host, String agent) { if(domain==null || domain.trim().isEmpty()) throw new IllegalArgumentException("The passed domain was null or empty", new Throwable()); if(host==null || host.trim().isEmpty()) throw new IllegalArgumentException("The passed host was null or empty", new Throwable()); if(agent==null || agent.trim().isEmpty()) throw new IllegalArgumentException("The passed agent was null or empty", new Throwable()); this.domain = domain; this.host = host; this.agent = agent; } /** * Creates a new MetricURIBuilder from a minimal or full uri string which is fully validated for MetricURI rules * @param metricUriPath The metric URI that must contain at least the domain, host and agent. */ private MetricURIBuilder(CharSequence metricUriPath) { // ==================================== // parse the domain, host and agent // the 3rd fragment might also contain // a metric name // ==================================== if(metricUriPath==null) throw new IllegalArgumentException("The passed metricUriPath was null", new Throwable()); URI uri = URLHelper.toURI(metricUriPath.toString().trim()); String path = uri.toString().trim(); if(path.isEmpty()) throw new IllegalArgumentException("The passed metricUriPath was empty", new Throwable()); String[] fragments = SLASH_SPLITTER.split(path); if(fragments.length<3) throw new IllegalArgumentException("The passed metricUriPath [" + metricUriPath + "] did not have the mandatory domain, host and agent", new Throwable()); domain = fragments[0].trim(); host = fragments[1].trim(); if(domain.isEmpty()) throw new IllegalArgumentException("The passed domain was null or empty", new Throwable()); if(host.isEmpty()) throw new IllegalArgumentException("The passed host empty", new Throwable()); metricName = getMetricName(fragments); agent = fragments[2].trim(); if(agent.isEmpty()) throw new IllegalArgumentException("The passed agent empty", new Throwable()); // ==================================== // parse the namespace // ==================================== if(fragments.length>3) { for(int i = 3; i < fragments.length; i++) { String frag = fragments[i].trim(); if(frag.isEmpty()) continue; namespaces.add(frag); } } // ==================================== // there might be a query on the end of the path, so parse for it. // ==================================== String query = uri.getQuery(); if(query==null || query.trim().isEmpty()) return; // QUERY_KEYS fragments = AMP_SPLITTER.split(query.trim()); for(String frag: fragments) { frag = frag.trim().toLowerCase(); int index = frag.indexOf("="); if(index==-1) throw new RuntimeException("Invalid MetricURI Option [" + frag + "] was not a key/value pair", new Throwable()); String key = frag.substring(0, index).trim(); String value = frag.substring(index+1).trim(); if(!QUERY_KEYS.contains(key)) throw new RuntimeException("Invalid MetricURI Option Key [" + key + "] was not a valid option key", new Throwable()); // One of OPT_MAX_DEPTH, OPT_METRIC_TYPE, OPT_METRIC_STATUS, OPT_SUB_TYPE if(OPT_MAX_DEPTH.equals(key)) { try { maxd = Integer.parseInt(value); if(maxd<0) throw new Exception(); } catch (Exception ex) { throw new RuntimeException("Invalid MaxDepth option [" + value + "]", new Throwable()); } } else if(OPT_METRIC_TYPE.equals(key)) { metricTypes.clear(); metricTypes.addAll(extractEnums(MetricType.class, value)); } else if(OPT_METRIC_STATUS.equals(key)) { statuses.clear(); statuses.addAll(extractEnums(MetricStatus.class, value)); } else if(OPT_SUB_TYPE.equals(key)) { subTypes.clear(); subTypes.addAll(extractEnums(SubscriptionType.class, value)); } } } /** * Sets the metric name * @param metricName The metric name to set * @return this builder */ public MetricURIBuilder metricName(String metricName) { if(metricName==null || metricName.trim().isEmpty()) throw new IllegalArgumentException("The passed metric name was null or empty", new Throwable()); this.metricName = metricName.trim(); return this; } /** * Set the MetricURI namespace to the passed namespaces, clearing the prior set (even if the passed array is empty !). * @param namespaces The namespaces to set. * @return this builder */ public MetricURIBuilder namespace(String...namespaces) { return namespace(false, namespaces); } /** * Set the MetricURI namespace to the passed namespaces * @param append true to add to the existing, false to replace * @param namespaces The namsepaces to add * @return this builder */ public MetricURIBuilder namespace(boolean append, String...namespaces) { if(!append) this.namespaces.clear(); if(namespaces!=null) { for(String s: namespaces) { if(s==null || s.trim().isEmpty()) continue; this.namespaces.add(s.trim()); } } return this; } /** * Adds metric types to the subscription URI * @param append true to add, false to replace * @param metricTypes An array of metric type enum constants to add * @return this builder */ public MetricURIBuilder metricType(boolean append, MetricType...metricTypes) { if(!append) this.metricTypes.clear(); if(metricTypes!=null) { for(MetricType t: metricTypes) { if(t!=null) this.metricTypes.add(t); } } return this; } /** * Appends metric types to the subscription URI * @param metricTypes An array of metric type enum constants to add * @return this builder */ public MetricURIBuilder appendMetricType(MetricType...metricTypes) { return metricType(true, metricTypes); } /** * Replaces the metric types in the subscription URI * @param metricTypes An array of metric type enum constants to replace the existing ones * @return this builder */ public MetricURIBuilder metricType(MetricType...metricTypes) { return metricType(false, metricTypes); } /** * Sets the metric types to long values only * @return this builder */ public MetricURIBuilder longMetricTypes() { return metricType(false, MetricType.getLongMetricTypes()); } /** * Extracts the metric name from the last member of the passed array. * If a metric name is found, the array member is stripped of the metric name and the <b><code>":"</code></b> delimiter. * @param fragments The path of the metric URI split by <b><code>"/"</code></b>. * @return The extracted metric name or null if one was not found */ private String getMetricName(String[] fragments) { String lastFragment = fragments[fragments.length-1]; int index = lastFragment.indexOf(':'); if(index!=-1) { String metricName = lastFragment.substring(index+1); fragments[fragments.length-1] = lastFragment.substring(0, index); if(metricName.trim().isEmpty()) return null; return metricName.trim(); } return null; } /** * Adds metric statuses to the subscription URI * @param append true to add, false to replace * @param metricStatuses An array of metric status enum constants to add * @return this builder */ public MetricURIBuilder metricStatus(boolean append, MetricStatus...metricStatuses) { if(!append) this.statuses.clear(); if(metricStatuses!=null) { for(MetricStatus t: metricStatuses) { if(t!=null) this.statuses.add(t); } } return this; } /** * Sets the maximum recursion depth for the metric URI * @param maxDepth The maximum recursion depth * @return this builder */ public MetricURIBuilder maxDepth(int maxDepth) { if(maxDepth<0) throw new IllegalArgumentException("Invalid max depth [" + maxDepth + "]", new Throwable()); maxd = maxDepth; return this; } /** * Appends metric statuses to the subscription URI * @param metricStatuses An array of metric status enum constants to append * @return this builder */ public MetricURIBuilder appendMetricStatus(MetricStatus...metricStatuses) { return metricStatus(true, metricStatuses); } /** * Replaces the metric statuses in the subscription URI * @param metricStatuses An array of metric status enum constants to replace the current ones with * @return this builder */ public MetricURIBuilder metricStatus(MetricStatus...metricStatuses) { return metricStatus(false, metricStatuses); } /** * Adds subscription types to the subscription URI * @param append true to add, false to replace * @param subTypes An array of subscription type enum constants to add * @return this builder */ public MetricURIBuilder subType(boolean append, SubscriptionType...subTypes) { if(!append) this.subTypes.clear(); if(subTypes!=null) { for(SubscriptionType t: subTypes) { if(t!=null) this.subTypes.add(t); } } return this; } /** * Appends subscription types to the subscription URI * @param subTypes An array of subscription type enum constants to add * @return this builder */ public MetricURIBuilder appendSubType(SubscriptionType...subTypes) { return subType(true, subTypes); } /** * Replaces subscription types in the subscription URI * @param subTypes An array of subscription type enum constants to replace the current ones * @return this builder */ public MetricURIBuilder subType(SubscriptionType...subTypes) { return subType(false, subTypes); } /** * @param args */ public static void main(String[] args) { log("MetricURI Builder Test"); log( MetricURIBuilder.newBuilder("DefaultDomain", "njw810", "GroovyAgent") .namespace("A=B", "C=D", "E=F") .metricName("TCPOperations") .longMetricTypes() .subType(SubscriptionType.NEW_METRIC) .metricStatus(MetricStatus.values()) .build() ); } // public Map<String, Object> attrListToMap(AttributeList attrList) { // Map<String, Object> attributeMap = new HashMap<String, Object>(attrList.size()); // for(Attribute attr: attrList.asList()) { // attributeMap.put(attr.getName(), attr.getValue()); // } // return attributeMap; // } public static void log(Object msg) { System.out.println(msg); } }