diff options
Diffstat (limited to 'qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid')
92 files changed, 14112 insertions, 0 deletions
diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Agent.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Agent.java new file mode 100644 index 0000000000..80acf93e55 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Agent.java @@ -0,0 +1,1465 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +// JMS Imports +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.MessageListener; +import javax.jms.Session; + +// Simple Logging Facade 4 Java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Misc Imports +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.AMQPMessage; +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.Notifier; +import org.apache.qpid.qmf2.common.NotifierWrapper; +import org.apache.qpid.qmf2.common.NullQmfEventListener; +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfCallback; +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfEvent; +import org.apache.qpid.qmf2.common.QmfEventListener; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfQuery; +import org.apache.qpid.qmf2.common.QmfQueryTarget; +import org.apache.qpid.qmf2.common.SchemaClass; +import org.apache.qpid.qmf2.common.SchemaClassId; +import org.apache.qpid.qmf2.common.SchemaEventClass; +import org.apache.qpid.qmf2.common.SchemaObjectClass; +import org.apache.qpid.qmf2.common.WorkItem; +import org.apache.qpid.qmf2.common.WorkQueue; + +/** + * A QMF agent component is represented by a instance of the Agent class. This class is the topmost object + * of the Agent application's object model. Associated with a particular agent are: + * <pre> + * * The set of objects managed by that agent + * * The set of schema that describes the structured objects owned by the agent + * * A collection of Consoles that are interfacing with the agent + * </pre> + * The Agent class communicates with the application using the same work-queue model as the Console. + * The agent maintains a work-queue of pending requests. Each pending request is associated with a handle. + * When the application is done servicing the work request, it passes the response to the agent along with + * the handle associated with the originating request. + * <p> + * The base class for the Agent object is the Agent class. This base class represents a single agent + * implementing internal store. + * + * <h3>Subscriptions</h3> + * This implementation of the QMF2 API has full support for QMF2 Subscriptions. + * <p> + * The diagram below shows the relationship between the Agent, the Subscription and SubscribableAgent interface. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * <p> + * <h3>Receiving Asynchronous Notifications</h3> + * This implementation of the QMF2 Agent actually supports two independent APIs to enable clients to receive + * Asynchronous notifications. + * <p> + * A QmfEventListener object is used to receive asynchronously delivered WorkItems. + * <p> + * This provides an alternative (simpler) API to the official QMF2 WorkQueue API that some (including the Author) + * may prefer over the official API. + * <p> + * The following diagram illustrates the QmfEventListener Event model. + * <p> + * Notes + * <ol> + * <li>This is provided as an alternative to the official QMF2 WorkQueue and Notifier Event model.</li> + * <li>Agent and Console methods are sufficiently thread safe that it is possible to call them from a callback fired + * from the onEvent() method that may have been called from the JMS MessageListener. Internally the synchronous + * and asynchronous calls are processed on different JMS Sessions to facilitate this</li> + * </ol> + * <p> + * <img alt="" src="doc-files/QmfEventListenerModel.png"> + * <p> + * The QMF2 API has a work-queue Callback approach. All asynchronous events are represented by a WorkItem object. + * When a QMF event occurs it is translated into a WorkItem object and placed in a FIFO queue. It is left to the + * application to drain this queue as needed. + * <p> + * This new API does require the application to provide a single callback. The callback is used to notify the + * application that WorkItem object(s) are pending on the work queue. This callback is invoked by QMF when one or + * more new WorkItem objects are added to the queue. To avoid any potential threading issues, the application is + * not allowed to call any QMF API from within the context of the callback. The purpose of the callback is to + * notify the application to schedule itself to drain the work queue at the next available opportunity. + * <p> + * For example, a console application may be designed using a select() loop. The application waits in the select() + * for any of a number of different descriptors to become ready. In this case, the callback could be written to + * simply make one of the descriptors ready, and then return. This would cause the application to exit the wait state, + * and start processing pending events. + * <p> + * The callback is represented by the Notifier virtual base class. This base class contains a single method. An + * application derives a custom notification handler from this class, and makes it available to the Console or Agent object. + * <p> + * The following diagram illustrates the Notifier and WorkQueue QMF2 API Event model. + * <p> + * Notes + * <ol> + * <li>There is an alternative (simpler but not officially QMF2) API based on implementing the QmfEventListener as + * described previously.</li> + * <li>BlockingNotifier is not part of QMF2 either but is how most people would probably write a Notifier.</li> + * <li>It's generally not necessary to use a Notifier as the Console provides a blocking getNextWorkitem() method.</li> + * </ol> + * <p> + * <img alt="" src="doc-files/WorkQueueEventModel.png"> + * <h3>Potential Issues with Qpid versions earlier than 0.12</h3> + * Note 1: This uses QMF2 so requires that the "--mgmt-qmf2 yes" option is applied to the broker (this is the default + * from Qpid 0.10). + * <p> + * Note 2: In order to use QMF2 the app-id field needs to be set. There appears to be no way to set the AMQP 0-10 + * specific app-id field on a message which the brokers QMFv2 implementation currently requires. + * <p> + * Gordon Sim has put together a patch for org.apache.qpid.client.message.AMQMessageDelegate_0_10 + * Found in client/src/main/java/org/apache/qpid/client/message/AMQMessageDelegate_0_10.java + * <pre> + * public void setStringProperty(String propertyName, String value) throws JMSException + * { + * checkPropertyName(propertyName); + * checkWritableProperties(); + * setApplicationHeader(propertyName, value); + * + * if ("x-amqp-0-10.app-id".equals(propertyName)) + * { + * _messageProps.setAppId(value.getBytes()); + * } + * } + * </pre> + * This gets things working. + * <p> + * A jira <a href=https://issues.apache.org/jira/browse/QPID-3302>QPID-3302</a> has been raised. + * This is fixed in Qpid 0.12. + * + * @author Fraser Adams + */ +public class Agent extends QmfData implements MessageListener, SubscribableAgent +{ + private static final Logger _log = LoggerFactory.getLogger(Agent.class); + + /** + * This TimerTask causes the Agent to sent a Hearbeat when it gets scheduled + */ + private final class Heartbeat extends TimerTask + { + public void run() + { + try + { + String vendorKey = _vendor.replace(".", "_"); + String productKey = _product.replace(".", "_"); + String instanceKey = _instance.replace(".", "_"); + String subject = "agent.ind.heartbeat." + vendorKey + "." + productKey + "." + instanceKey; + + MapMessage response = _syncSession.createMapMessage(); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "indication"); + response.setStringProperty("qmf.opcode", "_agent_heartbeat_indication"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", subject); + setValue("_timestamp", System.currentTimeMillis()*1000000l); + response.setObject("_values", mapEncode()); + + // Send heartbeat messages with a Time To Live (in msecs) set to two times the _heartbeatInterval + // to prevent stale heartbeats from getting to the consoles. + _producer.send(_topicAddress, response, Message.DEFAULT_DELIVERY_MODE, + Message.DEFAULT_PRIORITY, _heartbeatInterval*2000); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in sendHeartbeat()", jmse.getMessage()); + } + + // Reap any QmfAgentData Objects that have been marked as Deleted + // Use the iterator approach rather than foreach as we may want to call iterator.remove() to zap an entry + Iterator<QmfAgentData> i = _objectIndex.values().iterator(); + while (i.hasNext()) + { + QmfAgentData object = i.next(); + if (object.isDeleted()) + { + _log.debug("Removing deleted QmfAgentData Object from store"); + i.remove(); + } + } + } + } + + // Attributes + // ******************************************************************************************************** + + /** + * The _eventListener may be a real application QmfEventListener, a NullQmfEventListener or an application + * Notifier wrapped in a QmfEventListener. In all cases the Agent may call _eventListener.onEvent() at + * various places to pass a WorkItem to an asynchronous receiver. + */ + private QmfEventListener _eventListener; + + /** + * _schemaCache holds references to the Schema objects for easy lookup so we can return the info to + * the Console if necessary + */ + private Map<SchemaClassId, SchemaClass> _schemaCache = new ConcurrentHashMap<SchemaClassId, SchemaClass>(); + + /** + * _objectIndex is the global index of QmfAgentData objects registered with this Agent. + * The capacity of 100 is pretty arbitrary but the default of 16 seems too low for most Agents. + */ + private Map<ObjectId, QmfAgentData> _objectIndex = new ConcurrentHashMap<ObjectId, QmfAgentData>(100); + + /** + * This Map is used to look up Subscriptions by SubscriptionId + */ + private Map<String, Subscription> _subscriptions = new ConcurrentHashMap<String, Subscription>(); + + /** + * Used to implement a thread safe queue of WorkItem objects used to implement the Notifier API + */ + private WorkQueue _workQueue = new WorkQueue(); + + /** + * If a name is supplied, it must be unique across all attached to the AMQP bus under the given domain. + * The name must comprise three parts separated by colons: <vendor>:<product>[:<instance>], where the + * vendor is the Agent vendor name, the product is the Agent product itself and the instance is a UUID + * representing the running instance. If the instance is not supplied then a random UUID will be generated + */ + private String _name; + + /** + * The Agent vendor name + */ + private String _vendor; + + /** + * The Agent product name + */ + private String _product; + + /** + * A UUID representing the running instance + */ + private String _instance = UUID.randomUUID().toString(); + + /** + * The epoch may be used to maintain a count of the number of times an agent has been restarted. By + * incrementing this value and keeping a constant instance value an Agent can indicate to a client + * that it is a persistent Agent and has been restarted. The Broker Management Agent behaves in this way. + */ + private int _epoch = 1; + + /** + * The interval that this Agent waits between sending out hearbeat messages + */ + private int _heartbeatInterval = 30; + + /** + * The domain string is used to construct the name of the AMQP exchange to which the component's + * name string will be bound. If not supplied, the value of the domain defaults to "default". Both + * Agents and Components must belong to the same domain in order to communicate. + */ + private String _domain; + + /** + * This timer is used to schedule periodic events such as sending Heartbeats and subscription updates + */ + private Timer _timer; + + /** + * Various JMS related fields + */ + private Connection _connection = null; + private Session _asyncSession; + private Session _syncSession; + private MessageConsumer _locateConsumer; + private MessageConsumer _mainConsumer; + // _aliasConsumer is used for the alias address if the Agent is a broker Agent (used in Java Broker QMF plugin) + private MessageConsumer _aliasConsumer; + + private MessageProducer _producer; + + private String _quotedDirectBase; + private Destination _directAddress; + + private String _quotedTopicBase; + private Destination _topicAddress; + + // private implementation methods + // ******************************************************************************************************** + + + /** + * There's some slight "hackery" below. The Agent clearly needs to respond + * to requests and quite possibly using the JMS replyTo is the correct thing + * to do, however in older versions of Qpid invoking send() on the replyTo + * causes spurious exchangeDeclares to occur and the caching of replyTo wasn't + * as good as it might be. To get around this the Agent uses exchange name + * as the core address and sets the Message "qpid.subject" property with an + * appropriate Routing Key. + * @param handle the reply handle that contains the replyTo Address. + * @param message the JMS Message to be sent. + */ + private final void sendResponse(final Handle handle, final Message message) throws JMSException + { + // Just in case the replyTo issues still exist check if the replyTo starts + // with qmf.default.topic or qmf.default.direct and if so send to the + // main topic or direct Destinations, if not fall back to using the real + // replyTo Destination. TODO check if original replyTo issue still exists. + String replyTo = handle.getReplyTo().toString(); + if (replyTo.startsWith(_quotedTopicBase)) + { + _producer.send(_topicAddress, message); + } + else if (replyTo.startsWith(_quotedDirectBase)) + { + _producer.send(_directAddress, message); + } + else + { + _producer.send(handle.getReplyTo(), message); + } + } + + /** + * Send an _agent_locate_response back to the Console that requested the locate. + * @param handle the reply handle that contains the replyTo Address. + */ + private final void handleLocateRequest(final Handle handle) + { + try + { + MapMessage response = _syncSession.createMapMessage(); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "indication"); + response.setStringProperty("qmf.opcode", "_agent_locate_response"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + setValue("_timestamp", System.currentTimeMillis()*1000000l); + response.setObject("_values", mapEncode()); + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in handleLocateRequest()", jmse.getMessage()); + } + } + + /** + * Handle the query request and send the response back to the Console. + * @param handle the reply handle that contains the replyTo Address. + * @param query the inbound query from the Console. + */ + @SuppressWarnings("unchecked") + private final void handleQueryRequest(final Handle handle, final QmfQuery query) + { + QmfQueryTarget target = query.getTarget(); + + if (target == QmfQueryTarget.SCHEMA_ID) + { + List<Map> results = new ArrayList<Map>(_schemaCache.size()); + // Look up all SchemaClassId objects + for (SchemaClassId classId : _schemaCache.keySet()) + { + results.add(classId.mapEncode()); + } + queryResponse(handle, results, "_schema_id"); // Send the response back to the Console. + } + else if (target == QmfQueryTarget.SCHEMA) + { + List<Map> results = new ArrayList<Map>(1); + // Look up a SchemaClass object by the SchemaClassId obtained from the query + SchemaClassId classId = query.getSchemaClassId(); + SchemaClass schema = _schemaCache.get(classId); + if (schema != null) + { + results.add(schema.mapEncode()); + } + queryResponse(handle, results, "_schema"); // Send the response back to the Console. + } + else if (target == QmfQueryTarget.OBJECT_ID) + { + List<Map> results = new ArrayList<Map>(_objectIndex.size()); + // Look up all ObjectId objects + for (ObjectId objectId : _objectIndex.keySet()) + { + results.add(objectId.mapEncode()); + } + queryResponse(handle, results, "_object_id"); // Send the response back to the Console. + } + else if (target == QmfQueryTarget.OBJECT) + { + // If this is implementing the AgentExternal model we pass the QmfQuery on in a QueryWorkItem + if (this instanceof AgentExternal) + { + _eventListener.onEvent(new QueryWorkItem(handle, query)); + return; + } + else + { // If not implementing the AgentExternal model we handle the Query ourself. + //qmfContentType = "_data"; + if (query.getObjectId() != null) + { + List<Map> results = new ArrayList<Map>(1); + // Look up a QmfAgentData object by the ObjectId obtained from the query + ObjectId objectId = query.getObjectId(); + QmfAgentData object = _objectIndex.get(objectId); + if (object != null && !object.isDeleted()) + { + results.add(object.mapEncode()); + } + queryResponse(handle, results, "_data"); // Send the response back to the Console. + } + else + { + // Look up QmfAgentData objects by the SchemaClassId obtained from the query + // This is implemented by a linear search and allows searches with only the className specified. + // Linear searches clearly don't scale brilliantly, but the number of QmfAgentData objects managed + // by an Agent is generally fairly small, so it should be OK. Note that this is the same approach + // taken by the C++ broker ManagementAgent, so if it's a problem here........ + + // N.B. the results list declared here is a generic List of Objects. We *must* only pass a List of + // Map to queryResponse(), but conversely if the response items are sortable we need to sort them + // before doing mapEncode(). Unfortunately we don't know if the items are sortable a priori so + // we either add a Map or we add a QmfAgentData, then sort then mapEncode() each item. I'm not + // sure of a more elegant way to do this without creating two lists, which might not be so bad + // but we don't know the size of the list a priori either. + List results = new ArrayList(_objectIndex.size()); + // It's unlikely that evaluating this query will return a mixture of sortable and notSortable + // QmfAgentData objects, but it's best to check if that has occurred as accidentally passing a + // List of QmfAgentData instead of a List of Map to queryResponse() will break things. + boolean sortable = false; + boolean notSortable = false; + for (QmfAgentData object : _objectIndex.values()) + { + if (!object.isDeleted() && query.evaluate(object)) + { + if (object.isSortable()) + { // If QmfAgentData is marked sortable we add the QmfAgentData object to the List + // so we can sort first before mapEncoding. + results.add(object); + sortable = true; + } + else + { // If QmfAgentData is not marked sortable we mapEncode immediately and add the Map to List. + results.add(object.mapEncode()); + notSortable = true; + } + } + } + + // If both flags have been set something has gone a bit weird, so we log an error and clear the + // results List to avoid sending unconvertable data. Hopefully this condition should never occur. + if (sortable && notSortable) + { + _log.info("Query resulted in inconsistent mixture of sortable and non-sortable data."); + results.clear(); + } + else if (sortable) + { + Collections.sort(results); + int length = results.size(); + for (int i = 0; i < length; i++) + { + QmfAgentData object = (QmfAgentData)results.get(i); + results.set(i, object.mapEncode()); + } + } + queryResponse(handle, results, "_data"); // Send the response back to the Console. + } + } + } + else + { + raiseException(handle, "Query for _what => '" + target + "' not supported"); + return; + } + } // end of handleQueryRequest() + + /** + * Return a QmfAgentData from the internal Object store given its ObjectId. + * N.B. This method isn't part of the *official* QMF2 public API, however it is pretty useful and probably + * should be (as should evaluateQuery()). Given that wi.getType() == METHOD_CALL primarily uses the pattern: + * <pre> + * MethodCallWorkItem item = (MethodCallWorkItem)wi; + * MethodCallParams methodCallParams = item.getMethodCallParams(); + * String methodName = methodCallParams.getName(); + * ObjectId objectId = methodCallParams.getObjectId(); + * </pre> + * to identify the method name and Object instance it seems odd not to have a public API method to look up + * said Object by ObjectId. Clearly a separate Map could be maintained in client code but that seems pointless. + */ + public final QmfAgentData getObject(ObjectId objectId) + { + return _objectIndex.get(objectId); + } + + /** + * Send an exception back to the Console. + * @param handle the reply handle that contains the replyTo Address. + * @param message the exception message. + */ + public final void raiseException(final Handle handle, final String message) + { + try + { + MapMessage response = _syncSession.createMapMessage(); + response.setJMSCorrelationID(handle.getCorrelationId()); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "response"); + response.setStringProperty("qmf.opcode", "_exception"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + + QmfData exception = new QmfData(); + exception.setValue("error_text", message); + response.setObject("_values", exception.mapEncode()); + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in handleLocateRequest()", jmse.getMessage()); + } + } + + // methods implementing SubscribableAgent interface + // ******************************************************************************************************** + + /** + * Send a list of updated subscribed data to the Console. + * + * @param handle the console reply handle. + * @param results a list of subscribed data in Map encoded form. + */ + public final void sendSubscriptionIndicate(final Handle handle, final List<Map> results) + { + try + { + Message response = AMQPMessage.createListMessage(_syncSession); + response.setJMSCorrelationID(handle.getCorrelationId()); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "indication"); + response.setStringProperty("qmf.opcode", "_data_indication"); + response.setStringProperty("qmf.content", "_data"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + AMQPMessage.setList(response, results); + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in sendSubscriptionIndicate()", jmse.getMessage()); + } + } + + /** + * This method evaluates a QmfQuery over the Agent's data on behalf of a Subscription. + * + * @param query the QmfQuery that the Subscription wants to be evaluated over the Agent's data. + * @return a List of QmfAgentData objects that match the specified QmfQuery. + */ + public final List<QmfAgentData> evaluateQuery(final QmfQuery query) + { + List<QmfAgentData> results = new ArrayList<QmfAgentData>(_objectIndex.size()); + if (query.getTarget() == QmfQueryTarget.OBJECT) + { // Note that we don't include objects marked as deleted in the results here, because if an object gets + // destroyed we asynchronously publish its new state to subscribers, see QmfAgentData.destroy() method. + if (query.getObjectId() != null) + { + // Look up a QmfAgentData object by the ObjectId obtained from the query + ObjectId objectId = query.getObjectId(); + QmfAgentData object = _objectIndex.get(objectId); + if (object != null && !object.isDeleted()) + { + results.add(object); + } + } + else + { + // Look up QmfAgentData objects evaluating the query + for (QmfAgentData object : _objectIndex.values()) + { + if (!object.isDeleted() && query.evaluate(object)) + { + results.add(object); + } + } + } + } + return results; + } + + /** + * This method is called by the Subscription to tell the SubscribableAgent that the Subscription has been cancelled. + * + * @param subscription the Subscription that has been cancelled and is requesting removal. + */ + public final void removeSubscription(final Subscription subscription) + { + _subscriptions.remove(subscription.getSubscriptionId()); + } + + // MessageListener + // ******************************************************************************************************** + + /** + * MessageListener for QMF2 Console requests. + * + * @param message the JMS Message passed to the listener. + */ + public final void onMessage(final Message message) + { + try + { + String agentName = QmfData.getString(message.getObjectProperty("qmf.agent")); + String content = QmfData.getString(message.getObjectProperty("qmf.content")); + String opcode = QmfData.getString(message.getObjectProperty("qmf.opcode")); + //String routingKey = ((javax.jms.Topic)message.getJMSDestination()).getTopicName(); + //String contentType = ((org.apache.qpid.client.message.AbstractJMSMessage)message).getContentType(); + +//System.out.println(); +//System.out.println("agentName = " + agentName); +//System.out.println("content = " + content); +//System.out.println("opcode = " + opcode); +//System.out.println("routingKey = " + routingKey); +//System.out.println("contentType = " + contentType); + + Handle handle = new Handle(message.getJMSCorrelationID(), message.getJMSReplyTo()); + + if (opcode.equals("_agent_locate_request")) + { + handleLocateRequest(handle); + } + else if (opcode.equals("_method_request")) + { + if (AMQPMessage.isAMQPMap(message)) + { + _eventListener.onEvent + ( + new MethodCallWorkItem(handle, new MethodCallParams(AMQPMessage.getMap(message))) + ); + } + else + { + _log.info("onMessage() Received Method Request message in incorrect format"); + } + } + else if (opcode.equals("_query_request")) + { + if (AMQPMessage.isAMQPMap(message)) + { + try + { + QmfQuery query = new QmfQuery(AMQPMessage.getMap(message)); + handleQueryRequest(handle, query); + } + catch (QmfException qmfe) + { + raiseException(handle, "Query Request failed, invalid Query: " + qmfe.getMessage()); + } + } + else + { + _log.info("onMessage() Received Query Request message in incorrect format"); + } + } + else if (opcode.equals("_subscribe_request")) + { + if (AMQPMessage.isAMQPMap(message)) + { + try + { + SubscriptionParams subscriptionParams = + new SubscriptionParams(handle, AMQPMessage.getMap(message)); + if (this instanceof AgentExternal) + { + _eventListener.onEvent(new SubscribeRequestWorkItem(handle, subscriptionParams)); + } + else + { + Subscription subscription = new Subscription(this, subscriptionParams); + String subscriptionId = subscription.getSubscriptionId(); + _subscriptions.put(subscriptionId, subscription); + _timer.schedule(subscription, 0, subscriptionParams.getPublishInterval()); + subscriptionResponse(handle, subscription.getConsoleHandle(), subscriptionId, + subscription.getDuration(), subscription.getInterval(), null); + } + } + catch (QmfException qmfe) + { + raiseException(handle, "Subscribe Request failed, invalid Query: " + qmfe.getMessage()); + } + } + else + { + _log.info("onMessage() Received Subscribe Request message in incorrect format"); + } + } + else if (opcode.equals("_subscribe_refresh_indication")) + { + if (AMQPMessage.isAMQPMap(message)) + { + ResubscribeParams resubscribeParams = new ResubscribeParams(AMQPMessage.getMap(message)); + if (this instanceof AgentExternal) + { + _eventListener.onEvent(new ResubscribeRequestWorkItem(handle, resubscribeParams)); + } + else + { + String subscriptionId = resubscribeParams.getSubscriptionId(); + Subscription subscription = _subscriptions.get(subscriptionId); + if (subscription != null) + { + subscription.refresh(resubscribeParams); + subscriptionResponse(handle, + subscription.getConsoleHandle(), subscription.getSubscriptionId(), + subscription.getDuration(), subscription.getInterval(), null); + } + } + } + else + { + _log.info("onMessage() Received Resubscribe Request message in incorrect format"); + } + } + else if (opcode.equals("_subscribe_cancel_indication")) + { + if (AMQPMessage.isAMQPMap(message)) + { + QmfData qmfSubscribe = new QmfData(AMQPMessage.getMap(message)); + String subscriptionId = qmfSubscribe.getStringValue("_subscription_id"); + if (this instanceof AgentExternal) + { + _eventListener.onEvent(new UnsubscribeRequestWorkItem(subscriptionId)); + } + else + { + Subscription subscription = _subscriptions.get(subscriptionId); + if (subscription != null) + { + subscription.cancel(); + } + } + } + else + { + _log.info("onMessage() Received Subscribe Cancel Request message in incorrect format"); + } + } + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in onMessage()", jmse.getMessage()); + } + } // end of onMessage() + + // QMF API Methods + // ******************************************************************************************************** + + /** + * Constructor that provides defaults for name, domain and heartbeat interval and takes a Notifier/Listener. + * + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + */ + public Agent(final QmfCallback notifier) throws QmfException + { + this(null, null, notifier, 30); + } + + /** + * Constructor that provides defaults for name and domain and takes a Notifier/Listener + * + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + * @param interval is the heartbeat interval in seconds. + */ + public Agent(final QmfCallback notifier, final int interval) throws QmfException + { + this(null, null, notifier, interval); + } + + /** + * Main constructor, creates a Agent, but does NOT start it, that requires us to do setConnection() + * + * @param name If a name is supplied, it must be unique across all agents attached to the AMQP bus under the + * given domain. + * <p> + * The name must comprise three parts separated by colons: <pre><vendor>:<product>[:<instance>]</pre> + * where the vendor is the Agent vendor name, the product is the Agent product itself and the instance is a UUID + * representing the running instance. + * <p> + * If the instance is not supplied then a random UUID will be generated. + * @param domain the QMF "domain". + * <p> + * A QMF address is composed of two parts - an optional domain string, and a mandatory name string + * <pre>"qmf.<domain-string>.direct/<name-string>"</pre> + * The domain string is used to construct the name of the AMQP exchange to which the component's name string will + * be bound. If not supplied, the value of the domain defaults to "default". + * <p> + * Both Agents and Consoles must belong to the same domain in order to communicate. + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + * @param interval is the heartbeat interval in seconds. + */ + public Agent(final String name, final String domain, + final QmfCallback notifier, final int interval) throws QmfException + { + if (name != null) + { + String[] split = name.split(":"); + if (split.length < 2 || split.length > 3) + { + throw new QmfException("Agent name must be in the format <vendor>:<product>[:<instance>]"); + } + + _vendor = split[0]; + _product = split[1]; + + if (split.length == 3) + { + _instance = split[2]; + } + + _name = _vendor + ":" + _product + ":" + _instance; + } + + _domain = (domain == null) ? "default" : domain; + + if (notifier == null) + { + _eventListener = new NullQmfEventListener(); + } + else if (notifier instanceof Notifier) + { + _eventListener = new NotifierWrapper((Notifier)notifier, _workQueue); + } + else if (notifier instanceof QmfEventListener) + { + _eventListener = (QmfEventListener)notifier; + } + else + { + throw new QmfException("QmfCallback listener must be either a Notifier or QmfEventListener"); + } + + if (interval > 0) + { + _heartbeatInterval = interval; + } + } + + /** + * Returns the name string of the agent. + * @return the name string of the agent. + */ + public final String getName() + { + return _name; + } + + /** + * Set the vendor String, must be called before setConnection(). + * @param vendor the vendor name. + */ + public final void setVendor(final String vendor) + { + _vendor = vendor; + _name = _vendor + ":" + _product + ":" + _instance; + } + + /** + * Set the product String, must be called before setConnection(). + * @param product the product name. + */ + public final void setProduct(final String product) + { + _product = product; + _name = _vendor + ":" + _product + ":" + _instance; + } + + /** + * Set the instance String, must be called before setConnection(). + * @param instance the instance value. + */ + public final void setInstance(final String instance) + { + _instance = instance; + _name = _vendor + ":" + _product + ":" + _instance; + } + + /** + * Returns the current epoch value. + * @return the current epoch value. + */ + public final int getEpoch() + { + return _epoch; + } + + /** + * Set the new epoch value. + * @param epoch the new epoch value. + */ + public final void setEpoch(final int epoch) + { + _epoch = epoch; + } + + /** + * Releases Agent's resources. + */ + public final void destroy() + { + try + { + if (_connection != null) + { + removeConnection(_connection); + } + } + catch (QmfException qmfe) + { + // Ignore as we've already tested for _connection != null this should never occur + } + } + + /** + * Connect the Agent to the AMQP cloud. + * + * @param conn a javax.jms.Connection. + */ + public final void setConnection(final Connection conn) throws QmfException + { + setConnection(conn, ""); + } + + /** + * Connect the Agent to the AMQP cloud. + * <p> + * This is an extension to the standard QMF2 API allowing the user to specify address options in order to allow + * finer control over the Agent's ingest queue, such as an explicit name, non-default size or durability. + * + * @param conn a javax.jms.Connection. + * @param addressOptions options String giving finer grained control of the receiver queue. + * <p> + * As an example the following gives the Agent's ingest queue the name test-agent, size = 500000000 and ring policy. + * <pre> + * " ; {link: {name:'test-agent', x-declare: {arguments: {'qpid.policy_type': ring, 'qpid.max_size': 500000000}}}}" + * </pre> + */ + public final void setConnection(final Connection conn, final String addressOptions) throws QmfException + { + // Make the test and set of _connection synchronized just in case multiple threads attempt to add a _connection + // to the same Agent instance at the same time. + synchronized(this) + { + if (_connection != null) + { + throw new QmfException("Multiple connections per Agent is not supported"); + } + _connection = conn; + } + + if (_name == null || _vendor == null || _product == null) + { + throw new QmfException("The vendor, product or name is not set"); + } + + setValue("_epoch", _epoch); + setValue("_heartbeat_interval", _heartbeatInterval); + setValue("_name", _name); + setValue("_product", _product); + setValue("_vendor", _vendor); + setValue("_instance", _instance); + + try + { + _asyncSession = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + _syncSession = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Create a Destination for the QMF direct address, mainly used for request/response + String directBase = "qmf." + _domain + ".direct"; + _quotedDirectBase = "'" + directBase + "'"; + _directAddress = _syncSession.createQueue(directBase); + + // Create a Destination for the QMF topic address used to broadcast Events & Heartbeats. + String topicBase = "qmf." + _domain + ".topic"; + _quotedTopicBase = "'" + topicBase + "'"; + _topicAddress = _syncSession.createQueue(topicBase); + + // Create an unidentified MessageProducer for sending to various destinations. + _producer = _syncSession.createProducer(null); + + // TODO it should be possible to bind _locateConsumer, _mainConsumer and _aliasConsumer to the + // same queue if I can figure out the correct AddressString to use, probably not a big deal though. + + // Set up MessageListener on the Agent Locate Address + Destination locateAddress = _asyncSession.createQueue(topicBase + "/console.request.agent_locate"); + _locateConsumer = _asyncSession.createConsumer(locateAddress); + _locateConsumer.setMessageListener(this); + + // Set up MessageListener on the Agent address + String address = directBase + "/" + _name + addressOptions; + Destination agentAddress = _asyncSession.createQueue(address); + _mainConsumer = _asyncSession.createConsumer(agentAddress); + _mainConsumer.setMessageListener(this); + + // If the product name has been set to qpidd we create an additional consumer address of + // "qmf.default.direct/broker" in addition to the main address so that Consoles can talk to the + // broker Agent without needing to do Agent discovery. This is only really needed when the Agent + // class has been used to create the QmfManagementAgent for the Java broker QmfManagementPlugin. + // It's important to do this as many tools (such as qpid-config) and demo code tend to use the + // alias address rather than the discovered address when talking to the broker ManagementAgent. + if (_product.equals("qpidd")) + { + String alias = directBase + "/broker"; + _log.info("Creating address {} as an alias address for the broker Agent", alias); + Destination aliasAddress = _asyncSession.createQueue(alias); + _aliasConsumer = _asyncSession.createConsumer(aliasAddress); + _aliasConsumer.setMessageListener(this); + } + + _connection.start(); + + // Schedule a Heartbeat every _heartbeatInterval seconds sending the first one immediately + _timer = new Timer(true); + _timer.schedule(new Heartbeat(), 0, _heartbeatInterval*1000); + } + catch (JMSException jmse) + { + // If we can't create the QMF Destinations there's not much else we can do + _log.info("JMSException {} caught in setConnection()", jmse.getMessage()); + throw new QmfException("Failed to create sessions or destinations " + jmse.getMessage()); + } + } // end of setConnection() + + /** + * Remove the AMQP connection from the Agent. Un-does the setConnection() operation. + * + * @param conn a javax.jms.Connection. + */ + public final void removeConnection(final Connection conn) throws QmfException + { + if (conn != _connection) + { + throw new QmfException("Attempt to delete unknown connection"); + } + + try + { + _timer.cancel(); + _connection.close(); + } + catch (JMSException jmse) + { + throw new QmfException("Failed to remove connection, caught JMSException " + jmse.getMessage()); + } + _connection = null; + } + + /** + * Register a schema for an object class with the Agent. + * <p> + * The Agent must have a registered schema for an object class before it can handle objects of that class. + * + * @param schema the SchemaObjectClass to be registered + */ + public final void registerObjectClass(final SchemaObjectClass schema) + { + SchemaClassId classId = schema.getClassId(); + _schemaCache.put(classId, schema); + } + + /** + * Register a schema for an event class with the Agent. + * <p> + * The Agent must have a registered schema for an event class before it can handle events of that class. + * + * @param schema the SchemaEventClass to be registered + */ + public final void registerEventClass(final SchemaEventClass schema) + { + SchemaClassId classId = schema.getClassId(); + _schemaCache.put(classId, schema); + } + + /** + * Cause the agent to raise the given event. + * + * @param event the QmfEvent to be raised + */ + public final void raiseEvent(final QmfEvent event) + { + try + { + String packageKey = event.getSchemaClassId().getPackageName().replace(".", "_"); + String nameKey = event.getSchemaClassId().getClassName().replace(".", "_"); + String severity = event.getSeverity(); + String vendorKey = _vendor.replace(".", "_"); + String productKey = _product.replace(".", "_"); + String instanceKey = _instance.replace(".", "_"); + + String subject = "agent.ind.event." + packageKey + "." + nameKey + "." + severity + "." + vendorKey + "." + + productKey + "." + instanceKey; + + Message response = AMQPMessage.createListMessage(_syncSession); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "indication"); + response.setStringProperty("qmf.opcode", "_data_indication"); + response.setStringProperty("qmf.content", "_event"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", subject); + + List<Map> results = new ArrayList<Map>(); + results.add(event.mapEncode()); + AMQPMessage.setList(response, results); + _producer.send(_topicAddress, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in raiseEvent()", jmse.getMessage()); + } + } + + /** + * Passes a reference to an instance of a managed QMF object to the Agent. + * <p> + * The object's name must uniquely identify this object among all objects known to this Agent. + * <p> + * This method creates an ObjectId for the QmfAgentData being added, it does this by first checking + * the schema. + * <p> + * If an associated schema exists we look for the set of property names that have been + * specified as idNames. If idNames exists we look for their values within the object and use that + * to create the objectName. If we can't create a sensible name we use a randomUUID. + * @param object the QmfAgentData object to be added + */ + public void addObject(final QmfAgentData object) throws QmfException + { + // There are some cases where a QmfAgentData Object might have already set its ObjectId, for example where + // it may need to have a "well known" ObjectId. This is the case with the Java Broker Management Agent + // where tools such as qpid-config might have made assumptions about its ObjectId rather than doing "discovery". + ObjectId addr = object.getObjectId(); + if (addr == null) + { + SchemaClassId classId = object.getSchemaClassId(); + SchemaClass schema = _schemaCache.get(classId); + + // Try to create an objectName using the property names that have been specified as idNames in the schema + StringBuilder buf = new StringBuilder(); + // Initialise idNames as an empty array as we want to check if a key has been used to construct the name. + String[] idNames = {}; + if (schema != null && schema instanceof SchemaObjectClass) + { + idNames = ((SchemaObjectClass)schema).getIdNames(); + for (String property : idNames) + { + buf.append(object.getStringValue(property)); + } + } + String objectName = buf.toString(); + + // If the schema hasn't given any help we use a UUID. Note that we check the length of idNames too + // as a given named key property might legitimately be an empty string (e.g. the default direct + // exchange has name == "") + if (objectName.length() == 0 && idNames.length == 0) objectName = UUID.randomUUID().toString(); + + // Finish up the name by incorporating package and class names + objectName = classId.getPackageName() + ":" + classId.getClassName() + ":" + objectName; + + // Now we've got a good name for the object we create its ObjectId and add that to the object + addr = new ObjectId(_name, objectName, _epoch); + + object.setObjectId(addr); + } + + QmfAgentData foundObject = _objectIndex.get(addr); + if (foundObject != null) + { + // If a duplicate object has actually been Deleted we can reuse the address. + if (!foundObject.isDeleted()) + { + throw new QmfException("Duplicate QmfAgentData Address"); + } + } + + _objectIndex.put(addr, object); + + // Does the new object match any Subscriptions? If so add a reference to the matching Subscription and publish. + for (Subscription subscription : _subscriptions.values()) + { + QmfQuery query = subscription.getQuery(); + if (query.getObjectId() != null) + { + if (query.getObjectId().equals(addr)) + { + object.addSubscription(subscription.getSubscriptionId(), subscription); + object.publish(); + } + } + else if (query.evaluate(object)) + { + object.addSubscription(subscription.getSubscriptionId(), subscription); + object.publish(); + } + } + } // end of addObject() + + /** + * Returns the count of pending WorkItems that can be retrieved. + * @return the count of pending WorkItems that can be retrieved. + */ + public final int getWorkitemCount() + { + return _workQueue.size(); + } + + /** + * Obtains the next pending work item - blocking version. + * <p> + * The blocking getNextWorkitem() can be used without the need for a Notifier as it will block until + * a new item gets added to the work queue e.g. the following usage pattern. + * <pre> + * while ((wi = agent.getNextWorkitem()) != null) + * { + * System.out.println("WorkItem type: " + wi.getType()); + * } + * </pre> + * @return the next pending work item, or null if none available. + */ + public final WorkItem getNextWorkitem() + { + return _workQueue.getNextWorkitem(); + } + + /** + * Obtains the next pending work item - balking version. + * <p> + * The balking getNextWorkitem() is generally used with a Notifier which can be used as a gate to determine + * if any work items are available e.g. the following usage pattern. + * <pre> + * while (true) + * { + * notifier.waitForWorkItem(); // Assuming a BlockingNotifier has been used here + * System.out.println("WorkItem available, count = " + agent.getWorkitemCount()); + * + * WorkItem wi; + * while ((wi = agent.getNextWorkitem(0)) != null) + * { + * System.out.println("WorkItem type: " + wi.getType()); + * } + * } + * </pre> + * Note that it is possible for the getNextWorkitem() loop to retrieve multiple items from the _workQueue + * and for the Agent to add new items as the loop is looping thus when it finally exits and goes + * back to the outer loop notifier.waitForWorkItems() may return immediately as it had been notified + * whilst we were in the getNextWorkitem() loop. This will be evident by a getWorkitemCount() of 0 + * after returning from waitForWorkItem(). + * <p> + * This is the expected behaviour, but illustrates the need to check for nullness of the value returned + * by getNextWorkitem() - or alternatively to use getWorkitemCount() to put getNextWorkitem() in a + * bounded loop. + * + * @param timeout the timeout in seconds. If timeout = 0 it returns immediately with either a WorkItem or null + * @return the next pending work item, or null if none available. + */ + public final WorkItem getNextWorkitem(final long timeout) + { + return _workQueue.getNextWorkitem(timeout); + } + + /** + * Releases a WorkItem instance obtained by getNextWorkItem(). Called when the application has finished + * processing the WorkItem. + */ + public final void releaseWorkitem() + { + // To be honest I'm not clear what the intent of this method actually is. One thought is that it's here + // to support equivalent behaviour to the Python Queue.task_done() which is used by queue consumer threads. + // For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing + // on the task is complete. + // + // The problem with that theory is there is no equivalent QMF2 API call that would invoke the + // Queue.join() which is used in conjunction with Queue.task_done() to enable a synchronisation gate to + // be implemented to wait for completion of all worker thread. + // + // I'm a bit stumped and there's no obvious Java equivalent on BlockingQueue, so for now this does nothing. + } + + /** + * Indicate to the Agent that the application has completed processing a method request. + * <p> + * See the description of the METHOD_CALL WorkItem. + * @param methodName the method's name. + * @param handle the reply handle from WorkItem. + * @param outArgs the output argument map. + * @param error the error object that was created if the method failed in any way, otherwise null. + */ + public final void methodResponse(final String methodName, final Handle handle, + final QmfData outArgs, final QmfData error) + { + try + { + MapMessage response = _syncSession.createMapMessage(); + response.setJMSCorrelationID(handle.getCorrelationId()); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "response"); + response.setStringProperty("qmf.opcode", "_method_response"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + + if (error == null) + { + if (outArgs != null) + { + response.setObject("_arguments", outArgs.mapEncode()); + if (outArgs.getSubtypes() != null) + { + response.setObject("_subtypes", outArgs.getSubtypes()); + } + } + } + else + { + Map<String, Object> errorMap = error.mapEncode(); + for (Map.Entry<String, Object> entry : errorMap.entrySet()) + { + response.setObject(entry.getKey(), entry.getValue()); + } + } + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in methodResponse()", jmse.getMessage()); + } + } + + /** + * Send the query response back to the Console. + * @param handle the reply handle that contains the replyTo Address. + * @param results the list of mapEncoded query results. + * @param qmfContentType the value to be passed to the qmf.content Header. + */ + protected final void queryResponse(final Handle handle, List<Map> results, final String qmfContentType) + { + try + { + Message response = AMQPMessage.createListMessage(_syncSession); + response.setJMSCorrelationID(handle.getCorrelationId()); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "response"); + response.setStringProperty("qmf.opcode", "_query_response"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qmf.content", qmfContentType); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + AMQPMessage.setList(response, results); + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in queryResponse()", jmse.getMessage()); + } + } + + /** + * If the subscription request is successful, the Agent application must provide a unique subscriptionId. + * <p> + * If replying to a sucessful subscription refresh, the original subscriptionId must be supplied. + * <p> + * If the subscription or refresh fails, the subscriptionId should be set to null and error may be set to + * an application-specific QmfData instance that describes the error. + * <p> + * Should a refresh request fail, the consoleHandle may be set to null if unknown. + * + * @param handle the handle from the WorkItem. + * @param consoleHandle the console reply handle. + * @param subscriptionId a unique handle for the subscription supplied by the Agent. + * @param lifetime should be set to the duration of the subscription in seconds. + * @param publishInterval should be set to the time interval in seconds between successive publications + * on this subscription. + * @param error an application-specific QmfData instance that describes the error. + */ + public final void subscriptionResponse(final Handle handle, final Handle consoleHandle, final String subscriptionId, + final long lifetime, final long publishInterval, final QmfData error) + { + try + { + MapMessage response = _syncSession.createMapMessage(); + response.setJMSCorrelationID(handle.getCorrelationId()); + response.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + response.setStringProperty("method", "response"); + response.setStringProperty("qmf.opcode", "_subscribe_response"); + response.setStringProperty("qmf.agent", _name); + response.setStringProperty("qpid.subject", handle.getRoutingKey()); + + if (error == null) + { + response.setObject("_subscription_id", subscriptionId); + response.setObject("_duration", lifetime); + response.setObject("_interval", publishInterval); + } + else + { + Map<String, Object> errorMap = error.mapEncode(); + for (Map.Entry<String, Object> entry : errorMap.entrySet()) + { + response.setObject(entry.getKey(), entry.getValue()); + } + } + sendResponse(handle, response); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in subscriptionResponse()", jmse.getMessage()); + } + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/AgentExternal.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/AgentExternal.java new file mode 100644 index 0000000000..25b2db3d69 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/AgentExternal.java @@ -0,0 +1,259 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +// Misc Imports +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfCallback; +import org.apache.qpid.qmf2.common.QmfException; + +/** + * The AgentExternal class must be used by those applications that implement the external store model described in + * <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a>. + * <p> + * The AgentExternal class extends the Agent class by adding interfaces that notify the application when it needs to + * service a request for management operations from the agent. + * <p> + * N.B. The author is not convinced that there is any particular advantage of the AgentExternal model over the + * basic Agent model and indeed the API forces some constructs that are actually likely to be less efficient, as an + * example sending a separate queryResponse() for each object forces a look up of a List of QmfAgentData objects + * keyed by the consoleHandle for each call. There is also the need to separately iterate through the List of + * QmfAgentData objects thus created to create the mapEncoded list needed for sending via the QMF2 protocol. + * There are similar inefficiencies imposed in the subscriptionIndicate() method that are not present in the + * Subscription code implemented in the Agent class for the "Internal Store" Agent model. + * <p> + * To be honest the author only bothered to implement AgentExternal for completeness and is unlikely to use it himself. + * + * @author Fraser Adams + */ +public final class AgentExternal extends Agent +{ + /** + * This Map is used to hold query results. This is necessary as the API has each queryResponse() call send + * back an individual QmfAgentData, so we need to maintain these in a list keyed by the consoleHandle until + * the queryComplete() gets sent. + */ + private Map<String, List<QmfAgentData>> _queryResults = new ConcurrentHashMap<String, List<QmfAgentData>>(); + + // QMF API Methods + // ******************************************************************************************************** + + /** + * Constructor that provides defaults for name, domain and heartbeat interval and takes a Notifier/Listener. + * + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + */ + public AgentExternal(final QmfCallback notifier) throws QmfException + { + super(null, null, notifier, 30); + } + + /** + * Constructor that provides defaults for name and domain and takes a Notifier/Listener + * + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + * @param interval is the heartbeat interval in seconds. + */ + public AgentExternal(final QmfCallback notifier, final int interval) throws QmfException + { + super(null, null, notifier, interval); + } + + /** + * Main constructor, creates a Agent, but does NOT start it, that requires us to do setConnection() + * + * @param name If a name is supplied, it must be unique across all attached to the AMQP bus under the given domain. + * <p> + * The name must comprise three parts separated by colons: <pre><vendor>:<product>[:<instance>]</pre> + * where the vendor is the Agent vendor name, the product is the Agent product itself and the instance is a UUID + * representing the running instance. + * <p> + * If the instance is not supplied then a random UUID will be generated. + * @param domain the QMF "domain". + * <p> + * A QMF address is composed of two parts - an optional domain string, and a mandatory name string + * <pre>"qmf.<domain-string>.direct/<name-string>"</pre> + * The domain string is used to construct the name of the AMQP exchange to which the component's name string will + * be bound. If not supplied, the value of the domain defaults to "default". + * <p> + * Both Agents and Components must belong to the same domain in order to communicate. + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. + * <p> + * This API may be simpler and more convenient than the QMF2 Notifier API for many applications. + * @param interval is the heartbeat interval in seconds. + */ + public AgentExternal(final String name, final String domain, + final QmfCallback notifier, final int interval) throws QmfException + { + super(name, domain, notifier, interval); + } + + /** + * We override the base Class addObject() to throw an Exception as addObject() is used to populate the + * <b>internal</b> store. + */ + @Override + public void addObject(final QmfAgentData object) throws QmfException + { + throw new QmfException("Cannot call addObject() on AgentExternal as this method is used to populate the internal object store"); + } + + /** + * Indicate to QMF that the named object is available to be managed. Once this method returns, the agent will + * service requests from consoles referencing this data. + * + * @param objectName the name of the QmfAgentData being managed. + * @return a new ObjectId based on the objectName passed as a parameter. + */ + public ObjectId allocObjectId(final String objectName) + { + return new ObjectId(getName(), objectName, getEpoch()); + } + + /** + * Indicate to QMF that the named object is no longer available to be managed. + * + * @param objectName the name of the QmfAgentData being managed. + */ + public void freeObjectId(final String objectName) + { + // Null implementation. It's not really clear that there's anything useful that the Agent needs to do here + } + + /** + * Send a managed object in reply to a received query. Note that ownership of the object instance is returned to + * the caller on return from this call. + * + * @param handle the handle from the WorkItem. + * @param object a managed QmfAgentData object. + */ + public void queryResponse(final Handle handle, final QmfAgentData object) + { + String index = handle.getCorrelationId(); + List<QmfAgentData> objects = _queryResults.get(index); + if (objects == null) + { + objects = new ArrayList<QmfAgentData>(); + _queryResults.put(index, objects); + } + objects.add(object); + } + + /** + * Indicate to the agent that the application has completed processing a query request. + * Zero or more calls to the queryResponse() method should be invoked before calling query_complete(). + * If the query should fail - for example, due to authentication error - the result should be set to a + * non-zero error code ?TBD?. + * + * @param handle the handle from the WorkItem. + * @param statusCode if this is non zero it indicates that the query failed. + */ + public void queryComplete(final Handle handle, final int statusCode) + { + String index = handle.getCorrelationId(); + List<QmfAgentData> objects = _queryResults.get(index); + if (objects != null) + { + List<Map> results = new ArrayList<Map>(objects.size()); + for (QmfAgentData object : objects) + { + results.add(object.mapEncode()); + } + + // Send the response back to the Console + queryResponse(handle, results, "_data"); + _queryResults.remove(index); + } + } + + /** + * This has actually been implemented in the base Agent class as it's useful there too. + * + * If the subscription request is successful, the Agent application must provide a unique subscriptionId. + * If replying to a sucessful subscription refresh, the original subscriptionId must be supplied. + * If the subscription or refresh fails, the subscriptionId should be set to null and error may be set to + * an application-specific QmfData instance that describes the error. + * Should a refresh request fail, the consoleHandle may be set to null if unknown. + * + * @param handle the handle from the WorkItem + * @param consoleHandle the console reply handle + * @param subscriptionId a unique handle for the subscription supplied by the Agent + * @param lifetime should be set to the duration of the subscription in seconds. + * @param publishInterval should be set to the time interval in seconds between successive publications + * on this subscription. + * @param error an application-specific QmfData instance that describes the error. + */ + //public void subscriptionResponse(Handle handle, Handle consoleHandle, String subscriptionId, long lifetime, long + // publishInterval, QmfData error) + + /** + * Send a list of updated subscribed data to the Console. + * + * Note that the base Agent class contains a sendSubscriptionIndicate() for data that is already mapEncoded() + * To be honest AgentExternal Agents that happen to implement the SubscriptionProxy interface which reuses the + * Internal Agent Subscription code will almost certainly call the base class sendSubscriptionIndicate() as the + * SubscriptionProxy sendSubscriptionIndicate() has the same signature. + * + * @param handle the console reply handle. + * @param objects a list of subscribed data in QmfAgentData encoded form. + */ + public void subscriptionIndicate(final Handle handle, final List<QmfAgentData> objects) + { + List<Map> results = new ArrayList<Map>(objects.size()); + for (QmfAgentData object : objects) + { + results.add(object.mapEncode()); + } + sendSubscriptionIndicate(handle, results); + } + + /** + * Acknowledge a Subscription Cancel WorkItem. + * + * @param handle the handle from the WorkItem. + * @param consoleHandle the console reply handle. + */ + public void subscriptionCancel(final Handle handle, final String consoleHandle) + { + // Null implementation, there isn't really much that needs to be done after cancelling a Subscription + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallParams.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallParams.java new file mode 100644 index 0000000000..1f69709225 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallParams.java @@ -0,0 +1,123 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfData; + +/** + * This class contains the values passed by the Agent class to the Agent implementation within the MethodCallWorkItem. + * <p> + * It contains information that is needed by the Agent implementation to identify the object on which the method. + * is to be called via getObjectId(), the name of the method to be invoked via getName() and the arguments of the + * method via getArgs(). + * + * @author Fraser Adams + */ + +public final class MethodCallParams +{ + private final String _name; + private final ObjectId _objectId; + private final QmfData _args; + private final String _userId; + + /** + * Construct MethodCallParams. + * + * @param m the Map used to populate the WorkItem's parameters. + */ + public MethodCallParams(final Map m) + { + _name = QmfData.getString(m.get("_method_name")); + + Map oid = (Map)m.get("_object_id"); + _objectId = (oid == null) ? null : new ObjectId(oid); + + Map args = (Map)m.get("_arguments"); + if (args == null) + { + _args = null; + } + else + { + _args = new QmfData(args); + _args.setSubtypes((Map)m.get("_subtypes")); + } + _userId = QmfData.getString(m.get("_user_id")); + } + + /** + * Return a string containing the name of the method call. + * @return a string containing the name of the method call. + */ + public String getName() + { + return _name; + } + + /** + * Return the identifier for the object on which this method needs to be invoked. + * @return the identifier for the object on which this method needs to be invoked. + * <p> + * Returns null iff there is no associated object (a method call against the agent itself). + */ + public ObjectId getObjectId() + { + return _objectId; + } + + /** + * Return a map of input arguments for the method. + * @return a map of input arguments for the method. + * <p> + * Arguments are in "name"=<value> pairs. Returns null if no arguments are supplied. + */ + public QmfData getArgs() + { + return _args; + } + + /** + * Return authenticated user id of caller if present, else null. + * @return authenticated user id of caller if present, else null. + */ + public String getUserId() + { + return _userId; + } + + /** + * Helper/debug method to list the properties and their type. + */ + public void listValues() + { + System.out.println("MethodCallParams:"); + System.out.println("name: " + _name); + System.out.println("objectId: " + _objectId); + System.out.println("args: " + _args); + System.out.println("userId: " + _userId); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallWorkItem.java new file mode 100644 index 0000000000..cde10902e6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/MethodCallWorkItem.java @@ -0,0 +1,64 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * METHOD_CALL: The METHOD_CALL WorkItem describes a method call that must be serviced by the application on + * behalf of this Agent. + * + * The getParams() method of a METHOD_CALL WorkItem will return an instance of the MethodCallParams class. + * + * Use MethodCallWorkItem to enable neater access + * </pre> + * @author Fraser Adams + */ + +public final class MethodCallWorkItem extends WorkItem +{ + /** + * Construct a MethodCallWorkItem. Convenience constructor not in API + * + * @param handle the reply handle. + * @param params the MethodCallParams used to populate the WorkItem's param. + */ + public MethodCallWorkItem(final Handle handle, final MethodCallParams params) + { + super(WorkItemType.METHOD_CALL, handle, params); + } + + /** + * Return the MethodCallParams stored in the params Map. + * @return the MethodCallParams stored in the params Map. + */ + public MethodCallParams getMethodCallParams() + { + return (MethodCallParams)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QmfAgentData.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QmfAgentData.java new file mode 100644 index 0000000000..974c5746e2 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QmfAgentData.java @@ -0,0 +1,364 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +// Misc Imports +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfManaged; +import org.apache.qpid.qmf2.common.SchemaObjectClass; + +/** + * The Agent manages the data it represents by the QmfAgentData class - a derivative of the QmfData class. + * <p> + * The Agent is responsible for managing the values of the properties within the object, as well as servicing + * the object's method calls. Unlike the Console, the Agent has full control of the state of the object. + * <p> + * In most cases, for efficiency, it is expected that Agents would <i>actually</i> manage objects that are subclasses of + * QmfAgentData and maintain subclass specific properties as primitives, only actually explicitly setting the + * underlying Map properties via setValue() etc. when the object needs to be "serialised". This would most + * obviously be done by extending the mapEncode() method (noting that it's important to call QmfAgentData's mapEncode() + * first via super.mapEncode(); as this will set the state of the underlying QmfData). + * <p> + * This class provides a number of methods aren't in the QMF2 API per se, but they are used to manage the association + * between a managed object and any subscriptions that might be interested in it. + * <p> + * The diagram below shows the relationship between the Subscription and QmfAgentData. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * <p> + * In particular the QmfAgentData maintains references to active subscriptions to allow agents to asynchronously + * push data to subscribing Consoles immediately that data becomes available. + * <p> + * The update() method indicates that the object's state has changed and the publish() method <b>immediately</b> sends + * the new state to any subscription. + * <p> + * The original intention was to "auto update" by calling these from the setValue() method. Upon reflection this + * seems a bad idea, as in many cases there may be several properties that an Agent may wish to change which would + * lead to unnecessary calls to currentTimeMillis(), but also as theSubscription update is run via a TimerTask it is + * possible that an update indication could get sent part way through setting an object's overall state. + * Similarly calling the publish() method directly from setValue() would force an update indication on partial changes + * of state, which is generally not the desired behaviour. + * @author Fraser Adams + */ +public class QmfAgentData extends QmfManaged implements Comparable<QmfAgentData> +{ + private long _updateTimestamp; + private long _createTimestamp; + private long _deleteTimestamp; + private String _compareKey = null; + + /** + * This Map is used to look up Subscriptions that are interested in this data by SubscriptionId + */ + private Map<String, Subscription> _subscriptions = new ConcurrentHashMap<String, Subscription>(); + + /** + * Construct a QmfAgentData object of the type described by the given SchemaObjectClass. + * + * @param schema the schema describing the type of this QmfAgentData object. + */ + public QmfAgentData(final SchemaObjectClass schema) + { + long currentTime = System.currentTimeMillis()*1000000l; + _updateTimestamp = currentTime; + _createTimestamp = currentTime; + _deleteTimestamp = 0; + setSchemaClassId(schema.getClassId()); + } + + /** + * Return the creation timestamp. + * @return the creation timestamp. Timestamps are recorded in nanoseconds since the epoch + */ + public final long getCreateTime() + { + return _createTimestamp; + } + + /** + * Return the update timestamp. + * @return the update timestamp. Timestamps are recorded in nanoseconds since the epoch + */ + public final long getUpdateTime() + { + return _updateTimestamp; + } + + /** + * Return the deletion timestamp. + * @return the deletion timestamp, or zero if not deleted. Timestamps are recorded in nanoseconds since the epoch + */ + public final long getDeleteTime() + { + return _deleteTimestamp; + } + + /** + * Return true if deletion timestamp not zero. + * @return true if deletion timestamp not zero. + */ + public final boolean isDeleted() + { + return getDeleteTime() != 0; + } + + /** + * Mark the object as deleted by setting the deletion timestamp to the current time. + * <p> + * This method alse publishes the deleted object to any listening Subscription then removes references to the + * Subscription. + * <p> + * When this method returns the object should be ready for reaping. + */ + public final void destroy() + { + _deleteTimestamp = System.currentTimeMillis()*1000000l; + _updateTimestamp = System.currentTimeMillis()*1000000l; + publish(); + _subscriptions.clear(); + } + + /** + * Add the delta to the property. + * + * @param name the name of the property being modified. + * @param delta the value being added to the property. + */ + public final synchronized void incValue(final String name, final long delta) + { + long value = getLongValue(name); + value += delta; + setValue(name, value); + } + + /** + * Add the delta to the property. + * + * @param name the name of the property being modified. + * @param delta the value being added to the property. + */ + public final synchronized void incValue(final String name, final double delta) + { + double value = getDoubleValue(name); + value += delta; + setValue(name, value); + } + + /** + * Subtract the delta from the property. + * + * @param name the name of the property being modified. + * @param delta the value being subtracted from the property. + */ + public final synchronized void decValue(final String name, final long delta) + { + long value = getLongValue(name); + value -= delta; + setValue(name, value); + } + + /** + * Subtract the delta from the property. + * + * @param name the name of the property being modified. + * @param delta the value being subtracted from the property. + */ + public final synchronized void decValue(final String name, final double delta) + { + double value = getDoubleValue(name); + value -= delta; + setValue(name, value); + } + + // The following methods aren't in the QMF2 API per se, but they are used to manage the association between + // a managed object and any subscriptions that might be interested in it. + + /** + * Return the Subscription with the specified ID. + * @return the Subscription with the specified ID. + */ + public final Subscription getSubscription(final String subscriptionId) + { + return _subscriptions.get(subscriptionId); + } + + /** + * Add a new Subscription reference. + * @param subscriptionId the ID of the Subscription being added. + * @param subscription the Subscription being added. + */ + public final void addSubscription(final String subscriptionId, final Subscription subscription) + { + _subscriptions.put(subscriptionId, subscription); + } + + /** + * Remove a Subscription reference. + * @param subscriptionId the ID of the Subscription being removed. + */ + public final void removeSubscription(final String subscriptionId) + { + _subscriptions.remove(subscriptionId); + } + + + /** + * Set the _updateTimestamp to indicate (particularly to subscriptions) that the managed object has changed. + * <p> + * The update() method indicates that the object's state has changed and the publish() method <b>immediately</b> sends + * the new state to any subscription. + * <p> + * The original intention was to "auto update" by calling these from the setValue() method. Upon reflection this + * seems a bad idea, as in many cases there may be several properties that an Agent may wish to change which would + * lead to unnecessary calls to currentTimeMillis(), but also as the Subscription update is run via a TimerTask it + * is possible that an update indication could get sent part way through setting an object's overall state. + * Similarly calling the publish() method directly from setValue() would force an update indication on partial + * changes of state, which is generally not the desired behaviour. + */ + public final void update() + { + _updateTimestamp = System.currentTimeMillis()*1000000l; + } + + /** + * Iterate through any Subscriptions associated with this Object and force them to republish the Object's new state. + * <p> + * The update() method indicates that the object's state has changed and the publish() method <b>immediately</b> sends + * the new state to any subscription. + * <p> + * The original intention was to "auto update" by calling these from the setValue() method. Upon reflection this + * seems a bad idea, as in many cases there may be several properties that an Agent may wish to change which would + * lead to unnecessary calls to currentTimeMillis(), but also as the Subscription update is run via a TimerTask it + * is possible that an update indication could get sent part way through setting an object's overall state. + * Similarly calling the publish() method directly from setValue() would force an update indication on partial + * changes of state, which is generally not the desired behaviour. + */ + public final void publish() + { + update(); + if (getObjectId() == null) + { // If ObjectId is null the Object isn't yet Managed to we can't publish + return; + } + + List<Map> results = new ArrayList<Map>(); + results.add(mapEncode()); + for (Map.Entry<String, Subscription> entry : _subscriptions.entrySet()) + { + Subscription subscription = entry.getValue(); + subscription.publish(results); + } + } + + /** + * Return the underlying map. + * <p> + * In most cases, for efficiency, it is expected that Agents would <i>actually</i> manage objects that are + * subclasses of QmfAgentData and maintain subclass specific properties as primitives, only actually explicitly + * setting the underlying Map properties via setValue() etc. when the object needs to be "serialised". This would + * most obviously be done by extending the mapEncode() method (noting that it's important to call QmfAgentData's + * mapEncode() first via super.mapEncode(); as this will set the state of the underlying QmfData). + * + * @return the underlying map. + */ + @Override + public Map<String, Object> mapEncode() + { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("_values", super.mapEncode()); + if (_subtypes != null) + { + map.put("_subtypes", _subtypes); + } + map.put("_schema_id", getSchemaClassId().mapEncode()); + map.put("_object_id", getObjectId().mapEncode()); + map.put("_update_ts", _updateTimestamp); + map.put("_create_ts", _createTimestamp); + map.put("_delete_ts", _deleteTimestamp); + return map; + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + System.out.println("QmfAgentData:"); + System.out.println("create timestamp: " + new Date(getCreateTime()/1000000l)); + System.out.println("update timestamp: " + new Date(getUpdateTime()/1000000l)); + System.out.println("delete timestamp: " + new Date(getDeleteTime()/1000000l)); + } + + // The following methods allow instances of QmfAgentData to be compared with each other and sorted. + // N.B. This behaviour is not part of the specified QmfAgentData, but it's quite useful for some Agents. + + /** + * Set the key String to be used for comparing two QmfAgentData instances. This is primarily used by the Agent + * to allow it to order Query results (e.g. for getObjects()). + * @param compareKey the String that we wish to use as a compare key. + */ + public void setCompareKey(String compareKey) + { + _compareKey = compareKey; + } + + /** + * If a compare key has been set then the QmfAgentData is sortable. + * @return true if a compare key has been set and the QmfAgentData is sortable otherwise return false. + */ + public boolean isSortable() + { + return _compareKey != null; + } + + /** + * Compare the compare key of this QmfAgentData with the specified other QmfAgentData. + * Compares the compare keys (which are Strings) lexicographically. The comparison is based on the Unicode + * value of each character in the strings. + * @param rhs the String to be compared. + * @return the value 0 if the argument string is equal to this string; a value less than 0 if this string is + * lexicographically less than the string argument; and a value greater than 0 if this string is lexicographically + * greater than the string argument. + */ + public int compareTo(QmfAgentData rhs) + { + if (_compareKey == null) + { + return 0; + } + else + { + return this._compareKey.compareTo(rhs._compareKey); + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QueryWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QueryWorkItem.java new file mode 100644 index 0000000000..d617307c20 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/QueryWorkItem.java @@ -0,0 +1,85 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.QmfQuery; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * QUERY: The QUERY WorkItem describes a query that the application must service. The application should call the + * queryResponse() method for each object that satisfies the query. When complete, the application must call the + * queryComplete() method. If a failure occurs, the application should indicate the error to the agent by calling + * the query_complete() method with a description of the error. + * + * The getParams() method of a QUERY WorkItem will return an instance of the QmfQuery class. + * + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's queryResponse() + * and queryComplete() methods. + * </pre> + * Note that the API is a bit sketchy on the description of the QUERY WorkItem and whereas most WorkItems seem to return + * defined classes for their getParams() it's not so obvious here. There's an implication that getParams() just returns + * QmfQuery, but it's not clear where the uset_id bit fits. + * <p> + * As the API doesn't define a "QueryParams" class I've not included one, but I've added a getUserId() method to + * QueryWorkItem, this is a bit inconsistent with the approach for the other WorkItems though. + * + * @author Fraser Adams + */ + +public final class QueryWorkItem extends WorkItem +{ + /** + * Construct a QueryWorkItem. Convenience constructor not in API. + * + * @param handle the reply handle. + * @param params the QmfQuery used to populate the WorkItem's param. + */ + public QueryWorkItem(final Handle handle, final QmfQuery params) + { + super(WorkItemType.QUERY, handle, params); + } + + /** + * Return the QmfQuery stored in the params Map. + * @return the QmfQuery stored in the params Map. + */ + public QmfQuery getQmfQuery() + { + return (QmfQuery)getParams(); + } + + /** + * Return authenticated user id of caller if present, else null. + * @return authenticated user id of caller if present, else null. + */ + public String getUserId() + { + Map map = getQmfQuery().mapEncode(); + return (String)map.get("_user_id"); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeParams.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeParams.java new file mode 100644 index 0000000000..475b378b75 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeParams.java @@ -0,0 +1,78 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfData; + +/** + * Holds the information contained in a resubscription request made by a Console to an Agent + * + * @author Fraser Adams + */ +public final class ResubscribeParams extends QmfData +{ + /** + * Construct ResubscribeParams. + * + * @param m the Map used to populate the ResubscribeParams state. + */ + public ResubscribeParams(final Map m) + { + super(m); + } + + /** + * Return a SubscriptionId object. + * @return a SubscriptionId object. + */ + public String getSubscriptionId() + { + if (hasValue("_subscription_id")) + { + return getStringValue("_subscription_id"); + } + return null; + } + + /** + * Return the requested lifetime for the subscription. + * @return the requested lifetime for the subscription. Zero if the previous interval should be used. + */ + public long getLifetime() + { + return getLongValue("_duration"); + } + + /** + * Return authenticated user id of caller if present, else null. + * @return authenticated user id of caller if present, else null. + */ + public String getUserId() + { + return getStringValue("_user_id"); + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeRequestWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeRequestWorkItem.java new file mode 100644 index 0000000000..2ec9ed2c9c --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/ResubscribeRequestWorkItem.java @@ -0,0 +1,66 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * RESUBSCRIBE_REQUEST: The RESUBSCRIBE_REQUEST is sent by a Console to renew an existing subscription. The Console may + * request a new duration for the subscription, otherwise the previous lifetime interval is repeated. + * + * The getParams() method of a RESUBSCRIBE_REQUEST WorkItem will return an instance of the + * ResubscribeParams class. + * + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's + * subscriptionResponse() method. + * </pre> + * @author Fraser Adams + */ + +public final class ResubscribeRequestWorkItem extends WorkItem +{ + /** + * Construct a ResubscribeRequestWorkItem. Convenience constructor not in API. + * + * @param handle the reply handle. + * @param params the ResubscribeParams used to populate the WorkItem's param. + */ + public ResubscribeRequestWorkItem(final Handle handle, final ResubscribeParams params) + { + super(WorkItemType.RESUBSCRIBE_REQUEST, handle, params); + } + + /** + * Return the ResubscribeParams stored in the params Map. + * @return the ResubscribeParams stored in the params Map. + */ + public ResubscribeParams getResubscribeParams() + { + return (ResubscribeParams)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribableAgent.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribableAgent.java new file mode 100644 index 0000000000..619d00df7b --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribableAgent.java @@ -0,0 +1,70 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +// Misc Imports +import java.util.List; +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.QmfQuery; + +/** + * This interface provides a number of methods that are called by a Subscription in order to interact with an + * Agent or an Agent's managed data. + * <p> + * The purpose of this interface is primarily about removing a circular dependency between Subscription and Agent + * so the Subscription doesn't invoke these methods on an Agent instance, rather it invokes them on a + * SubscribeableAgent instance. + * <p> + * The following diagram illustrates the interactions between the Agent, Subscription and SubscribableAgent. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * + * @author Fraser Adams + */ +public interface SubscribableAgent +{ + /** + * Send a list of updated subscribed data to the Console. + * + * @param handle the console reply handle + * @param results a list of subscribed data in Map encoded form + */ + public void sendSubscriptionIndicate(Handle handle, List<Map> results); + + /** + * This method evaluates a QmfQuery over the Agent's data on behalf of a Subscription + * + * @param query the QmfQuery that the Subscription wants to be evaluated over the Agent's data + * @return a List of QmfAgentData objects that match the specified QmfQuery + */ + public List<QmfAgentData> evaluateQuery(QmfQuery query); + + /** + * This method is called by the Subscription to tell the SubscriberProxy that the Subscription has been cancelled. + * + * @param subscription the Subscription that has been cancelled and is requesting removal. + */ + public void removeSubscription(Subscription subscription); +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribeRequestWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribeRequestWorkItem.java new file mode 100644 index 0000000000..a9c4816aba --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscribeRequestWorkItem.java @@ -0,0 +1,69 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * SUBSCRIBE_REQUEST: The SUBSCRIBE_REQUEST WorkItem provides a query that the agent application must periodically + * publish until the subscription is cancelled or expires. On receipt of this WorkItem, the + * application should call the Agent subscriptionResponse() method to acknowledge the request. + * On each publish interval, the application should call Agent subscriptionIndicate(), passing a + * list of the objects that satisfy the query. The subscription remains in effect until an + * UNSUBSCRIBE_REQUEST WorkItem for the subscription is received, or the subscription expires. + * + * The getParams() method of a QUERY WorkItem will return an instance of the SubscriptionParams class. + * + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's + * subscriptionResponse() method. + * </pre> + * @author Fraser Adams + */ + +public final class SubscribeRequestWorkItem extends WorkItem +{ + /** + * Construct a SubscribeRequestWorkItem. Convenience constructor not in API + * + * @param handle the reply handle + * @param params the SubscriptionParams used to populate the WorkItem's param + */ + public SubscribeRequestWorkItem(final Handle handle, final SubscriptionParams params) + { + super(WorkItemType.SUBSCRIBE_REQUEST, handle, params); + } + + /** + * Return the SubscriptionParams stored in the params Map. + * @return the SubscriptionParams stored in the params Map. + */ + public SubscriptionParams getSubscriptionParams() + { + return (SubscriptionParams)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Subscription.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Subscription.java new file mode 100644 index 0000000000..2f49702c7c --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/Subscription.java @@ -0,0 +1,280 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +// Simple Logging Facade 4 Java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.UUID; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfQuery; +import org.apache.qpid.qmf2.common.QmfQueryTarget; + +/** + * This TimerTask represents a running Subscription on the Agent. + * <p> + * The main reason we have Subscriptions as TimerTasks is to enable proper cleanup of the references stored in + * the _subscriptions Map when the Subscription expires. The timer also causes QmfAgenData that have been updated + * since the last interval to be published. + * <p> + * The following diagram illustrates the Subscription relationships with the Agent and QmfAgentData. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * @author Fraser Adams + */ +public final class Subscription extends TimerTask +{ + private static final Logger _log = LoggerFactory.getLogger(Subscription.class); + + // Duration is the time (in seconds) the Subscription is active before it automatically expires unless refreshed + private static final int DEFAULT_DURATION = 300; + private static final int MAX_DURATION = 3600; + private static final int MIN_DURATION = 10; + + // Interval is the period (in milliseconds) between subscription ubdates. + private static final int DEFAULT_INTERVAL = 30000; + private static final int MIN_INTERVAL = 1000; + + private SubscribableAgent _agent; + private long _startTime = System.currentTimeMillis(); + private long _lastUpdate = _startTime*1000000l; + private String _subscriptionId; + private Handle _consoleHandle; + private QmfQuery _query; + private long _duration = 0; + private long _interval = 0; + + /** + * Tells the SubscribableAgent to send the results to the Console via a subscription indicate message. + * + * @param results the list of mapEncoded QmfAgentData that currently match the query associated with this + * Subscription. + */ + protected void publish(List<Map> results) + { + _agent.sendSubscriptionIndicate(_consoleHandle, results); + _lastUpdate = System.currentTimeMillis()*1000000l; + } + + /** + * Construct a new Subscription. + * @param agent the SubscribableAgent to which this Subscription is associated. + * @param params the SubscriptionParams object that contains the information needed to create a Subscription. + */ + public Subscription(SubscribableAgent agent, SubscriptionParams params) throws QmfException + { + _agent = agent; + _subscriptionId = UUID.randomUUID().toString(); + _consoleHandle = params.getConsoleHandle(); + _query = params.getQuery(); + setDuration(params.getLifetime()); + setInterval(params.getPublishInterval()); + + _log.debug("Creating Subscription {}, duration = {}, interval = {}", new Object[] {_subscriptionId, _duration, _interval}); + } + + /** + * This method gets called periodically by the Timer scheduling this TimerTask. + * <p> + * First a check is made to see if the Subscription has expired, if it has then it is cancelled. + * <p> + * If the Subscription isn't cancelled the Query gets evaluated against all registered objects and any that match + * which are new to the Subscription or have changed since the last update get published. + */ + public void run() + { + long elapsed = (long)Math.round((System.currentTimeMillis() - _startTime)/1000.0f); + if (elapsed >= _duration) + { + _log.debug("Subscription {} has expired, removing", _subscriptionId); + // The Subscription has expired so cancel it + cancel(); + } + else + { + List<QmfAgentData> objects = _agent.evaluateQuery(_query); + List<Map> results = new ArrayList<Map>(objects.size()); + for (QmfAgentData object : objects) + { + if (object.getSubscription(_subscriptionId) == null) + { + // The object is new to this Subscription so publish it + object.addSubscription(_subscriptionId, this); + results.add(object.mapEncode()); + } + else + { + // If the object has had update() called since last Subscription update publish it. + // Note that in many cases an Agent might call publish() on a managed object rather than + // update() which immediately forces a data indication to be sent to the subscriber on + // the Console. + if (object.getUpdateTime() > _lastUpdate) + { + results.add(object.mapEncode()); + } + } + } + + if (results.size() > 0) + { + publish(results); + } + } + } + + /** + * Refresh the subscription by zeroing its elapsed time. + * + * @param resubscribeParams the ResubscribeParams passed by the Console potentially containing new duration + * information. + */ + public void refresh(ResubscribeParams resubscribeParams) + { + _log.debug("Refreshing Subscription {}", _subscriptionId); + _startTime = System.currentTimeMillis(); + setDuration(resubscribeParams.getLifetime()); + } + + /** + * Cancel the Subscription, tidying references up and cancelling the TimerTask. + */ + @Override + public boolean cancel() + { + _log.debug("Cancelling Subscription {}", _subscriptionId); + // This Subscription is about to be deleted, remove it from any Objects that may be referencing it. + List<QmfAgentData> objects = _agent.evaluateQuery(_query); + for (QmfAgentData object : objects) + { + object.removeSubscription(_subscriptionId); + } + + _agent.removeSubscription(this); + return super.cancel(); // Cancel the TimerTask + } + + /** + * Return the SubscriptionId of this subscription. + * @return the SubscriptionId of this subscription. + */ + public String getSubscriptionId() + { + return _subscriptionId; + } + + /** + * Return the consoleHandle of this subscription. + * @return the consoleHandle of this subscription. + */ + public Handle getConsoleHandle() + { + return _consoleHandle; + } + + /** + * Set the Subscription lifetime in seconds. If the value passed to this method is zero the duration gets + * set to the Agent's DEFAULT_DURATION is the duration has not already been set, if the duration has already + * been set passing in a zero value has no effect on the duration. + * If the value passed is non-zero the duration passed gets restricted between the Agent's MIN_DURATION + * and MAX_DURATION. + * + * @param duration the new Subscription lifetime in seconds. + */ + public void setDuration(long duration) + { + if (duration == 0) + { + if (_duration == 0) + { + _duration = DEFAULT_DURATION; + } + return; + } + else + { + if (duration > MAX_DURATION) + { + duration = MAX_DURATION; + } + else if (duration < MIN_DURATION) + { + duration = MIN_DURATION; + } + } + _duration = duration; + } + + /** + * Return the current Subscription lifetime value in seconds. + * @return the current Subscription lifetime value in seconds. + */ + public long getDuration() + { + return _duration; + } + + /** + * Set the Subscription refresh interval in seconds. If the value passed to this method is zero the interval gets + * set to the Agent's DEFAULT_INTERVAL otherwise the interval passed gets restricted to be {@literal >= } the Agent's + * MIN_INTERVAL. + * + * @param interval the time (in milliseconds) between periodic updates of data in this Subscription. + */ + public void setInterval(long interval) + { + if (interval == 0) + { + interval = DEFAULT_INTERVAL; + } + else if (interval < MIN_INTERVAL) + { + interval = MIN_INTERVAL; + } + _interval = interval; + } + + /** + * Return The time (in milliseconds) between periodic updates of data in this Subscription. + * @return The time (in milliseconds) between periodic updates of data in this Subscription. + */ + public long getInterval() + { + return _interval; + } + + /** + * Return The Subscription's QmfQuery. + * @return The Subscription's QmfQuery. + */ + public QmfQuery getQuery() + { + return _query; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscriptionParams.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscriptionParams.java new file mode 100644 index 0000000000..1cc2a9a5d1 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/SubscriptionParams.java @@ -0,0 +1,102 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfQuery; + +/** + * Holds the information contained in a subscription request made by a Console to an Agent + * + * @author Fraser Adams + */ +public final class SubscriptionParams extends QmfData +{ + private final Handle _consoleHandle; + + /** + * Construct SubscriptionParams. + * + * @param handle the handle that the console uses to identify this subscription. + * @param m the Map used to populate the SubscriptionParams state. + */ + public SubscriptionParams(final Handle handle, final Map m) + { + super(m); + _consoleHandle = handle; + } + + /** + * Return the handle that the console uses to identify this subscription. + * @return the handle that the console uses to identify this subscription. + * <p> + * This handle must be passed along with every published update from the Agent. + */ + public Handle getConsoleHandle() + { + return _consoleHandle; + } + + /** + * Return the QmfQuery object associated with the SubscriptionParams. + * @return the QmfQuery object associated with the SubscriptionParams. + */ + public QmfQuery getQuery() throws QmfException + { + return new QmfQuery((Map)getValue("_query")); + } + + /** + * Return the requested time interval in seconds for updates. + * @return the requested time interval in seconds for updates. Zero if the Agent's default interval should be used. + */ + public long getPublishInterval() + { + return getLongValue("_interval"); + } + + /** + * Return the requested lifetime for the subscription. + * @return the requested lifetime for the subscription. Zero if the Agent's default subscription lifetime + * should be used. + */ + public long getLifetime() + { + return getLongValue("_duration"); + } + + /** + * Return authenticated user id of caller if present, else null. + * @return authenticated user id of caller if present, else null. + */ + public String getUserId() + { + return getStringValue("_user_id"); + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/UnsubscribeRequestWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/UnsubscribeRequestWorkItem.java new file mode 100644 index 0000000000..45f316b1b4 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/UnsubscribeRequestWorkItem.java @@ -0,0 +1,65 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.agent; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * UNSUBSCRIBE_REQUEST: The UNSUBSCRIBE_REQUEST is sent by a Console to terminate an existing subscription. The Agent + * application should terminate the given subscription if it exists, and cancel sending any further + * updates against it. + * + * The getParams() method of a UNSUBSCRIBE_REQUEST WorkItem will return a String holding the + * subscriptionId + * + * The getHandle() method returns null. + * </pre> + * @author Fraser Adams + */ + +public final class UnsubscribeRequestWorkItem extends WorkItem +{ + /** + * Construct an UnsubscribeRequestWorkItem. Convenience constructor not in API + * + * @param params the ResubscribeParams used to populate the WorkItem's param + */ + public UnsubscribeRequestWorkItem(final String params) + { + super(WorkItemType.UNSUBSCRIBE_REQUEST, null, params); + } + + /** + * Return the subscriptionId String stored in the params Map. + * @return the subscriptionId String stored in the params Map. + */ + public String getSubscriptionId() + { + return (String)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfData.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfData.png Binary files differnew file mode 100644 index 0000000000..2665803e39 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfData.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfEventListenerModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfEventListenerModel.png Binary files differnew file mode 100644 index 0000000000..26a5f71b56 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfEventListenerModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfQuery.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfQuery.png Binary files differnew file mode 100644 index 0000000000..9e471a08c0 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/QmfQuery.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Schema.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Schema.png Binary files differnew file mode 100644 index 0000000000..b0277f4fc5 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Schema.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Subscriptions.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Subscriptions.png Binary files differnew file mode 100644 index 0000000000..d43370e4ef --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/Subscriptions.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/WorkQueueEventModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/WorkQueueEventModel.png Binary files differnew file mode 100644 index 0000000000..fc2a722985 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/agent/doc-files/WorkQueueEventModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/AMQPMessage.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/AMQPMessage.java new file mode 100644 index 0000000000..94b539c0b3 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/AMQPMessage.java @@ -0,0 +1,309 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// JMS Imports +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.MessageFormatException; +import javax.jms.Session; + +// Misc Imports +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +// Need the following to decode and encode amqp/list messages +import java.nio.ByteBuffer; +import org.apache.qpid.transport.codec.BBDecoder; +import org.apache.qpid.transport.codec.BBEncoder; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfData; + +/** + * Provides static helper methods for encoding and decoding "amqp/list" and "amqp/map" ContentTypes. + *<p> + * Unfortunately the encoding of amqp/map and amqp/list messages is not as useful as it might be in the + * Qpid JMS runtime. amqp/list messages don't <i>actually</i> have a useful encoding in Qpid JMS, so we have to + * fake it in this class by encoding/decoding java.util.List objects into a JMS BytesMessage and setting + * the ContentType to "amqp/list". + *<p> + * Whilst amqp/map messages are encoded as JMS MapMessage this isn't necessarily the most useful format as + * MapMessage does not conform to the java.util.Map interface. As QMF methods returning lists return lists + * of java.util.Map there's a bit of an inconsistency of type that getMap() resolves. + * + * @author Fraser Adams + */ +public final class AMQPMessage +{ + /** + * Make constructor private at this class provides a set of static helper methods and doesn't need instantiated. + */ + private AMQPMessage() + { + } + + /** + * This method exposes the AMQP Content-Type from a JMS Message. This has been put into an accessor + * method because some evil hackery has to take place to set the Content-Type as no pure JMS API + * property currently gets mapped to Content-Type, so we have to cast to AbstractJMSMessage. + * + * @param message a JMS Message. + * @return the AMQP Content-Type e.g. amqp/list, amqp/map etc. + */ + public static String getContentType(final Message message) + { + return ((org.apache.qpid.client.message.AbstractJMSMessage)message).getContentType(); + } + + /** + * This method sets the AMQP Content-Type on a JMS Message. This has been put into a mutator + * method because some evil hackery has to take place to set the Content-Type as no pure JMS API + * property currently gets mapped to Content-Type, so we have to cast to AbstractJMSMessage. + * + * @param message a JMS Message. + * @param contentType the AMQP Content-Type that we'd like to set, e.g. amqp/list, amqp/map etc. + */ + public static void setContentType(final Message message, String contentType) + { + ((org.apache.qpid.client.message.AbstractJMSMessage)message).setContentType(contentType); + } + + /** + * Provides an abstracted way for client code to explicitly check if a Message is an AMQP List. + * + * @param message a JMS Message. + * @return true if the Message is an AMQP List, otherwise returns false. + */ + public static boolean isAMQPList(final Message message) + { + if (getContentType(message).equals("amqp/list") || message instanceof BytesMessage) + { + // I *think* that the test for BytesMessage is actually redundant and that Content-Type would + // always have to be "amqp/list" for JMS to expose the Message as a BytesMessage but I've + // kept the test because pre Qpid 0.20 exposed lists as BytesMessage. + return true; + } + else + { + return false; + } + } + + /** + * Provides an abstracted way for client code to explicitly check if a Message is an AMQP Map. + * + * @param message a JMS Message. + * @return true if the Message is an AMQP Map, otherwise returns false. + */ + public static boolean isAMQPMap(final Message message) + { + if (getContentType(message).equals("amqp/map") || + (message instanceof MapMessage && !isAMQPList(message))) + { + return true; + } + else + { + return false; + } + } + + /** + * Builds a java.util.Map from a JMS MapMessage. + * This is really a helper method to make code more homogenous as QmfData objects are constructed from Maps + * but JMS returns MapMessages which don't share a common interface. This method enumerates MapMessage + * Properties and Objects and stores them in a java.util.Map. + * + * @param message a JMS Message + * @return a java.util.Map containing the the properties extracted from the Message. + * <p> + * Note that this method copies the Message properties <b>and</b> the properties from the MapMessage Map. + * <p> + * This method also attempts to populate "_user_id" using the JMSXUserID property, however that's not as + * easy as it sounds!! There's a bug in AMQMessageDelegate_0_10.getStringProperty() whereby if the property + * is "JMSXUserID" it returns "new String(_messageProps.getUserId());" however if the client uses anonymous + * authentication _messageProps.getUserId() returns null. In order to get around this this class unfortunately + * has to delve inside "org.apache.qpid.client.message.AbstractJMSMessage". + */ + public static Map<String, Object> getMap(final Message message) throws JMSException + { + if (message == null) + { + throw new MessageFormatException("Attempting to do AMQPMessage.getMap() on null Message"); + } + else if (message instanceof MapMessage) + { + Map<String, Object> object = new HashMap<String, Object>(); + MapMessage msg = (MapMessage)message; + for (Enumeration e = msg.getMapNames(); e.hasMoreElements();) + { + String key = (String)e.nextElement(); + object.put(key, msg.getObject(key)); + } + + if (object.size() == 0) + { // If there is no MapMessage content return an empty Map. + return object; + } + + // If there is MapMessage content include the Message properties in the returned Map. + for (Enumeration e = msg.getPropertyNames(); e.hasMoreElements();) + { + String prop = (String)e.nextElement(); + object.put(prop, QmfData.getString(msg.getObjectProperty(prop))); + } + + // Should be msg.getStringProperty("JMSXUserID"). See comments above for the reason behind this evil hack. + org.apache.qpid.client.message.AMQMessageDelegate_0_10 delegate = (org.apache.qpid.client.message.AMQMessageDelegate_0_10)(((org.apache.qpid.client.message.AbstractJMSMessage)msg).getDelegate()); + byte[] rawUserId = delegate.getMessageProperties().getUserId(); + if (rawUserId != null) + { + String userId = new String(rawUserId); + object.put("_user_id", userId); + } + + return object; + } + else + { + return null; + } + } + + /** + * JMS QMF returns amqp/list types as a BytesMessage this method decodes that into a java.util.List + * <p> + * Taken from Gordon Sim's initial JMS QMF Example using the BBDecoder + * <p> + * Trivia: This block of code from Gordon Sim is the seed that spawned the whole of this Java QMF2 API + * implementation - cheers Gordon. + * + * @param message amqp/list encoded JMS Message + * @return a java.util.List decoded from Message + */ + @SuppressWarnings("unchecked") + public static <T> List<T> getList(final Message message) throws JMSException + { + if (message == null) + { + throw new MessageFormatException("Attempting to do AMQPMessage.getList() on null Message"); + } + else if (message instanceof BytesMessage) + { + BytesMessage msg = (BytesMessage)message; + + //only handles responses up to 2^31-1 bytes long + byte[] data = new byte[(int) msg.getBodyLength()]; + msg.readBytes(data); + BBDecoder decoder = new BBDecoder(); + decoder.init(ByteBuffer.wrap(data)); + return (List<T>)decoder.readList(); + } + else if (message instanceof MapMessage) + { /* + * In Qpid version 0.20 instead of exposing amqp/list as a BytesMessage as above rather it is exposed + * as a MapMessage!!??? the Object Keys are the indices into the List. We create a java.util.List + * out of this by iterating through the getMapNames() Enumeration and copying the Objects into the List. + * This amount of copying doesn't feel healthy and we can't even work out the capacity for the List + * a priori, but I'm not sure of a better way at present. I can't say I much like how amqp/list or indeed + * amqp/map are currently encoded. I'd *much* prefer to see them exposed as JMS ObjectMessage. + */ + MapMessage msg = (MapMessage)message; + List resultList = new ArrayList(50); // Initial capacity of 50, can we better estimate this? + + for (Enumeration e = msg.getMapNames(); e.hasMoreElements();) + { + String key = (String)e.nextElement(); + resultList.add(msg.getObject(key)); + } + return resultList; + } + else + { + return null; + } + } + + /** + * Creates an amqp/list encoded Message out of a BytesMessage. + * <p> + * This is somewhat of a dirty hack that needs to be monitored as qpid versions change. + * <p> + * Unfortunately there's no "clean" way to encode or decode amqp/list messages via the JMS API. + * + * @param session used to create the JMS Message + * @return an amqp/list encoded JMS Message + */ + public static Message createListMessage(final Session session) throws JMSException + { + BytesMessage message = session.createBytesMessage(); + setContentType(message, "amqp/list"); + return message; + } + + /** + * Encodes a java.util.List on an amqp/list encoded BytesMessage. + * <p> + * This is somewhat of a dirty hack that needs to be monitored as qpid versions change. + * <p> + * This method uses the org.apache.qpid.transport.codec.BBEncoder writeList() method to encode + * a List into a ByteBuffer then writes the bytes from the buffer into a JMS BytesMessage. + * + * @param message amqp/list encoded JMS BytesMessage + * @param list to encode into JMS Message + */ + @SuppressWarnings("unchecked") + public static void setList(final Message message, final List list) throws JMSException + { + String type = getContentType(message); + if (!type.equals("amqp/list")) + { + throw new MessageFormatException("Can only do setList() on amqp/list encoded Message"); + } + + if (message == null) + { + throw new MessageFormatException("Attempting to do AMQPMessage.setList() on null Message"); + } + else if (message instanceof BytesMessage) + { + BBEncoder encoder = new BBEncoder(1024); + encoder.writeList(list); + ByteBuffer buf = encoder.segment(); + byte[] data = new byte[buf.limit()]; + buf.get(data); + ((BytesMessage)message).writeBytes(data); + } + else + { + throw new MessageFormatException("Attempting to do setList() on " + message.getClass().getCanonicalName()); + } + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BlockingNotifier.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BlockingNotifier.java new file mode 100644 index 0000000000..634a5e60b0 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BlockingNotifier.java @@ -0,0 +1,65 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * Implementation of the Notifier Interface that provides a waitForWorkItem() method which blocks until the + * Console has called the indication() method indicating that there are WorkItems available. + * <p> + * This class isn't part of the QMF2 API however it's almost certainly how most clients would choose to use the + * Notifier API so it seems useful to provide an implementation. + * + * @author Fraser Adams + */ +public final class BlockingNotifier implements Notifier +{ + private boolean _waiting = true; + + /** + * This method blocks until the indication() method has been called, this is generally called by the Console + * when new WorkItems are made available. + */ + public synchronized void waitForWorkItem() + { + while (_waiting) + { + try + { + wait(); + } + catch (InterruptedException ie) + { + continue; + } + } + _waiting = true; + } + + /** + * Called to indicate the availability of WorkItems. This method unblocks waitForWorkItem() + */ + public synchronized void indication() + { + _waiting = false; + notifyAll(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanEquals.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanEquals.java new file mode 100644 index 0000000000..4eba04cc03 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanEquals.java @@ -0,0 +1,79 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanEquals Expression + * + * @author Fraser Adams + */ +public final class BooleanEquals extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanEquals + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanEquals(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanEquals() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanEquals(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "equal to" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + return _operands[0].equals(_operands[1]); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExists.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExists.java new file mode 100644 index 0000000000..522748ebb6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExists.java @@ -0,0 +1,72 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanExists Expression + * + * @author Fraser Adams + */ +public final class BooleanExists extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanExists + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanExists(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanExists() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanExists(final List expr) throws QmfException + { + super(1, expr); + } + + /** + * Evaluate "exists" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + return _operands[0] != null; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExpression.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExpression.java new file mode 100644 index 0000000000..999dd6d222 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanExpression.java @@ -0,0 +1,221 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This class represents the base class for all Boolean Expressions created by expanding the Query predicate. + * + * @author Fraser Adams + */ +public abstract class BooleanExpression extends Expression +{ + private static Map<String, BooleanExpression> _factories = new HashMap<String, BooleanExpression>(); + protected String[] _operands; + private String[] _keys; + + /** + * Initialise the _factories Map, which contains the prototype instances of each concrete BooleanExpression + * keyed by the operator String. + */ + static + { + _factories.put("eq", new BooleanEquals()); + _factories.put("ne", new BooleanNotEquals()); + _factories.put("lt", new BooleanLessThan()); + _factories.put("le", new BooleanLessEqual()); + _factories.put("gt", new BooleanGreaterThan()); + _factories.put("ge", new BooleanGreaterEqual()); + _factories.put("re_match", new BooleanRegexMatch()); + _factories.put("exists", new BooleanExists()); + _factories.put("true", new BooleanTrue()); + _factories.put("false", new BooleanFalse()); + } + + /** + * Factory method to create concrete Expression instances based on the operator name extracted from the expression List. + * This method will create a BooleanExpression from an "eq", "ne", "lt" etc. operator using the prototype + * obtained from the _factories Map. + * + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public static Expression createExpression(final List expr) throws QmfException + { + Iterator iter = expr.listIterator(); + if (!iter.hasNext()) + { + throw new QmfException("Missing operator in predicate expression"); + } + + String op = (String)iter.next(); + BooleanExpression factory = _factories.get(op); + if (factory == null) + { + throw new QmfException("Unknown operator in predicate expression"); + } + + return factory.create(expr); + } + + /** + * Factory method to create a concrete instance of BooleanExpression + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public abstract Expression create(final List expr) throws QmfException; + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + protected BooleanExpression() + { + } + + /** + * Main Constructor, used to populate unevaluated operands. This loops through the input expression list. If the + * Object is a String is is treated as a key such that when the expression is evaluated the key will be used to + * obtain a propery from the QmfData object. If the Object is a sub-List it is checked to see if it's a quoted + * String, if it is the quoted String is stored as the operand. If it's neither of these the actual object from + * the expression List is used as the operand. + * + * @param operandCount the number of operands in this Expression, the value is generally passed by the subclass. + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + protected BooleanExpression(final int operandCount, final List expr) throws QmfException + { + Iterator iter = expr.listIterator(); + String op = (String)iter.next(); // We've already tested for hasNext() in the factory + + _operands = new String[operandCount]; + _keys = new String[operandCount]; + + for (int i = 0; i < operandCount; i++) + { + if (!iter.hasNext()) + { + throw new QmfException("Too few operands for operation: " + op); + } + + Object object = iter.next(); + _operands[i] = object.toString(); + + if (object instanceof String) + { + _keys[i] = _operands[i]; + _operands[i] = null; + } + else if (object instanceof List) + { + List sublist = (List)object; + Iterator subiter = sublist.listIterator(); + + if (subiter.hasNext() && ((String)subiter.next()).equals("quote")) + { + if (subiter.hasNext()) + { + _operands[i] = subiter.next().toString(); + if (subiter.hasNext()) + { + throw new QmfException("Extra tokens at end of 'quote'"); + } + } + } + else + { + throw new QmfException("Expected '[quote, <token>]'"); + } + } + } + + if (iter.hasNext()) + { + throw new QmfException("Too many operands for operation: " + op); + } + } + + /** + * Populates operands that are obtained at evaluation time. In other words operands that are obtained by using + * the key obtained from the static operand evaluation to look up an associated property from the QmfData object. + * @param data the object to extract the operand(s) from + */ + protected void populateOperands(final QmfData data) + { + for (int i = 0; i < _operands.length; i++) + { + String key = _keys[i]; + if (key != null) + { + String value = null; + + if (data.hasValue(key)) + { // If there's a property of the data object named key look it up as a String + value = data.getStringValue(key); + } + else + { // If there's no property of the data object named key look up its Described/Managed metadata + if (data instanceof QmfManaged) + { + QmfManaged managedData = (QmfManaged)data; + if (key.equals("_schema_id")) + { + value = managedData.getSchemaClassId().toString(); + } + else if (key.equals("_object_id")) + { + value = managedData.getObjectId().toString(); + } + else if (managedData.getSchemaClassId().hasValue(key)) + { // If it's not _schema_id or _object_id check the SchemaClassId properties e.g. + // _package_name, _class_name, _type or _hash + value = managedData.getSchemaClassId().getStringValue(key); + } + } + + if (value == null) + { // If a value still can't be found for the key check if it's available in the mapEncoded form + Map m = data.mapEncode(); + if (m.containsKey(key)) + { + value = QmfData.getString(m.get(key)); + } + } + } + + _operands[i] = value; + } +//System.out.println("key: " + key + ", operand = " + _operands[i]); + } + } + + /** + * Evaluate expression against a QmfData instance. + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public abstract boolean evaluate(final QmfData data); +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanFalse.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanFalse.java new file mode 100644 index 0000000000..00080dfb10 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanFalse.java @@ -0,0 +1,60 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanFalse Expression + * + * @author Fraser Adams + */ +public final class BooleanFalse extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanFalse + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanFalse(); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanFalse() + { + } + + /** + * Evaluate "false" expression against a QmfData instance. + * @param data the object to evaluate the expression against + * @return false. + */ + public boolean evaluate(final QmfData data) + { + return false; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterEqual.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterEqual.java new file mode 100644 index 0000000000..7aaeb8e687 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterEqual.java @@ -0,0 +1,89 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanGreaterEqual Expression + * + * @author Fraser Adams + */ +public final class BooleanGreaterEqual extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanGreaterEqual + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanGreaterEqual(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanGreaterEqual() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanGreaterEqual(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "greater than or equal to" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + try + { + double l = Double.parseDouble(_operands[0]); + double r = Double.parseDouble(_operands[1]); + return l >= r; + } + catch (NumberFormatException nfe) + { + // If converting to double fails try a lexicographic comparison + return _operands[0].compareTo(_operands[1]) >= 0; + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterThan.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterThan.java new file mode 100644 index 0000000000..2ac44f35ec --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanGreaterThan.java @@ -0,0 +1,89 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanGreaterThan Expression + * + * @author Fraser Adams + */ +public final class BooleanGreaterThan extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanGreaterThan + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanGreaterThan(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanGreaterThan() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanGreaterThan(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "greater than" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + try + { + double l = Double.parseDouble(_operands[0]); + double r = Double.parseDouble(_operands[1]); + return l > r; + } + catch (NumberFormatException nfe) + { + // If converting to double fails try a lexicographic comparison + return _operands[0].compareTo(_operands[1]) > 0; + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessEqual.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessEqual.java new file mode 100644 index 0000000000..2461f116d9 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessEqual.java @@ -0,0 +1,89 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanLessEqual Expression + * + * @author Fraser Adams + */ +public final class BooleanLessEqual extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanLessEqual + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanLessEqual(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanLessEqual() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanLessEqual(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "less than or equal to" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + try + { + double l = Double.parseDouble(_operands[0]); + double r = Double.parseDouble(_operands[1]); + return l <= r; + } + catch (NumberFormatException nfe) + { + // If converting to double fails try a lexicographic comparison + return _operands[0].compareTo(_operands[1]) <= 0; + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessThan.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessThan.java new file mode 100644 index 0000000000..1a65119e59 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanLessThan.java @@ -0,0 +1,89 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanLessThan Expression + * + * @author Fraser Adams + */ +public final class BooleanLessThan extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanLessThan + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanLessThan(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanLessThan() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanLessThan(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "less than" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + try + { + double l = Double.parseDouble(_operands[0]); + double r = Double.parseDouble(_operands[1]); + return l < r; + } + catch (NumberFormatException nfe) + { + // If converting to double fails try a lexicographic comparison + return _operands[0].compareTo(_operands[1]) < 0; + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanNotEquals.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanNotEquals.java new file mode 100644 index 0000000000..a72a467c1f --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanNotEquals.java @@ -0,0 +1,79 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanNotEquals Expression + * + * @author Fraser Adams + */ +public final class BooleanNotEquals extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanNotEquals + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanNotEquals(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanNotEquals() + { + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanNotEquals(final List expr) throws QmfException + { + super(2, expr); + } + + /** + * Evaluate "not equal to" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null) + { + return false; + } + + return !_operands[0].equals(_operands[1]); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanRegexMatch.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanRegexMatch.java new file mode 100644 index 0000000000..30311ba712 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanRegexMatch.java @@ -0,0 +1,95 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A class to create and evaluate the BooleanRegexMatch Expression + * + * @author Fraser Adams + */ +public final class BooleanRegexMatch extends BooleanExpression +{ + private final Pattern _pattern; + + /** + * Factory method to create an instance of BooleanRegexMatch + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanRegexMatch(expr); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanRegexMatch() + { + _pattern = null; + } + + /** + * Main Constructor, uses base class constructor to populate unevaluated operands + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public BooleanRegexMatch(final List expr) throws QmfException + { + super(2, expr); + + try + { + _pattern = Pattern.compile(_operands[1]); + } + catch (PatternSyntaxException pse) + { + throw new QmfException("Error in regular expression " + pse.getMessage()); + } + } + + /** + * Evaluate "regex match" expression against a QmfData instance. + * N.B. to avoid complexities with types this class treats operands as Strings performing an appropriate evaluation + * of the String that makes sense for a given expression e.g. parsing as a double for {@literal >, >=, <, <= } + * + * @param data the object to evaluate the expression against + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + populateOperands(data); + + if (_operands[0] == null || _operands[1] == null || _pattern == null) + { + return false; + } + + Matcher matcher = _pattern.matcher(_operands[0]); + return matcher.find(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanTrue.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanTrue.java new file mode 100644 index 0000000000..b092e7daaf --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/BooleanTrue.java @@ -0,0 +1,60 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to create and evaluate the BooleanTrue Expression + * + * @author Fraser Adams + */ +public final class BooleanTrue extends BooleanExpression +{ + /** + * Factory method to create an instance of BooleanTrue + * @param expr the List of Expressions extracted by parsing the Query predicate + * @return an instance of the concrete BooleanExpression + */ + public Expression create(final List expr) throws QmfException + { + return new BooleanTrue(); + } + + /** + * Basic Constructor primarily used by the prototype instance of each concrete BooleanExpression + */ + public BooleanTrue() + { + } + + /** + * Evaluate "true" expression against a QmfData instance. + * @param data the object to evaluate the expression against + * @return true. + */ + public boolean evaluate(final QmfData data) + { + return true; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Expression.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Expression.java new file mode 100644 index 0000000000..4e92af911b --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Expression.java @@ -0,0 +1,77 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Iterator; +import java.util.List; + +/** + * This class represents the base class for all Expressions created by expanding the Query predicate. + * <p> + * Depending on the structure of the expression list there might be a nested structure of Expressions comprising + * a mixture of LogicalExpressions and BooleanExpressions. + * <p> + * The Expression structure is illustrated below in the context of its relationship with QmfQuery. + * <img alt="" src="doc-files/QmfQuery.png"> + * + * @author Fraser Adams + */ +public abstract class Expression +{ + /** + * Factory method to create concrete Expression instances base on the operator name extracted from the expression List. + * This method will create a LogicalExpression from an "and", "or" or "not" operator otherwise it will create + * a BooleanExpression. + * + * @param expr the List of Expressions extracted by parsing the Query predicate + */ + public static Expression createExpression(final List expr) throws QmfException + { + Iterator iter = expr.listIterator(); + if (!iter.hasNext()) + { + throw new QmfException("Missing operator in predicate expression"); + } + + String op = (String)iter.next(); + if (op.equals("not")) + { + return new LogicalNot(expr); + } + if (op.equals("and")) + { + return new LogicalAnd(expr); + } + if (op.equals("or")) + { + return new LogicalOr(expr); + } + return BooleanExpression.createExpression(expr); + } + + /** + * Evaluate expression against a QmfData instance. + * @return true if query matches the QmfData instance, else false. + */ + public abstract boolean evaluate(final QmfData data); +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Handle.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Handle.java new file mode 100644 index 0000000000..7f81b06900 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Handle.java @@ -0,0 +1,107 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// JMS Imports +import javax.jms.Destination; // Needed for replyTo +import javax.jms.JMSException; + +/** + * This class represents the reply Handle used for asynchronous operations + * + * @author Fraser Adams + */ +public final class Handle +{ + private final String _correlationId; + private final Destination _replyTo; + + /** + * Construct a Handle containing only a correlationId + * + * @param correlationId - a String used to tie together requests and responses + */ + public Handle(final String correlationId) + { + _correlationId = correlationId; + _replyTo = null; + } + + /** + * Construct a Handle containing a correlationId and a replyTo. + * + * @param correlationId - a String used to tie together requests and responses + * @param replyTo - the JMS replyTo + */ + public Handle(final String correlationId, final Destination replyTo) + { + _correlationId = correlationId; + _replyTo = replyTo; + } + + /** + * Returns the correlationId String. + * @return the correlationId String + */ + public String getCorrelationId() + { + return _correlationId; + } + + /** + * Return the replyTo Destination. + * @return the replyTo Destination + */ + public Destination getReplyTo() + { + return _replyTo; + } + + /** + * Returns the Routing Key for the replyTo as a String + * <p> + * All things being equal it probably makes most logical sense to use the replyTo obtained from the JMS + * Message when replying to a request however..... for Qpid up to version 0.12 at least there seems to be + * a bug with the replyTo whereby invoking send() on the replyTo causes spurious exchangeDeclares to occur. + * The exchangeDeclare is apparently to validate the destination however there is supposed to be a cache + * that should prevent this from occurring if the replyTo Destination is reused, but that's broken. + * <p> + * As an alternative we get hold of the Routing Key of the replyTo which, is sneakily available from getTopicName() + * the Routing Key can then be used as the subject of the returned message to enable delivery of the Message + * to the appropriate address. + * <p> + * org.apache.qpid.client.AMQTopic.getTopicName() returns "getRoutingKey().asString()" so this seems an OK + * way to get the Routing Key from the replyTo using the pure JMS API. + * + * @return the Routing Key for the replyTo + */ + public String getRoutingKey() + { + try + { + return ((javax.jms.Topic)_replyTo).getTopicName(); + } + catch (JMSException jmse) + { + return ""; + } + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalAnd.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalAnd.java new file mode 100644 index 0000000000..12b9f3d093 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalAnd.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to evaluate the LogicalAnd Expression + * + * @author Fraser Adams + */ + +public final class LogicalAnd extends LogicalExpression +{ + /** + * This method iterates through collecting the sub-expressions of the Logical Expression + * + * @param expr the List of sub-expressions extracted by parsing the Query predicate, the first one should be + * the Logical Expression's operator name + */ + public LogicalAnd(final List expr) throws QmfException + { + super(expr); + } + + /** + * Evaluate the Logical And expression against a QmfData instance. + * @return false if any of the sub-expressions is false otherwise returns true + */ + public boolean evaluate(final QmfData data) + { + for (Expression e : _subExpressions) + { + if (!e.evaluate(data)) + { + return false; + } + } + return true; + } +} + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalExpression.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalExpression.java new file mode 100644 index 0000000000..23a5ac3abd --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalExpression.java @@ -0,0 +1,64 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/* + * This class represents the base class for all Logical Expressions (and, or, not) created by expanding the Query predicate. + * + * @author Fraser Adams + */ +public abstract class LogicalExpression extends Expression +{ + protected List<Expression> _subExpressions = new ArrayList<Expression>(); + + /** + * Constructor. This method iterates through collecting the sub-expressions of the Logical Expression + * + * @param expr the List of sub-expressions extracted by parsing the Query predicate, the first one should be + * the Logical Expression's operator name + */ + public LogicalExpression(final List expr) throws QmfException + { + Iterator iter = expr.listIterator(); + String op = (String)iter.next(); +//System.out.println("LogicalExpression, op = " + op); + + // Collect sub-expressions + while (iter.hasNext()) + { + Object object = iter.next(); + if (object instanceof List) + { + _subExpressions.add(createExpression((List)object)); + } + else + { + throw new QmfException("Operands of " + op + " must be Lists"); + } + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalNot.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalNot.java new file mode 100644 index 0000000000..029ea00af9 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalNot.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to evaluate the LogicalNot Expression + * + * @author Fraser Adams + */ + +public final class LogicalNot extends LogicalExpression +{ + /** + * This method iterates through collecting the sub-expressions of the Logical Expression + * + * @param expr the List of sub-expressions extracted by parsing the Query predicate, the first one should be + * the Logical Expression's operator name + */ + public LogicalNot(final List expr) throws QmfException + { + super(expr); + } + + /** + * Evaluate the Logical Not expression against a QmfData instance. + * @return false if any of the sub-expressions is true otherwise returns true + */ + public boolean evaluate(final QmfData data) + { + for (Expression e : _subExpressions) + { + if (e.evaluate(data)) + { + return false; + } + } + return true; + } +} + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalOr.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalOr.java new file mode 100644 index 0000000000..72c8c1ff15 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/LogicalOr.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.List; + +/** + * A class to evaluate the LogicalOr Expression + * + * @author Fraser Adams + */ + +public final class LogicalOr extends LogicalExpression +{ + /** + * This method iterates through collecting the sub-expressions of the Logical Expression + * + * @param expr the List of sub-expressions extracted by parsing the Query predicate, the first one should be + * the Logical Expression's operator name + */ + public LogicalOr(final List expr) throws QmfException + { + super(expr); + } + + /** + * Evaluate the Logical Or expression against a QmfData instance. + * @return true if any of the sub-expressions is true otherwise returns false + */ + public boolean evaluate(final QmfData data) + { + for (Expression e : _subExpressions) + { + if (e.evaluate(data)) + { + return true; + } + } + return false; + } +} + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Notifier.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Notifier.java new file mode 100644 index 0000000000..ecdce269b4 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/Notifier.java @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * The QMF2 API has a work-queue Callback approach. All asynchronous events are represented by a WorkItem object. + * When a QMF event occurs it is translated into a WorkItem object and placed in a FIFO queue. It is left to the + * application to drain this queue as needed. + * <p> + * This new API does require the application to provide a single callback. The callback is used to notify the + * application that WorkItem object(s) are pending on the work queue. This callback is invoked by QMF when one or + * more new WorkItem objects are added to the queue. To avoid any potential threading issues, the application is + * not allowed to call any QMF API from within the context of the callback. The purpose of the callback is to + * notify the application to schedule itself to drain the work queue at the next available opportunity. + * <p> + * For example, a console application may be designed using a select() loop. The application waits in the select() + * for any of a number of different descriptors to become ready. In this case, the callback could be written to + * simply make one of the descriptors ready, and then return. This would cause the application to exit the wait state, + * and start processing pending events. + * <p> + * The callback is represented by the Notifier virtual base class. This base class contains a single method. An + * application derives a custom notification handler from this class, and makes it available to the Console or Agent object. + * <p> + * The following diagram illustrates the Notifier and WorkQueue QMF2 API Event model. + * <p> + * Notes + * <ol> + * <li>There is an alternative (simpler but not officially QMF2) API based on implementing the QmfEventListener.</li> + * <li>BlockingNotifier is not part of QMF2 either but is how most people would probably write a Notifier.</li> + * <li>It's generally not necessary to use a Notifier as the Console provides a blocking getNextWorkitem() method.</li> + * </ol> + * <p> + * <img alt="" src="doc-files/WorkQueueEventModel.png"> + * + * @author Fraser Adams + */ +public interface Notifier extends QmfCallback +{ + /** + * Called when the Console internal work queue becomes non-empty due to the arrival of one or more WorkItems. + * <p> + * This method will be called by the internal QMF management thread. It is illegal to invoke any QMF APIs + * from within this callback. The purpose of this callback is to indicate that the application should schedule + * itself to process the work items. A common implementation would be to call notify() to unblock a waiting Thread. + * + */ + public void indication(); +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NotifierWrapper.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NotifierWrapper.java new file mode 100644 index 0000000000..42bb9cc9d7 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NotifierWrapper.java @@ -0,0 +1,57 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * Implementation of QmfEventListener that wraps a Notifier instance. This class populates the WorkItem + * queue then invokes the Notifier's indication() method to notify clients of available data. + * <p> + * This approach allows us to support two separate asynchronous notification APIs without too much effort. + * + * @author Fraser Adams + */ +public final class NotifierWrapper implements QmfEventListener +{ + private final Notifier _notifier; + private final WorkQueue _workQueue; + + /** + * Wraps a Notifier and WorkQueue so that they me be triggered by a QmfEventListener onEvent() call. + * @param notifier the Notifier instance that will be triggered when NotifierWrapper receives a WorkItem. + * @param workQueue the WorkQueue instance that the WorkItem will be placed on. + */ + public NotifierWrapper(final Notifier notifier, final WorkQueue workQueue) + { + _notifier = notifier; + _workQueue = workQueue; + } + + /** + * This method adds the WorkItem to the WorkQueue then notifies any clients through the Notifier.indication(). + * + * @param item the WorkItem to add to the queue + */ + public void onEvent(final WorkItem item) + { + _workQueue.addWorkItem(item); + _notifier.indication(); + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NullQmfEventListener.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NullQmfEventListener.java new file mode 100644 index 0000000000..447adbe391 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/NullQmfEventListener.java @@ -0,0 +1,43 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * Implementation of QmfEventListener with an empty onEvent(). + * <p> + * Agent or Console instantiate this class if no QmfCallback is supplied so they can avoid lots of ugly tests for + * _eventListener == null. + * + * @author Fraser Adams + */ +public final class NullQmfEventListener implements QmfEventListener +{ + /** + * Passes a WorkItem to the listener. This class provides a null implementation + * + * @param item the WorkItem passed to the listener + */ + public void onEvent(final WorkItem item) + { + // Null implementation + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/ObjectId.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/ObjectId.java new file mode 100644 index 0000000000..b84822def7 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/ObjectId.java @@ -0,0 +1,159 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Map; + +/** + * This class provides a wrapper for QMF Object IDs to enable easier comparisons. + * <p> + * QMF Object IDs are Maps and <i>in theory</i> equals should work, but strings in QMF are actually often returned + * as byte[] due to inconsistent binary and UTF-8 encodings being used and byte[].equals() compares the address not a + * bytewise comparison. + * <p> + * This class creates a String from the internal ObjectId state information to enable easier comparison and rendering. + * + * @author Fraser Adams + */ +public final class ObjectId extends QmfData +{ + private final String _agentName; + private final String _objectName; + private final long _agentEpoch; + + /** + * Create an ObjectId given the ID created via ObjectId.toString(). + * @param oid a string created via ObjectId.toString(). + */ + public ObjectId(String oid) + { + String[] split = oid.split("@"); + + _agentName = split.length == 3 ? split[0] : ""; + _agentEpoch = split.length == 3 ? Long.parseLong(split[1]) : 0; + _objectName = split.length == 3 ? split[2] : ""; + + setValue("_agent_name", _agentName); + setValue("_agent_epoch", _agentEpoch); + setValue("_object_name", _objectName); + } + + /** + * Create an ObjectId given an agentName, objectName and agentEpoch. + * @param agentName the name of the Agent managing the object. + * @param objectName the name of the managed object. + * @param agentEpoch a count used to identify if an Agent has been restarted. + */ + public ObjectId(String agentName, String objectName, long agentEpoch) + { + _agentName = agentName; + _objectName = objectName; + _agentEpoch = agentEpoch; + setValue("_agent_name", _agentName); + setValue("_object_name", _objectName); + setValue("_agent_epoch", _agentEpoch); + } + + /** + * Create an ObjectId from a Map. In essence it "deserialises" its state from the Map. + * @param m the Map the Object is retrieving its state from. + */ + public ObjectId(Map m) + { + super(m); + _agentName = getStringValue("_agent_name"); + _objectName = getStringValue("_object_name"); + _agentEpoch = getLongValue("_agent_epoch"); + } + + /** + * Create an ObjectId from a QmfData object. In essence it "deserialises" its state from the QmfData object. + * @param qmfd the QmfData the Object is retrieving its state from. + */ + public ObjectId(QmfData qmfd) + { + this(qmfd.mapEncode()); + } + + /** + * Returns the name of the Agent managing the object. + * @return the name of the Agent managing the object. + */ + public String getAgentName() + { + return _agentName; + } + + /** + * Returns the name of the managed object. + * @return the name of the managed object. + */ + public String getObjectName() + { + return _objectName; + } + + /** + * Returns the Epoch count. + * @return the Epoch count. + */ + public long getAgentEpoch() + { + return _agentEpoch; + } + + /** + * Compares two ObjectId objects for equality. + * @param rhs the right hands side ObjectId in the comparison. + * @return true if the two ObjectId objects are equal otherwise returns false. + */ + @Override + public boolean equals(Object rhs) + { + if (rhs instanceof ObjectId) + { + return toString().equals(rhs.toString()); + } + return false; + } + + /** + * Returns the ObjectId hashCode. + * @return the ObjectId hashCode. + */ + @Override + public int hashCode() + { + return toString().hashCode(); + } + + /** + * Returns a String representation of the ObjectId. + * @return a String representation of the ObjectId. + */ + @Override + public String toString() + { + return _agentName + "@" + _agentEpoch + "@" + _objectName; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfCallback.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfCallback.java new file mode 100644 index 0000000000..d57282b2e7 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfCallback.java @@ -0,0 +1,33 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * QmfCallback is a "marker interface" used to specify objects that will be notified of a particular + * condition by the Console. This interface is extended by the QmfEventListener and Notifier interfaces + * in order to provide two different types of callback semantic. + * + * @author Fraser Adams + */ +public interface QmfCallback +{ +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfData.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfData.java new file mode 100644 index 0000000000..c634564ba4 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfData.java @@ -0,0 +1,443 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +/** + * QMF defines the QmfData class to represent an atomic unit of managment data. + * <p> + * The QmfData class defines a collection of named data values. Optionally, a string tag, or "sub-type" may be + * associated with each data item. This tag may be used by an application to annotate the data item. + * (Note the tag implementation is TBD). + * <p> + * In QMFv2, message bodies are AMQP maps and are therefore easily extended without causing backward + * compatibility problems. Another benefit of the map-message format is that all messages can be fully + * parsed when received without the need for external schema data + * <p> + * This class is the base class for all QMF2 Data Types in this implementation. It is intended to provide a + * useful wrapper for the underlying Map, supplying accessor and mutator methods that give an element of type + * safety (for example strings appear to be encoded as a mixture of byte[] and String depending of the agent, so + * this class checks for this in its getString()) + * <p> + * The following diagram represents the QmfData class hierarchy. + * <p> + * <img alt="" src="doc-files/QmfData.png"> + * + * @author Fraser Adams + */ +public class QmfData +{ + protected Map<String, Object> _values = null; + protected Map<String, String> _subtypes = null; + + /** + * The default constructor, initialises the QmfData with an empty Map. + */ + public QmfData() + { + _values = new HashMap<String, Object>(); + } + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the Map used to construct the QmfData. + */ + @SuppressWarnings("unchecked") + public QmfData(final Map m) + { + if (m == null) + { // This branch is just a safety precaution and shouldn't generally occur in normal usage scenarios. + // Initialise with a new HashMap. Should then behave the same as the default Constructor. + _values = new HashMap<String, Object>(); + } + else + { + Map<String, Object> values = (Map<String, Object>)m.get("_values"); + _values = (values == null) ? m : values; + + Map<String, String> subtypes = (Map<String, String>)m.get("_subtypes"); + _subtypes = subtypes; + } + } + + /** + * Get the state of the _subtypes Map, (generally used when serialising method request/response arguments. + * + * @return the value of the _subtypes Map. + */ + public Map<String, String> getSubtypes() + { + return _subtypes; + } + + + /** + * Set the state of the _subtypes Map, (generally used when deserialising method request/response arguments. + * + * @param subtypes the new value of the _subtypes Map. + */ + @SuppressWarnings("unchecked") + public void setSubtypes(Map subtypes) + { + _subtypes = subtypes; + } + + /** + * Helper method to return the <i>best</i> String representation of the given Object. + * <p> + * There seem to be some inconsistencies where string properties are sometimes returned as byte[] and + * sometimes as Strings. It seems to vary depending on the Agent and is due to strings being encoded as a mix of + * binary strings and UTF-8 strings by C++ Agent classes. Getting it wrong results in ClassCastExceptions, which + * is clearly unfortunate. + * <p> + * This is basically a helper method to check the type of a property and return the most "appropriate" + * String representation for it. + * + * @param p a property in Object form + * @return the most appropriate String representation of the property + */ + public static final String getString(final Object p) + { + if (p == null) + { + return ""; + } + else if (p instanceof String) + { + return (String)p; + } + else if (p instanceof byte[]) + { + return new String((byte[])p); + } + else return p.toString(); + } + + /** + * Helper method to return the <i>best</i> long representation of the given Object. + * <p> + * There seem to be some inconsistencies where properties are sometimes returned as Integer and + * sometimes as Long. It seems to vary depending on the Agent and getting it wrong results in + * ClassCastExceptions, which is clearly unfortunate. + * <p> + * This is basically a helper method to check the type of a property and return a long representation for it. + * + * @param p a property in Object form + * @return the long representation for it. + */ + public static final long getLong(final Object p) + { + if (p == null) + { + return 0; + } + else if (p instanceof Long) + { + return ((Long)p).longValue(); + } + else if (p instanceof Integer) + { + return ((Integer)p).intValue(); + } + else if (p instanceof Short) + { + return ((Short)p).shortValue(); + } + else return 0; + } + + /** + * Helper method to return the <i>best</i> boolean representation of the given Object. + * <p> + * There seem to be some inconsistencies where boolean properties are sometimes returned as Boolean and + * sometimes as Long/Integer/Short. It seems to vary depending on the Agent and getting it wrong results in + * ClassCastExceptions, which is clearly unfortunate. + * <p> + * This is basically a helper method to check the type of a property and return a boolean representation for it. + * + * @param p a property in Object form + * @return the boolean representation for it. + */ + public static final boolean getBoolean(final Object p) + { + if (p == null) + { + return false; + } + else if (p instanceof Boolean) + { + return ((Boolean)p).booleanValue(); + } + else if (p instanceof Long) + { + return ((Long)p).longValue() > 0; + } + else if (p instanceof Integer) + { + return ((Integer)p).intValue() > 0; + } + else if (p instanceof Short) + { + return ((Short)p).shortValue() > 0; + } + else if (p instanceof String) + { + return Boolean.parseBoolean((String)p); + } + else return false; + } + + /** + * Helper method to return the <i>best</i> double representation of the given Object. + * <p> + * There seem to be some inconsistencies where properties are sometimes returned as Float and + * sometimes as Double. It seems to vary depending on the Agent and getting it wrong results in + * ClassCastExceptions, which is clearly unfortunate. + * <p> + * This is basically a helper method to check the type of a property and return a Double representation for it. + * + * @param p a property in Object form + * @return the Double representation for it. + */ + public static final double getDouble(final Object p) + { + if (p == null) + { + return 0.0d; + } + else if (p instanceof Float) + { + return ((Float)p).floatValue(); + } + else if (p instanceof Double) + { + return ((Double)p).doubleValue(); + } + else return 0.0d; + } + + /** + * Determines if the named property exists. + * + * @param name of the property to check. + * @return true if the property exists otherwise false. + */ + public final boolean hasValue(final String name) + { + return _values.containsKey(name); + } + + /** + * Accessor method to return a named property as an Object. + * + * @param name of the property to return as an Object. + * @return value of property as an Object. + */ + @SuppressWarnings("unchecked") + public final <T> T getValue(final String name) + { + return (T)_values.get(name); + } + + /** + * Mutator method to set a named Object property. + * + * @param name the name of the property to set. + * @param value the value of the property to set. + */ + public final void setValue(final String name, final Object value) + { + _values.put(name, value); + } + + /** + * Mutator method to set a named Object property. + * + * @param name the name of the property to set. + * @param value the value of the property to set. + * @param subtype the subtype of the property. + */ + public final void setValue(final String name, final Object value, final String subtype) + { + setValue(name, value); + setSubtype(name, subtype); + } + + /** + * Mutator to set or modify the subtype associated with name. + * + * @param name the name of the property to set the subtype for. + * @param subtype the subtype of the property. + */ + public final void setSubtype(final String name, final String subtype) + { + if (_subtypes == null) + { + _subtypes = new HashMap<String, String>(); + } + _subtypes.put(name, subtype); + } + + /** + * Accessor to return the subtype associated with named property. + * + * @param name the name of the property to get the subtype for. + * @return the subtype of the named property or null if not present. + */ + public final String getSubtype(final String name) + { + if (_subtypes == null) + { + return null; + } + return _subtypes.get(name); + } + + /** + * Accessor method to return a named property as a boolean. + * + * @param name of the property to return as a boolean. + * @return value of property as a boolean. + */ + public final boolean getBooleanValue(final String name) + { + return getBoolean(getValue(name)); + } + + /** + * Accessor method to return a named property as a long. + * + * @param name of the property to return as a long. + * @return value of property as a long. + */ + public final long getLongValue(final String name) + { + return getLong(getValue(name)); + } + + /** + * Accessor method to return a named property as a double. + * + * @param name of the property to return as a double. + * @return value of property as a double. + */ + public final double getDoubleValue(final String name) + { + return getDouble(getValue(name)); + } + + /** + * Accessor method to return a named property as a String. + * + * @param name of the property to return as a String. + * @return value of property as a String. + */ + public final String getStringValue(final String name) + { + return getString(getValue(name)); + } + + /** + * Accessor method to return a reference property. + * <p> + * Many QMF Objects contain reference properties, e.g. references to other QMF Objects. + * This method allows these to be obtained as ObjectId objects to enable much easier + * comparison and rendering. + * @return the retrieved value as an ObjectId instance. + */ + public final ObjectId getRefValue(final String name) + { + return new ObjectId((Map)getValue(name)); + } + + /** + * Mutator method to set a named reference property. + * <p> + * Many QMF Objects contain reference properties, e.g. references to other QMF Objects. + * This method allows these to be set as ObjectId objects. + * + * @param name the name of the property to set. + * @param value the value of the property to set. + */ + public final void setRefValue(final String name, final ObjectId value) + { + setValue(name, value.mapEncode()); + } + + /** + * Mutator method to set a named reference property. + * <p> + * Many QMF Objects contain reference properties, e.g. references to other QMF Objects. + * This method allows these to be set as ObjectId objects. + * + * @param name the name of the property to set. + * @param value the value of the property to set. + * @param subtype the subtype of the property. + */ + public final void setRefValue(final String name, final ObjectId value, final String subtype) + { + setRefValue(name, value); + setSubtype(name, subtype); + } + + /** + * Return the underlying Map representation of this QmfData. + * @return the underlying Map. + */ + public Map<String, Object> mapEncode() + { + return _values; + } + + /** + * Helper/debug method to list the properties and their type. + */ + public void listValues() + { + for (Map.Entry<String, Object> entry : _values.entrySet()) + { + Object key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof Map) + { // Check if the value part is an ObjectId and display appropriately + Map map = (Map)value; + if (map.containsKey("_object_name")) + { + System.out.println(key + ": " + new ObjectId(map)); + } + else + { + System.out.println(key + ": " + getString(value)); + } + } + else + { + System.out.println(key + ": " + getString(value)); + } + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfDescribed.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfDescribed.java new file mode 100644 index 0000000000..c81f674d1f --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfDescribed.java @@ -0,0 +1,84 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Map; +import java.util.UUID; + +/** + * Subclass of QmfData. + * <p> + * When representing formally defined data, a QmfData instance is assigned a Schema. + * + * @author Fraser Adams + */ +public class QmfDescribed extends QmfData +{ + private SchemaClassId _schema_id; + + /** + * The default constructor, initialises the underlying QmfData base class with an empty Map + */ + protected QmfDescribed() + { + } + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the QmfDescribed + */ + public QmfDescribed(final Map m) + { + super(m); + _schema_id = (m == null) ? null : new SchemaClassId((Map)m.get("_schema_id")); + } + + /** + * Returns the SchemaClassId describing this object. + * @return the SchemaClassId describing this object. + */ + public final SchemaClassId getSchemaClassId() + { + return _schema_id; + } + + /** + * Sets the SchemaClassId describing this object. + * @param schema_id the SchemaClassId describing this object. + */ + public final void setSchemaClassId(final SchemaClassId schema_id) + { + _schema_id = schema_id; + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + _schema_id.listValues(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEvent.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEvent.java new file mode 100644 index 0000000000..06c19ca0ab --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEvent.java @@ -0,0 +1,224 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * QMF supports event management functionality. An event is a notification that is sent by an Agent to alert Console(s) + * of a change in some aspect of the system under management. Like QMF Data, Events may be formally defined by a Schema + * or not. Unlike QMF Data, Events are not manageable entities - they have no lifecycle. Events simply indicate a point + * in time where some interesting action occurred. + * <p> + * An AMQP timestamp value is associated with each QmfEvent instance. It indicates the moment in time the event occurred. + * This timestamp is mandatory. + * <p> + * A severity level may be associated with each QmfEvent instance. The following severity levels are supported: + * <pre> + * * "emerg" - system is unusable + * * "alert" - action must be taken immediately + * * "crit" - the system is in a critical condition + * * "err" - there is an error condition + * * "warning" - there is a warning condition + * * "notice" - a normal but significant condition + * * "info" - a purely informational message + * * "debug" - messages generated to debug the application + * </pre> + * The default severity is "notice". + * + * @author Fraser Adams + */ +public final class QmfEvent extends QmfDescribed +{ + private static final String[] severities = {"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"}; + + private long _timestamp; + private int _severity; + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the QmfEvent + */ + public QmfEvent(final Map m) + { + super(m); + _timestamp = m.containsKey("_timestamp") ? getLong(m.get("_timestamp")) : System.currentTimeMillis()*1000000l; + _severity = m.containsKey("_severity") ? (int)getLong(m.get("_severity")) : 5; + } + + /** + * This constructor taking a SchemaEventClass is the main constructor used by Agents when creating Events + * + * @param schema the SchemaEventClass describing the Event + */ + public QmfEvent(final SchemaEventClass schema) + { + _timestamp = System.currentTimeMillis()*1000000l; + setSchemaClassId(schema.getClassId()); + } + + /** + * Return the timestamp. Timestamps are recorded in nanoseconds since the epoch. + * <p> + * An AMQP timestamp value is associated with each QmfEvent instance. It indicates the moment in time the event + * occurred. This timestamp is mandatory. + * + * @return the AMQP timestamp value in nanoseconds since the epoch. + */ + public long getTimestamp() + { + return _timestamp; + } + + /** + * Return the severity. + * <p> + * A severity level may be associated with each QmfEvent instance. The following severity levels are supported: + * <pre> + * * "emerg" - system is unusable + * * "alert" - action must be taken immediately + * * "crit" - the system is in a critical condition + * * "err" - there is an error condition + * * "warning" - there is a warning condition + * * "notice" - a normal but significant condition + * * "info" - a purely informational message + * * "debug" - messages generated to debug the application + * </pre> + * The default severity is "notice" + * + * @return the severity value as a String as described above + */ + public String getSeverity() + { + return severities[_severity]; + } + + /** + * Set the severity level of the Event + * @param severity the severity level of the Event as an int + */ + public void setSeverity(final int severity) + { + if (severity < 0 || severity > 7) + { + // If supplied value is out of range we set to the default severity + _severity = 5; + return; + } + _severity = severity; + } + + /** + * Set the severity level of the Event + * @param severity the severity level of the Event as a String. + * <p> + * The following severity levels are supported: + * <pre> + * * "emerg" - system is unusable + * * "alert" - action must be taken immediately + * * "crit" - the system is in a critical condition + * * "err" - there is an error condition + * * "warning" - there is a warning condition + * * "notice" - a normal but significant condition + * * "info" - a purely informational message + * * "debug" - messages generated to debug the application + * </pre> + */ + public void setSeverity(final String severity) + { + for (int i = 0; i < severities.length; i++) + { + if (severity.equals(severities[i])) + { + _severity = i; + return; + } + } + // If we can't match the values we set to the default severity + _severity = 5; + } + + /** + * Return the underlying Map representation of this QmfEvent + * @return the underlying map. + */ + @Override + public Map<String, Object> mapEncode() + { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("_values", super.mapEncode()); + if (_subtypes != null) + { + map.put("_subtypes", _subtypes); + } + map.put("_schema_id", getSchemaClassId().mapEncode()); + map.put("_timestamp", _timestamp); + map.put("_severity", _severity); + return map; + } + + /** + * Helper/debug method to list the object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("QmfEvent:"); + System.out.println(this); + } + + /** + * Returns a String representation of the QmfEvent. + * <p> + * The String representation attempts to mirror the python class Event __repr__ method as far as possible. + * @return a String representation of the QmfEvent. + */ + @Override + public String toString() + { + if (getSchemaClassId() == null) + { + return "<uninterpretable>"; + } + + String out = new Date(getTimestamp()/1000000l).toString(); + out += " " + getSeverity() + " " + getSchemaClassId().getPackageName() + ":" + getSchemaClassId().getClassName(); + + StringBuilder buf = new StringBuilder(); + for (Map.Entry<String, Object> entry : super.mapEncode().entrySet()) + { + String disp = getString(entry.getValue()); + if (disp.contains(" ")) + { + disp = "\"" + disp + "\""; + } + + buf.append(" " + entry.getKey() + "=" + disp); + } + + return out + buf.toString(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEventListener.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEventListener.java new file mode 100644 index 0000000000..c6b1c14350 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfEventListener.java @@ -0,0 +1,51 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * A QmfEventListener object is used to receive asynchronously delivered WorkItems. + * <p> + * This provides an alternative (simpler) API to the official QMF2 WorkQueue API that some (including the Author) + * may prefer over the official API. + * <p> + * The following diagram illustrates the QmfEventListener Event model. + * <p> + * Notes + * <ol> + * <li>This is provided as an alternative to the official QMF2 WorkQueue and Notifier Event model.</li> + * <li>Agent and Console methods are sufficiently thread safe that it is possible to call them from a callback fired + * from the onEvent() method that may have been called from the JMS MessageListener. Internally the synchronous + * and asynchronous calls are processed on different JMS Sessions to facilitate this</li> + * </ol> + * <p> + * <img alt="" src="doc-files/QmfEventListenerModel.png"> + * + * @author Fraser Adams + */ +public interface QmfEventListener extends QmfCallback +{ + /** + * Passes a WorkItem to the listener. + * + * @param item the WorkItem passed to the listener + */ + public void onEvent(WorkItem item); +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfException.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfException.java new file mode 100644 index 0000000000..aa397e6116 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfException.java @@ -0,0 +1,42 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * A QmfException object + * + * @author Fraser Adams + */ +public class QmfException extends Exception +{ + private static final long serialVersionUID = 7526471155622776147L; + + /** + * Create a QmfException with a given message String. + * @param message the message String. + */ + public QmfException(String message) + { + super(message); + } +} + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfManaged.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfManaged.java new file mode 100644 index 0000000000..44dbcef9bf --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfManaged.java @@ -0,0 +1,83 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Map; + +/** + * Subclass of QmfDescribed, which is itself a subclass of QmfData. + * <p> + * When representing managed data, a QmfData instance is assigned an object identifier + * + * @author Fraser Adams + */ +public class QmfManaged extends QmfDescribed +{ + private ObjectId _object_id; + + /** + * The default constructor, initialises the underlying QmfData base class with an empty Map + */ + protected QmfManaged() + { + } + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the QmfManaged + */ + public QmfManaged(final Map m) + { + super(m); + _object_id = (m == null) ? null : new ObjectId((Map)m.get("_object_id")); + } + + /** + * Returns the ObjectId of this managed object. + * @return the ObjectId of this managed object. + */ + public final ObjectId getObjectId() + { + return _object_id; + } + + /** + * Sets the ObjectId of this managed object. + * @param object_id the ObjectId of this managed object. + */ + public final void setObjectId(final ObjectId object_id) + { + _object_id = object_id; + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + System.out.println("_object_id: " + getObjectId()); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQuery.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQuery.java new file mode 100644 index 0000000000..e9b8e13deb --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQuery.java @@ -0,0 +1,343 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +// Reuse this class as it provides a handy mechanism to parse an predicate String into a Map +import org.apache.qpid.messaging.util.AddressParser; + +/** + * A Query is a mechanism for interrogating the management database. A Query represents a selector which is sent to + * an Agent. The Agent applies the Query against its management database, and returns those objects which meet the + * constraints described in the query. + * <p> + * A Query must specify the class of information it is selecting. This class of information is considered the target + * of the query. Any data objects selected by the query will be of the type indicated by the target. + * <p> + * A Query may also specify a selector which is used as a filter against the set of all target instances. Only those + * instances accepted by the filter will be returned in response to the query. + * <p> + * N.B. There appear to be a number of differences in the description of the map encoding of a Query between the + * QMF2 API specified at <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> and the + * QMF2 protocol that is specified at <a href=https://cwiki.apache.org/qpid/qmf-map-message-protocol.html>QMF Map + * Message Protocol</a> in particular the use of the underscore to specify key names e.g. "_what", "_where", + * "_object_id", "_schema_id". + * <p> + * This implementation trusts the protocol specification more than the API specification as the underscores are more + * consistent with the rest of the protocol and the underscored variants are what have been observed when querying + * the broker ManagementAgent. + * <p> + * A QmfQuery may be constructed as either an "ID" query (to query for a specific ObjectId or SchemaClassId) or a + * "PREDICATE" query (to query based upon an expression). Note that QMF considers string arguments in boolean + * expressions to be names of data values in the target object. When evaluating a predicate expression, QMF will fetch + * the value of the named data item from each candidate target object. The value is then used in the boolean expression. + * In other words, QMF considers string arguments to be variables in the expression. In order to indicate that a string + * should be treated as a literal instead, the string must be quoted using the "quote" expression. + * <p> + * <b>Examples</b> + * <p> + * Assume a QmfData type defines fields named "name", "address" and "town". The following predicate expression matches + * any instance with a name field set to "tross", or any instance where the name field is "jross", the address field is + * "1313 Spudboy Lane" and the town field is "Utopia": + * <p> + * <pre> + * ["or" ["eq" "name" ["quote" "tross"]] + * ["and" ["eq" "name" ["quote" "jross"]] + * ["eq" "address" ["quote" "1313 Spudboy Lane"]] + * ["eq" ["quote" "Utopia"] "town"] + * ] + * ] + * </pre> + * Assume a QmfData type with fields "name" and "age". A predicate to find all instances with name matching the regular + * expression "?ross" with an optional age field that is greater than the value 29 or less than 12 would be: + * <pre> + * ["and" ["re_match" "name" ["quote" "?ross"]] + * ["and" ["exists" "age"] + * ["or" ["gt" "age" 27] ["lt" "age" 12]] + * ] + * ] + * </pre> + * <p> + * The Expression structure is illustrated below in the context of its relationship with QmfQuery. + * <img alt="" src="doc-files/QmfQuery.png"> + * + * + * @author Fraser Adams + */ +public final class QmfQuery extends QmfData +{ + public static final QmfQuery ID = new QmfQuery(); + public static final QmfQuery PREDICATE = new QmfQuery(); + + private QmfQueryTarget _target; + private SchemaClassId _classId; + private String _packageName; + private String _className; + private ObjectId _objectId; + private List _predicate; + private Expression _expression; + + /** + * This Constructor is only used to construct the ID and PREDICATE objects + */ + private QmfQuery() + { + } + + /** + * Construct an QmfQuery with no Selector from a QmfQueryTarget + * @param target the query target + */ + public QmfQuery(final QmfQueryTarget target) + { + _target = target; + setValue("_what", _target.toString()); + } + + /** + * Construct an ID QmfQuery from a QmfQueryTarget and SchemaClassId + * @param target the query target + * @param classId the SchemaClassId to evaluate against + */ + public QmfQuery(final QmfQueryTarget target, final SchemaClassId classId) + { + _target = target; + _classId = classId; + _packageName = _classId.getPackageName(); + _className = _classId.getClassName(); + setValue("_what", _target.toString()); + setValue("_schema_id", _classId.mapEncode()); + } + + /** + * Construct an ID QmfQuery from a QmfQueryTarget and ObjectId + * @param target the query target + * @param objectId the ObjectId to evaluate against + */ + public QmfQuery(final QmfQueryTarget target, final ObjectId objectId) + { + _target = target; + _objectId = objectId; + setValue("_what", _target.toString()); + setValue("_object_id", _objectId.mapEncode()); + } + + /** + * Construct a PREDICATE QmfQuery from a QmfQueryTarget and predicate String + * @param target the query target + * @param predicateString the predicate to evaluate against + */ + public QmfQuery(final QmfQueryTarget target, final String predicateString) throws QmfException + { + _target = target; + + if (predicateString.charAt(0) == '[') + { + Map predicateMap = new AddressParser("{'_where': " + predicateString + "}").map(); + _predicate = (List)predicateMap.get("_where"); + _expression = Expression.createExpression(_predicate); + } + else + { + throw new QmfException("Invalid predicate format"); + } + + setValue("_what", _target.toString()); + setValue("_where", _predicate); + } + + /** + * Construct a QmfQuery from a Map encoding + * @param m encoding the query + */ + public QmfQuery(final Map m) throws QmfException + { + super(m); + + _target = QmfQueryTarget.valueOf(getStringValue("_what")); + + if (hasValue("_object_id")) + { + _objectId = getRefValue("_object_id"); + } + + if (hasValue("_schema_id")) + { + _classId = new SchemaClassId((Map)getValue("_schema_id")); + _packageName = _classId.getPackageName(); + _className = _classId.getClassName(); + } + + if (hasValue("_where")) + { + _predicate = (List)getValue("_where"); + _expression = Expression.createExpression(_predicate); + } + } + + /** + * Return target name. + * @return target name. + */ + public QmfQueryTarget getTarget() + { + return _target; + } + + /** + * Undefined by QMF2 API. + * <p> + * According to <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Specification</a> + * "The value of the <target name string> map entry is ignored for now, its use is TBD." + * so this method returns a null Map. + */ + public Map getTargetParam() + { + return null; + } + + /** + * Return QmfQuery.ID or QmfQuery.PREDICATE or null if there is no Selector + * @return QmfQuery.ID or QmfQuery.PREDICATE or null if there is no Selector + */ + public QmfQuery getSelector() + { + if (_predicate == null) + { + if (_objectId == null && _classId == null) + { + return null; + } + return ID; + } + return PREDICATE; + } + + /** + * Return predicate expression if selector type is QmfQuery.PREDICATE + * @return predicate expression if selector type is QmfQuery.PREDICATE + */ + public List getPredicate() + { + return _predicate; + } + + /** + * Return the SchemaClassId if selector type is QmfQuery.ID + * @return the SchemaClassId if selector type is QmfQuery.ID + */ + public SchemaClassId getSchemaClassId() + { + return _classId; + } + + /** + * Return the ObjectId if selector type is QmfQuery.ID + * @return the ObjectId if selector type is QmfQuery.ID + */ + public ObjectId getObjectId() + { + return _objectId; + } + + /** + * Evaluate query against a QmfData instance. + * @return true if query matches the QmfData instance, else false. + */ + public boolean evaluate(final QmfData data) + { + if (_predicate == null) + { + if (data instanceof QmfManaged) + { + QmfManaged managedData = (QmfManaged)data; + // Evaluate an ID query on Managed Data + if (_objectId != null && _objectId.equals(managedData.getObjectId())) + { + return true; + } + else if (_classId != null) + { + SchemaClassId dataClassId = managedData.getSchemaClassId(); + String dataClassName = dataClassId.getClassName(); + String dataPackageName = dataClassId.getPackageName(); + + // Wildcard the package name if it hasn't been specified when checking class name + if (_className.equals(dataClassName) && + (_packageName.length() == 0 || _packageName.equals(dataPackageName))) + { + return true; + } + + // Wildcard the class name if it hasn't been specified when checking package name + if (_packageName.equals(dataPackageName) && + (_className.length() == 0 || _className.equals(dataClassName))) + { + return true; + } + } + } + return false; + } + else + { + // Evaluate a PREDICATE query by evaluating against the expression created from the predicate + if (_predicate.size() == 0) + { + return true; + } + + return _expression.evaluate(data); + } + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("QmfQuery:"); + System.out.println("target: " + _target); + if (_predicate != null) + { + System.out.println("selector: QmfQuery.PREDICATE"); + System.out.println("predicate: " + _predicate); + } + else if (_classId != null) + { + System.out.println("selector: QmfQuery.ID"); + _classId.listValues(); + } + else if (_objectId != null) + { + System.out.println("selector: QmfQuery.ID"); + System.out.println(_objectId); + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQueryTarget.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQueryTarget.java new file mode 100644 index 0000000000..01aba88bfc --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfQueryTarget.java @@ -0,0 +1,36 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * An enum containing the QMF type code indicating a QMF query target defined in + * <a href=https://cwiki.apache.org/qpid/qmf-map-message-protocol.html>QMF Map Message Protocol</a> + * + * @author Fraser Adams + */ +public enum QmfQueryTarget +{ + SCHEMA_ID, + SCHEMA, + OBJECT_ID, + OBJECT; +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfType.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfType.java new file mode 100644 index 0000000000..a19ebcf393 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/QmfType.java @@ -0,0 +1,39 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +/** + * An enum containing the QMF type code indicating a QMF object's data type + * + * @author Fraser Adams + */ +public enum QmfType +{ + TYPE_VOID, + TYPE_BOOL, + TYPE_INT, + TYPE_FLOAT, + TYPE_STRING, + TYPE_MAP, + TYPE_LIST, + TYPE_UUID; +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClass.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClass.java new file mode 100644 index 0000000000..045628dc1a --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClass.java @@ -0,0 +1,135 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.UUID; + +/** + * Subclass of QmfData + * <p> + * When representing formally defined data, a QmfData instance is assigned a Schema. + * <p> + * The following diagram illustrates the QMF2 Schema class hierarchy. + * <p> + * <img alt="" src="doc-files/Schema.png"> + * + * @author Fraser Adams + */ +public class SchemaClass extends QmfData +{ + public static final SchemaClass EMPTY_SCHEMA = new SchemaClass(); + + private SchemaClassId _classId; + + /** + * The default constructor, initialises the underlying QmfData base class with an empty Map + */ + protected SchemaClass() + { + } + + /** + * The main constructor, taking a java.util.Map as a parameter. + * + * @param m the map used to construct the SchemaClass + */ + public SchemaClass(final Map m) + { + super(m); + _classId = new SchemaClassId((Map)getValue("_schema_id")); + } + + /** + * Return the SchemaClassId that identifies this Schema instance. + * @return the SchemaClassId that identifies this Schema instance. + */ + public final SchemaClassId getClassId() + { + if (_classId.getHashString() == null) + { + _classId = new SchemaClassId(_classId.getPackageName(), + _classId.getClassName(), + _classId.getType(), + generateHash()); + } + return _classId; + } + + /** + * Set the SchemaClassId that identifies this Schema instance. + * @param cid the SchemaClassId that identifies this Schema instance. + */ + public final void setClassId(final SchemaClassId cid) + { + _classId = cid; + } + + /** + * Return a hash generated over the body of the schema, and return a representation of the hash + * @return a hash generated over the body of the schema, and return a representation of the hash + */ + public final UUID generateHash() + { + try + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + updateHash(md5); + return UUID.nameUUIDFromBytes(md5.digest()); + } + catch (NoSuchAlgorithmException nsae) + { + } + return null; + } + + /** + * Generate the partial hash for the schema fields in this base class + * @param md5 the MessageDigest to be updated + */ + protected void updateHash(MessageDigest md5) + { + try + { + md5.update(_classId.getPackageName().getBytes("UTF-8")); + md5.update(_classId.getClassName().getBytes("UTF-8")); + md5.update(_classId.getType().getBytes("UTF-8")); + } + catch (UnsupportedEncodingException uee) + { + } + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + _classId.listValues(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClassId.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClassId.java new file mode 100644 index 0000000000..5ad4ba8249 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaClassId.java @@ -0,0 +1,202 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.Map; +import java.util.UUID; + +/** + * Schema are identified by a combination of their package name and class name. A hash value over the body of + * the schema provides a revision identifier. The class SchemaClassId represents this Schema identifier. + * <p> + * If the hash value is not supplied, then the value of the hash string will be set to None. This will be the + * case when a SchemaClass is being dynamically constructed, and a proper hash is not yet available. + * + * @author Fraser Adams + */ +public final class SchemaClassId extends QmfData +{ + private String _packageName = ""; + private String _className = ""; + private String _type = ""; + private UUID _hash = null; + + /** + * The main constructor, taking a java.util.Map as a parameter. + * + * @param m the map used to construct the SchemaClassId. + */ + public SchemaClassId(final Map m) + { + super(m); + _packageName = getStringValue("_package_name"); + _className = getStringValue("_class_name"); + _type = getStringValue("_type"); + _hash = hasValue("_hash") ? (UUID)getValue("_hash") : null; + } + + /** + * Construct a SchemaClassId from a given class name. + * + * @param className the class name. + */ + public SchemaClassId(final String className) + { + this(null, className, null, null); + } + + /** + * Construct a SchemaClassId from a given package name and class name. + * + * @param packageName the package name. + * @param className the class name. + */ + public SchemaClassId(final String packageName, final String className) + { + this(packageName, className, null, null); + } + + /** + * Construct a SchemaClassId from a given package name and class name and type (_data or _event). + * + * @param packageName the package name. + * @param className the class name. + * @param type the schema type (_data or _event). + */ + public SchemaClassId(final String packageName, final String className, final String type) + { + this(packageName, className, type, null); + } + + /** + * Construct a SchemaClassId from a given package name and class name, type (_data or _event) and hash. + * + * @param packageName the package name. + * @param className the class name. + * @param type the schema type (_data or _event). + * @param hash a UUID representation of the md5 hash of the Schema, + */ + public SchemaClassId(final String packageName, final String className, final String type, final UUID hash) + { + if (packageName != null) + { + setValue("_package_name", packageName); + _packageName = packageName; + } + + if (className != null) + { + setValue("_class_name", className); + _className = className; + } + + if (type != null) + { + setValue("_type", type); + _type = type; + } + + if (hash != null) + { + setValue("_hash", hash); + _hash = hash; + } + } + + /** + * Return The name of the associated package. + * @return The name of the associated package. Returns empty String if there's no package name. + */ + public String getPackageName() + { + return _packageName; + } + + /** + * Return The name of the class within the package. + * @return The name of the class within the package. Returns empty String if there's no class name. + */ + public String getClassName() + { + return _className; + } + + /** + * Return The type of schema, either "_data" or "_event". + * @return The type of schema, either "_data" or "_event". Returns empty String if type is unknown. + */ + public String getType() + { + return _type; + } + + /** + * Return The MD5 hash of the schema. + * @return The MD5 hash of the schema, in the format "%08x-%08x-%08x-%08x" + */ + public UUID getHashString() + { + return _hash; + } + + /** + * Compares two SchemaClassId objects for equality. + * @param rhs the right hands side SchemaClassId in the comparison. + * @return true if the two SchemaClassId objects are equal otherwise returns false. + */ + @Override + public boolean equals(final Object rhs) + { + if (rhs instanceof SchemaClassId) + { + SchemaClassId that = (SchemaClassId)rhs; + String lvalue = _packageName + _className + _hash; + String rvalue = that._packageName + that._className + that._hash; + return lvalue.equals(rvalue); + } + return false; + } + + /** + * Returns the SchemaClassId hashCode. + * @return the SchemaClassId hashCode. + */ + @Override + public int hashCode() + { + String lvalue = _packageName + _className + _hash; + return lvalue.hashCode(); + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("_package_name: " + getPackageName()); + System.out.println("_class_name: " + getClassName()); + System.out.println("_type: " + getType()); + System.out.println("_hash: " + getHashString()); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaEventClass.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaEventClass.java new file mode 100644 index 0000000000..c563c63362 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaEventClass.java @@ -0,0 +1,266 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Subclass of SchemaClass + * <p> + * Agent applications may dynamically construct instances of these objects by adding properties at run + * time. However, once the Schema is made public, it must be considered immutable, as the hash value + * must be constant once the Schema is in use. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that the + * properties are represented by an unordered map of SchemaProperty entries indexed by property name, however + * these are actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY elements that describe the + * schema event's properties. + * <p> + * In this implementation getProperties() returns a {@code List<SchemaProperty>} reflecting the reality of the protocol + * rather than what is suggested by the API documentation. + * + * @author Fraser Adams + */ +public final class SchemaEventClass extends SchemaClass +{ + private List<SchemaProperty> _properties = new ArrayList<SchemaProperty>(); + + /** + * The main constructor, taking a java.util.Map as a parameter. + * + * @param m the map used to construct the SchemaEventClass + */ + public SchemaEventClass(final Map m) + { + super(m); + if (m != null) + { + List<Map> mapEncodedProperties = this.<List<Map>>getValue("_properties"); + if (mapEncodedProperties != null) + { // In theory this shouldn't be null as "_properties" property of SchemaClass is not optional but..... + for (Map property : mapEncodedProperties) + { + addProperty(new SchemaProperty(property)); + } + } + } + } + + /** + * Construct a SchemaEventClass from a package name and a class name. + * + * @param packageName the package name. + * @param className the class name. + */ + public SchemaEventClass(final String packageName, final String className) + { + setClassId(new SchemaClassId(packageName, className, "_event")); + } + + /** + * Construct a SchemaEventClass from a SchemaClassId. + * + * @param classId the SchemaClassId identifying this Schema. + */ + public SchemaEventClass(final SchemaClassId classId) + { + setClassId(new SchemaClassId(classId.getPackageName(), classId.getClassName(), "_event")); + } + + /** + * Return optional default severity of this Property. + * @return optional default severity of this Property. + */ + public long getDefaultSeverity() + { + return getLongValue("_default_severity"); + } + + /** + * Set the default severity of this Schema Event + * + * @param severity optional severity of this Schema Event. + */ + public void setDefaultSeverity(final int severity) + { + setValue("_default_severity", severity); + } + + /** + * Return optional string description of this Schema Event. + * @return optional string description of this Schema Event. + */ + public String getDesc() + { + return getStringValue("_desc"); + } + + /** + * Set the optional string description of this Schema Object. + * + * @param description optional string description of this Schema Object. + */ + public void setDesc(final String description) + { + setValue("_desc", description); + } + + /** + * Return the count of SchemaProperties in this instance. + * @return the count of SchemaProperties in this instance. + */ + public long getPropertyCount() + { + return getProperties().size(); + } + + /** + * Return Schema Object's properties. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the properties are represented by an unordered map of SchemaProperty indexed by property name however it + * is actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY elements that describe the + * schema objects's properties. In this implementation getProperties() returns a {@code List<SchemaProperty>} + * reflecting the reality of the protocol rather than what is suggested by the API documentation. + * + * @return Schema Object's properties. + */ + public List<SchemaProperty> getProperties() + { + return _properties; + } + + /** + * Return the SchemaProperty for the parameter "name". + * @param name the name of the SchemaProperty to return. + * @return the SchemaProperty for the parameter "name". + */ + public SchemaProperty getProperty(final String name) + { + for (SchemaProperty p : _properties) + { + if (p.getName().equals(name)) + { + return p; + } + } + return null; + } + + /** + * Return the SchemaProperty for the index i. + * @param i the index of the SchemaProperty to return. + * @return the SchemaProperty for the index i. + */ + public SchemaProperty getProperty(final int i) + { + return _properties.get(i); + } + + /** + * Add a new Property. + * + * @param name the name of the SchemaProperty + * @param value the SchemaProperty associated with "name" + */ + public void addProperty(final String name, final SchemaProperty value) + { + value.setValue("_name", name); + _properties.add(value); + } + + /** + * Add a new Property. + * + * @param value the SchemaProperty associated with "name" + */ + public void addProperty(final SchemaProperty value) + { + _properties.add(value); + } + + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("SchemaEventClass:"); + getClassId().listValues(); + + if (hasValue("_desc")) System.out.println("desc: " + getDesc()); + if (hasValue("_default_severity")) System.out.println("default severity: " + getDefaultSeverity()); + + if (getPropertyCount() > 0) + { + System.out.println("properties:"); + } + for (SchemaProperty p : _properties) + { + p.listValues(); + } + } + + /** + * Return the underlying map. + * <p> + * We need to convert any properties from SchemaProperty to Map. + * + * @return the underlying map. + */ + @Override + public Map<String, Object> mapEncode() + { + // I think a "_methods" property is mandatory for a SchemaClass even if it's empty + setValue("_methods", Collections.EMPTY_LIST); + List<Map> mapEncodedProperties = new ArrayList<Map>(); + for (SchemaProperty p : _properties) + { + mapEncodedProperties.add(p.mapEncode()); + } + setValue("_properties", mapEncodedProperties); + setValue("_schema_id", getClassId().mapEncode()); + return super.mapEncode(); + } + + /** + * Generate the partial hash for the schema fields in this class. + * @param md5 the MessageDigest to be updated. + */ + @Override + protected void updateHash(MessageDigest md5) + { + super.updateHash(md5); + + for (SchemaProperty p : _properties) + { + p.updateHash(md5); + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaMethod.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaMethod.java new file mode 100644 index 0000000000..c1db746851 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaMethod.java @@ -0,0 +1,275 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The SchemaMethod class describes a method call's parameter list. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the parameter list is represented by an unordered map of SchemaProperty entries indexed by parameter name, + * however is is actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY elements that describe + * the method's arguments". + * <p> + * In this implementation getArguments() returns a {@code List<SchemaProperty>} reflecting the reality of the protocol + * rather than what is suggested by the API documentation. + * + * @author Fraser Adams + */ +public final class SchemaMethod extends QmfData +{ + private List<SchemaProperty> _arguments = new ArrayList<SchemaProperty>(); + + /** + * The main constructor, taking a java.util.Map as a parameter. + * + * @param m the map used to construct the SchemaMethod. + */ + public SchemaMethod(final Map m) + { + super(m); + if (m != null) + { + List<Map> mapEncodedArguments = this.<List<Map>>getValue("_arguments"); + if (mapEncodedArguments != null) + { // In theory this shouldn't be null as "_arguments" property of SchemaMethod is not optional but..... + for (Map argument : mapEncodedArguments) + { + addArgument(new SchemaProperty(argument)); + } + } + } + } + + /** + * Construct a SchemaMethod from its name. + * + * @param name the name of the SchemaMethod. + */ + public SchemaMethod(final String name) + { + this(name, null); + } + + /** + * Construct a SchemaMethod from its name and description. + * + * @param name the name of the SchemaMethod. + * @param description a description of the SchemaMethod. + */ + public SchemaMethod(final String name, final String description) + { + setValue("_name", name); + + if (description != null) + { + setValue("_desc", description); + } + } + + /** + * Construct a SchemaMethod from a map of "name":{@code <SchemaProperty>} entries and description. + * + * Note this Constructor is the one given in the QMF2 API specification at + * <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a>Note too that this method does not + * set a name so setName() needs to be called explicitly by clients after construction. + * + * @param args a Map of "name":{@code <SchemaProperty>} entries. + * @param description a description of the SchemaMethod. + */ + public SchemaMethod(final Map<String, SchemaProperty> args, final String description) + { + if (description != null) + { + setValue("_desc", description); + } + + for (Map.Entry<String, SchemaProperty> entry : args.entrySet()) + { + addArgument(entry.getKey(), entry.getValue()); + } + } + + /** + * Return the method's name. + * @return the method's name. + */ + public String getName() + { + return getStringValue("_name"); + } + + /** + * Sets the method's name. + * @param name the method's name. + */ + public void setName(final String name) + { + setValue("_name", name); + } + + /** + * Return a description of the method. + * @return a description of the method. + */ + public String getDesc() + { + return getStringValue("_desc"); + } + + /** + * Return the number of arguments for this method. + * @return the number of arguments for this method. + */ + public int getArgumentCount() + { + return getArguments().size(); + } + + /** + * Return the Method's arguments. + *<p> + * <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the parameter list is represented by an unordered map of SchemaProperty entries indexed by parameter name, + * however is is actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY elements that describe + * the method's arguments". In this implementation getArguments() returns a {@code List<SchemaProperty>} reflecting the + * reality of the protocol rather than what is suggested by the API documentation. + * + * @return the Method's arguments. + */ + public List<SchemaProperty> getArguments() + { + return _arguments; + } + + /** + * Return the argument with the name "name" as a SchemaProperty. + * @param name the name of the SchemaProperty to return. + * @return the argument with the name "name" as a SchemaProperty. + */ + public SchemaProperty getArgument(final String name) + { + for (SchemaProperty p : _arguments) + { + if (p.getName().equals(name)) + { + return p; + } + } + return null; + } + + /** + * Return the argument for the index i as a SchemaProperty. + * @param i the index of the SchemaProperty to return. + * @return the argument for the index i as a SchemaProperty. + */ + public SchemaProperty getArgument(final int i) + { + return _arguments.get(i); + } + + /** + * Add a new method argument. + * + * @param name the name of the SchemaProperty. + * @param value the SchemaProperty to add. + */ + public void addArgument(final String name, final SchemaProperty value) + { + value.setValue("_name", name); + _arguments.add(value); + } + + /** + * Add a new method argument. + * + * @param value the SchemaProperty to add. + */ + public void addArgument(final SchemaProperty value) + { + _arguments.add(value); + } + + /** + * Return the underlying map. + * + * @return the underlying map. + */ + @Override + public Map<String, Object> mapEncode() + { + List<Map> args = new ArrayList<Map>(); + for (SchemaProperty p : _arguments) + { + args.add(p.mapEncode()); + } + setValue("_arguments", args); + + return super.mapEncode(); + } + + /** + * Generate the partial hash for the schema fields in this class. + * @param md5 the MessageDigest to be updated. + */ + protected void updateHash(MessageDigest md5) + { + try + { + md5.update(getName().getBytes("UTF-8")); + md5.update(getDesc().toString().getBytes("UTF-8")); + } + catch (UnsupportedEncodingException uee) + { + } + for (SchemaProperty p : _arguments) + { + p.updateHash(md5); + } + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("SchemaMethod:"); + System.out.println("_name: " + getName()); + if (hasValue("_desc")) System.out.println("_desc: " + getDesc()); + if (getArgumentCount() > 0) + { + System.out.println("_arguments:"); + } + for (SchemaProperty p : _arguments) + { + p.listValues(); + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaObjectClass.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaObjectClass.java new file mode 100644 index 0000000000..734d040be6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaObjectClass.java @@ -0,0 +1,378 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Subclass of SchemaClass. + * <p> + * The structure of QmfData objects is formally defined by the class SchemaObjectClass. + * <p> + * Agent applications may dynamically construct instances of these objects by adding properties and methods + * at run time. However, once the Schema is made public, it must be considered immutable, as the hash value + * must be constant once the Schema is in use. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the properties and methods are represented by an unordered map of SchemaProperty or SchemaMethod entries indexed by + * property or method name, however these are actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY + * and "List of SCHEMA_METHOD" elements that describe the schema objects's properties and methods". In this + * implementation getProperties() returns a {@code List<SchemaProperty>} and getMethods() returns a {@code List<SchemaMethod>} + * reflecting the reality of the protocol rather than what is suggested by the API documentation. + * + * @author Fraser Adams + */ +public final class SchemaObjectClass extends SchemaClass +{ + private List<SchemaMethod> _methods = new ArrayList<SchemaMethod>(); + private List<SchemaProperty> _properties = new ArrayList<SchemaProperty>(); + private String[] _idNames = {}; + + /** + * The main constructor, taking a java.util.Map as a parameter. + * + * @param m the map used to construct the SchemaObjectClass. + */ + public SchemaObjectClass(final Map m) + { + super(m); + if (m != null) + { + List<Map> mapEncodedMethods = this.<List<Map>>getValue("_methods"); + if (mapEncodedMethods != null) + { // In theory this shouldn't be null as "_methods" property of SchemaClass is not optional but..... + for (Map method : mapEncodedMethods) + { + addMethod(new SchemaMethod(method)); + } + } + + List<Map> mapEncodedProperties = this.<List<Map>>getValue("_properties"); + if (mapEncodedProperties != null) + { // In theory this shouldn't be null as "_properties" property of SchemaClass is not optional but..... + for (Map property : mapEncodedProperties) + { + addProperty(new SchemaProperty(property)); + } + } + } + } + + /** + * Construct a SchemaObjectClass from a package name and a class name. + * + * @param packageName the package name. + * @param className the class name. + */ + public SchemaObjectClass(final String packageName, final String className) + { + setClassId(new SchemaClassId(packageName, className, "_data")); + } + + /** + * Construct a SchemaObjectClass from a SchemaClassId. + * + * @param classId the SchemaClassId identifying this SchemaObjectClass. + */ + public SchemaObjectClass(final SchemaClassId classId) + { + setClassId(new SchemaClassId(classId.getPackageName(), classId.getClassName(), "_data")); + } + + /** + * Return optional string description of this Schema Object. + * @return optional string description of this Schema Object. + */ + public String getDesc() + { + return getStringValue("_desc"); + } + + /** + * Set the Schema Object's description. + * + * @param description optional string description of this Schema Object. + */ + public void setDesc(final String description) + { + setValue("_desc", description); + } + + /** + * Return the list of property names to use when constructing the object identifier. + * <p> + * Get the value of the list of property names to use when constructing the object identifier. + * When a QmfAgentData object is created the values of the properties specified here are used to + * create the associated ObjectId object name. + * @return the list of property names to use when constructing the object identifier. + */ + public String[] getIdNames() + { + // As it's not performance critical copy _idNames, because "return _idNames;" causes findBugs to moan. + return Arrays.copyOf(_idNames, _idNames.length); + } + + /** + * Return the count of SchemaProperties in this instance. + * @return the count of SchemaProperties in this instance. + */ + public long getPropertyCount() + { + return getProperties().size(); + } + + /** + * Return Schema Object's properties. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the properties are represented by an unordered map of SchemaProperty indexed by property name, however it + * is actually represented in the QMF2 protocol as a "List of SCHEMA_PROPERTY elements that describe the + * schema objects's properties. In this implementation getProperties() returns a {@code List<SchemaProperty>} + * reflecting the reality of the protocol rather than what is suggested by the API documentation. + * + * @return Schema Object's properties. + */ + public List<SchemaProperty> getProperties() + { + return _properties; + } + + /** + * Return the SchemaProperty for the parameter "name". + * @param name the name of the SchemaProperty to return. + * @return the SchemaProperty for the parameter "name". + */ + public SchemaProperty getProperty(final String name) + { + for (SchemaProperty p : _properties) + { + if (p.getName().equals(name)) + { + return p; + } + } + return null; + } + + /** + * Return the SchemaProperty for the index i. + * @param i the index of the SchemaProperty to return. + * @return the SchemaProperty for the index i. + */ + public SchemaProperty getProperty(final int i) + { + return _properties.get(i); + } + + /** + * Return the count of SchemaMethod's in this instance. + * @return the count of SchemaMethod's in this instance. + */ + public long getMethodCount() + { + return getMethods().size(); + } + + /** + * Return Schema Object's methods. + * <p> + * Note that <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API</a> suggests that + * the methods are represented by an unordered map of SchemaMethod indexed by method name, however it + * is actually represented in the QMF2 protocol as a "List of SCHEMA_METHOD elements that describe the + * schema objects's methods. In this implementation getMethods() returns a {@code List<SchemaMethod>} + * reflecting the reality of the protocol rather than what is suggested by the API documentation. + * + * @return Schema Object's methods. + */ + public List<SchemaMethod> getMethods() + { + return _methods; + } + + /** + * Return the SchemaMethod for the parameter "name". + * @param name the name of the SchemaMethod to return. + * @return the SchemaMethod for the parameter "name". + */ + public SchemaMethod getMethod(final String name) + { + for (SchemaMethod m : _methods) + { + if (m.getName().equals(name)) + { + return m; + } + } + return null; + } + + /** + * Return the SchemaMethod for the index i. + * @param i the index of the SchemaMethod to return. + * @return the SchemaMethod for the index i. + */ + public SchemaMethod getMethod(final int i) + { + return _methods.get(i); + } + + /** + * Add a new Property. + * + * @param name the name of the SchemaProperty. + * @param value the SchemaProperty associated with "name". + */ + public void addProperty(final String name, final SchemaProperty value) + { + value.setValue("_name", name); + _properties.add(value); + } + + /** + * Add a new Property. + * + * @param value the SchemaProperty associated with "name". + */ + public void addProperty(final SchemaProperty value) + { + _properties.add(value); + } + + /** + * Add a new Method. + * + * @param name the name of the SchemaMethod. + * @param value the SchemaMethod associated with "name". + */ + public void addMethod(final String name, final SchemaMethod value) + { + value.setValue("_name", name); + _methods.add(value); + } + + /** + * Add a new Method. + * + * @param value the SchemaMethod associated with "name". + */ + public void addMethod(final SchemaMethod value) + { + _methods.add(value); + } + + /** + * Set the value of the list of property names to use when constructing the object identifier. + * <p> + * When a QmfAgentData object is created the values of the properties specified here are used to + * create the associated ObjectId object name. + * @param idNames the list of property names to use when constructing the object identifier. + */ + public void setIdNames(final String... idNames) + { + _idNames = idNames; + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("SchemaObjectClass:"); + getClassId().listValues(); + + if (hasValue("_desc")) System.out.println("desc: " + getDesc()); + + if (getMethodCount() > 0) + { + System.out.println("methods:"); + } + for (SchemaMethod m : _methods) + { + m.listValues(); + } + + if (getPropertyCount() > 0) + { + System.out.println("properties:"); + } + for (SchemaProperty p : _properties) + { + p.listValues(); + } + } + + /** + * Return the underlying map. + * <p> + * We need to convert any methods from SchemaMethod to Map and any properties from SchemaProperty to Map + * + * @return the underlying map. + */ + @Override + public Map<String, Object> mapEncode() + { + List<Map> mapEncodedMethods = new ArrayList<Map>(); + for (SchemaMethod m : _methods) + { + mapEncodedMethods.add(m.mapEncode()); + } + setValue("_methods", mapEncodedMethods); + + List<Map> mapEncodedProperties = new ArrayList<Map>(); + for (SchemaProperty p : _properties) + { + mapEncodedProperties.add(p.mapEncode()); + } + setValue("_properties", mapEncodedProperties); + + setValue("_schema_id", getClassId().mapEncode()); + + return super.mapEncode(); + } + + /** + * Generate the partial hash for the schema fields in this class. + * @param md5 the MessageDigest to be updated. + */ + @Override + protected void updateHash(MessageDigest md5) + { + super.updateHash(md5); + + for (SchemaMethod m : _methods) + { + m.updateHash(md5); + } + + for (SchemaProperty p : _properties) + { + p.updateHash(md5); + } + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaProperty.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaProperty.java new file mode 100644 index 0000000000..80c6e3ebd0 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/SchemaProperty.java @@ -0,0 +1,364 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.Map; + +// Reuse this class as it provides a handy mechanism to parse an options String into a Map +import org.apache.qpid.messaging.util.AddressParser; + +/** + * The SchemaProperty class describes a single data item in a QmfData object. A SchemaProperty is a list of + * named attributes and their values. QMF defines a set of primitive attributes. An application can extend + * this set of attributes with application-specific attributes. + * <p> + * QMF reserved attribute names all start with the underscore character ("_"). Do not create an application-specific + * attribute with a name starting with an underscore. + * <p> + * Once instantiated, the SchemaProperty is immutable. + * <p> + * Note that there appear to be some differences between the fields mentioned in + * <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API propsal</a> and + * <a href=https://cwiki.apache.org/qpid/qmf-map-message-protocol.html>QMF2 Map Message protocol</a>. + * I've gone with what's stated in the protocol documentation as this seems more accurate, at least for Qpid 0.10 + * + * @author Fraser Adams + */ +public final class SchemaProperty extends QmfData +{ + /** + * Construct a SchemaProperty from its Map representation. + */ + public SchemaProperty(final Map m) + { + super(m); + } + + /** + * Construct a SchemaProperty from its name and type. + * + * @param name the name of the SchemaProperty. + * @param type the QmfType of the SchemaProperty. + */ + public SchemaProperty(final String name, final QmfType type) throws QmfException + { + this(name, type, null); + } + + /** + * Construct a SchemaProperty from its name, type and options. + * + * @param name the name of the SchemaProperty. + * @param type the QmfType of the SchemaProperty. + * @param options a String containing the SchemaProperty options in the form: + * <pre> + * "{<option1>: <value1>, <option2>: <value2>}". + * </pre> + * Example: + * <pre> + * "{dir:IN, unit:metre, min:1, max:5, desc: 'ladder extension range'}" + * </pre> + */ + public SchemaProperty(final String name, final QmfType type, final String options) throws QmfException + { + setValue("_name", name); + setValue("_type", type.toString()); + + if (options != null && options.length() != 0) + { + Map optMap = new AddressParser(options).map(); + + if (optMap.containsKey("index")) + { + String value = optMap.get("index").toString(); + setValue("_index", Boolean.valueOf(value)); + } + + if (optMap.containsKey("access")) + { + String value = (String)optMap.get("access"); + if (value.equals("RC") || value.equals("RO") || value.equals("RW")) + { + setValue("_access", value); + } + else + { + throw new QmfException("Invalid value for 'access' option. Expected RC, RO, or RW"); + } + } + + if (optMap.containsKey("unit")) + { + String value = (String)optMap.get("unit"); + setValue("_unit", value); + } + + if (optMap.containsKey("min")) + { // Slightly odd way of parsing, but AddressParser stores integers as Integer, which seems OK but it limits + // things like queue length to 2GB. I think this should change so this code deliberately avoids assuming + // integers are encoded as Integer by using the String representation instead. + long value = Long.parseLong(optMap.get("min").toString()); + setValue("_min", value); + } + + if (optMap.containsKey("max")) + { // Slightly odd way of parsing, but AddressParser stores integers as Integer. which seems OK but it limits + // things like queue length to 2GB. I think this should change so this code deliberately avoids assuming + // integers are encoded as Integer by using the String representation instead. + long value = Long.parseLong(optMap.get("max").toString()); + setValue("_max", value); + } + + if (optMap.containsKey("maxlen")) + { // Slightly odd way of parsing, but AddressParser stores integers as Integer, which seems OK but it limits + // things like queue length to 2GB. I think this should change so this code deliberately avoids assuming + // integers are encoded as Integer by using the String representation instead. + long value = Long.parseLong(optMap.get("maxlen").toString()); + setValue("_maxlen", value); + } + + if (optMap.containsKey("desc")) + { + String value = (String)optMap.get("desc"); + setValue("_desc", value); + } + + if (optMap.containsKey("dir")) + { + String value = (String)optMap.get("dir"); + if (value.equals("IN")) + { + setValue("_dir", "I"); + } + else if (value.equals("OUT")) + { + setValue("_dir", "O"); + } + else if (value.equals("INOUT")) + { + setValue("_dir", "IO"); + } + else + { + throw new QmfException("Invalid value for 'dir' option. Expected IN, OUT, or INOUT"); + } + } + + if (optMap.containsKey("subtype")) + { + String value = (String)optMap.get("subtype"); + setValue("_subtype", value); + } + } + } + + /** + * Return the property's name. + * @return the property's name. + */ + public String getName() + { + return getStringValue("_name"); + } + + /** + * Return the property's QmfType. + * @return the property's QmfType. + */ + public QmfType getType() + { + return QmfType.valueOf(getStringValue("_type")); + } + + /** + * Return true iff this property is an index of an object. + * @return true iff this property is an index of an object. Default is false. + */ + public boolean isIndex() + { + return hasValue("_index") ? getBooleanValue("_index") : false; + } + + /** + * Return true iff this property is optional. + * @return true iff this property is optional. Default is false. + */ + public boolean isOptional() + { + return hasValue("_optional") ? getBooleanValue("_optional") : false; + } + + /** + * Return the property's remote access rules. + * @return the property's remote access rules "RC"=read/create, "RW"=read/write, "RO"=read only (default). + */ + public String getAccess() + { + return getStringValue("_access"); + } + + /** + * Return an annotation string describing units of measure for numeric values (optional). + * @return an annotation string describing units of measure for numeric values (optional). + */ + public String getUnit() + { + return getStringValue("_unit"); + } + + /** + * Return minimum value (optional). + * @return minimum value (optional). + */ + public long getMin() + { + return getLongValue("_min"); + } + + /** + * Return maximum value (optional). + * @return maximum value (optional). + */ + public long getMax() + { + return getLongValue("_max"); + } + + /** + * Return maximum length for string values (optional). + * @return maximum length for string values (optional). + */ + public long getMaxLen() + { + return getLongValue("_maxlen"); + } + + /** + * Return optional string description of this Property. + * @return optional string description of this Property. + */ + public String getDesc() + { + return getStringValue("_desc"); + } + + /** + * Return the direction of information travel. + * @return "I"=input, "O"=output, or "IO"=input/output (required for method arguments, otherwise optional). + */ + public String getDirection() + { + return getStringValue("_dir"); + } + + /** + * Return string indicating the formal application type. + * @return string indicating the formal application type for the data, example: "URL", "Telephone number", etc. + */ + public String getSubtype() + { + return getStringValue("_subtype"); + } + + /** + * Return a SchemaClassId. If the type is a reference to another managed object. + * @return a SchemaClassId. If the type is a reference to another managed object, this field may be used. + to specify the required class for that object + */ + public SchemaClassId getReference() + { + return new SchemaClassId((Map)getValue("_references")); + } + + /** + * Return the value of the attribute named "name". + * @return the value of the attribute named "name". + * <p> + * This method can be used to retrieve application-specific attributes. "name" should start with the prefix "x-". + */ + public String getAttribute(final String name) + { + return getStringValue(name); + } + + /** + * Generate the partial hash for the schema fields in this class. + * @param md5 the MessageDigest to be updated + */ + protected void updateHash(MessageDigest md5) + { + try + { + md5.update(getName().getBytes("UTF-8")); + md5.update(getType().toString().getBytes("UTF-8")); + md5.update(getSubtype().getBytes("UTF-8")); + md5.update(getAccess().getBytes("UTF-8")); + if (isIndex()) + { + md5.update((byte)1); + } + else + { + md5.update((byte)0); + } + if (isOptional()) + { + md5.update((byte)1); + } + else + { + md5.update((byte)0); + } + md5.update(getUnit().getBytes("UTF-8")); + md5.update(getDesc().getBytes("UTF-8")); + md5.update(getDirection().getBytes("UTF-8")); + } + catch (UnsupportedEncodingException uee) + { + } + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + System.out.println("SchemaProperty:"); + System.out.println("_name: " + getName()); + System.out.println("_type: " + getType()); + if (hasValue("_index")) System.out.println("is index: " + isIndex()); + if (hasValue("_optional")) System.out.println("is optional: " + isOptional()); + if (hasValue("_access")) System.out.println("access: " + getAccess()); + if (hasValue("_unit")) System.out.println("unit: " + getUnit()); + if (hasValue("_min")) System.out.println("min: " + getMin()); + if (hasValue("_max")) System.out.println("max: " + getMax()); + if (hasValue("_max_len")) System.out.println("maxlen: " + getMaxLen()); + if (hasValue("_desc")) System.out.println("desc: " + getDesc()); + if (hasValue("_dir")) System.out.println("dir: " + getDirection()); + if (hasValue("_subtype")) System.out.println("subtype: " + getSubtype()); + if (hasValue("_references")) System.out.println("reference: " + getReference()); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkItem.java new file mode 100644 index 0000000000..90afc70f31 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkItem.java @@ -0,0 +1,400 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +import java.util.HashMap; +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.console.Agent; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <p> + * A WorkItem describes an event that has arrived for the application to process. + * <p> + * The Notifier is invoked when one or more WorkItems become available for processing. + * <p> + * The type of the Object returned by getParams is determined by the WorkItemType and described below. + * <p> + * + * <table width="100%" border="1" summary=""><thead><tr><th>WorkItem Type</th><th>Description</th></tr></thead><tbody> + * + * + * <tr> + * <td>AGENT_ADDED:</td> + * <td> + * When the QMF Console receives the first heartbeat from an Agent, an AGENT_ADDED WorkItem is pushed onto the + * work-queue. + * <p> + * The getParams() method returns a map which contains a reference to the new Console Agent instance. The reference is + * indexed from the map using the key string "agent". There is no handle associated with this WorkItem. + * <p> + * Note: If a new Agent is discovered as a result of the Console findAgent() method, then no AGENT_ADDED WorkItem is + * generated for that Agent. + * <p> + * Use AgentAddedWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>AGENT_DELETED:</td> + * <td> + * When a known Agent stops sending heartbeat messages the Console will time out that Agent. On Agent timeout, an + * AGENT_DELETED WorkItem is pushed onto the work-queue. + * <p> + * The getParams() method returns a map which contains a reference to the Agent instance that has been deleted. + * The reference is indexed from the map using the key string "agent". There is no handle associated with this WorkItem. + * <p> + * The Console application must release all saved references to the Agent before returning the WorkItem. + * <p> + * Use AgentDeletedWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>AGENT_RESTARTED:</td> + * <td> + * Sent when the QMF Console detects an Agent was restarted, an AGENT_RESTARTED WorkItem is pushed onto the work-queue. + * <p> + * The getParams() method returns a map which contains a reference to the Console Agent instance. The reference + * is indexed from the map using the key string "agent". There is no handle associated with this WorkItem. + * <p> + * Use AgentRestartedWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>AGENT_HEARTBEAT:</td> + * <td> + * When the QMF Console receives heartbeats from an Agent, an AGENT_HEARTBEAT WorkItem is pushed onto the work-queue. + * <p> + * The getParams() method returns a map which contains a reference to the Console Agent instance. The reference + * is indexed from the map using the key string "agent". There is no handle associated with this WorkItem. + * <p> + * Note: the first heartbeat results in an AGENT_ADDED WorkItem for Agent not an AGENT_HEARTBEAT. + * <p> + * Use AgentHeartbeatWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>NEW_PACKAGE:</td><td>TBD</td> + * </tr> + * + * + * <tr> + * <td>NEW_CLASS:</td><td>TBD</td> + * </tr> + * + * + * <tr> + * <td>OBJECT_UPDATE:</td> + * <td> + * The OBJECT_UPDATE WorkItem is generated in response to an asynchronous refresh made by a QmfConsoleData object. + * <p> + * The getParams() method will return a QmfConsoleData. + * <p> + * The getHandle() method returns the reply handle provided to the refresh() method call. + * This handle is merely the handle used for the asynchronous response, it is not associated with the QmfConsoleData + * in any other way. + * <p> + * Use ObjectUpdateWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>METHOD_RESPONSE:</td> + * <td> + * The METHOD_RESPONSE WorkItem is generated in response to an asynchronous invokeMethod made by a QmfConsoleData object. + * <p> + * The getParams() method will return a MethodResult object. + * <p> + * The getHandle() method returns the reply handle provided to the refresh() method call. + * This handle is merely the handle used for the asynchronous response, it is not associated with the QmfConsoleData + * in any other way. + * <p> + * Use MethodResponseWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>EVENT_RECEIVED:</td> + * <td> + * When an Agent generates a QmfEvent an EVENT_RECEIVED WorkItem is pushed onto the work-queue. + * <p> + * The getParams() method returns a map which contains a reference to the Console Agent instance that generated + * the Event and a reference to the QmfEvent itself. The Agent reference is indexed from the map using the key string + * "agent, The QmfEvent reference is indexed from the map using the key string "event". There is no handle associated + * with this WorkItem. + * <p> + * Use EventReceivedWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>SUBSCRIBE_RESPONSE:</td> + * <td> + * The SUBSCRIBE_RESPONSE WorkItem returns the result of a subscription request made by this Console. This WorkItem is + * generated when the Console's createSubscription() is called in an asychronous manner, rather than pending for the result. + * <p> + * The getParams() method will return an instance of the SubscribeParams class. + * <p> + * The SubscriptionId object must be used when the subscription is refreshed or cancelled. It must be passed to the + * Console's refresh_subscription() and cancelSubscription() methods. The value of the SubscriptionId does not change + * over the lifetime of the subscription. + * <p> + * The console handle will be provided by the Agent on each data indication event that corresponds to this subscription. + * It should not change for the lifetime of the subscription. + * <p> + * The getHandle() method returns the reply handle provided to the createSubscription() method call. This handle is + * merely the handle used for the asynchronous response, it is not associated with the subscription in any other way. + * <p> + * Once a subscription is created, the Agent that maintains the subscription will periodically issue updates for the + * subscribed data. This update will contain the current values of the subscribed data, and will appear as the first + * SUBSCRIPTION_INDICATION WorkItem for this subscription. + * <p> + * Use SubscribeResponseWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>SUBSCRIPTION_INDICATION:</td> + * <td> + * The SUBSCRIPTION_INDICATION WorkItem signals the arrival of an update to subscribed data from the Agent. + * <p> + * The getParams() method will return an instance of the SubscribeIndication class. + * The getHandle() method returns null. + * <p> + * Use SubscriptionIndicationWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>RESUBSCRIBE_RESPONSE:</td> + * <td> + * The RESUBSCRIBE_RESPONSE WorkItem is generated in response to a subscription refresh request made by this Console. + * This WorkItem is generated when the Console's refreshSubscription() is called in an asychronous manner, rather than + * pending for the result. + * <p> + * The getParams() method will return an instance of the SubscribeParams class. + * <p> + * The getHandle() method returns the reply handle provided to the refreshSubscription() method call. This handle is + * merely the handle used for the asynchronous response, it is not associated with the subscription in any other way. + * <p> + * Use ResubscribeResponseWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>METHOD_CALL:</td> + * <td> + * The METHOD_CALL WorkItem describes a method call that must be serviced by the application on behalf of this Agent. + * <p> + * The getParams() method will return an instance of the MethodCallParams class. + * <p> + * Use MethodCallWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>QUERY:</td> + * <td> + * The QUERY WorkItem describes a query that the application must service. The application should call the + * queryResponse() method for each object that satisfies the query. When complete, the application must call the + * queryComplete() method. If a failure occurs, the application should indicate the error to the agent by calling + * the query_complete() method with a description of the error. + * <p> + * The getParams() method will return an instance of the QmfQuery class. + * <p> + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's queryResponse() + * and queryComplete() methods. + * <p> + * Use QueryWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>SUBSCRIBE_REQUEST:</td> + * <td> + * The SUBSCRIBE_REQUEST WorkItem provides a query that the agent application must periodically publish until the + * subscription is cancelled or expires. On receipt of this WorkItem, the application should call the Agent + * subscriptionResponse() method to acknowledge the request. On each publish interval, the application should call Agent + * subscriptionIndicate(), passing a list of the objects that satisfy the query. The subscription remains in effect until + * an UNSUBSCRIBE_REQUEST WorkItem for the subscription is received, or the subscription expires. + * <p> + * The getParams() method will return an instance of the SubscriptionParams class. + * <p> + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's + * subscriptionResponse() method. + * <p> + * Use SubscribeRequestWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>RESUBSCRIBE_REQUEST:</td> + * <td> + * The RESUBSCRIBE_REQUEST is sent by a Console to renew an existing subscription. The Console may request a new + * duration for the subscription, otherwise the previous lifetime interval is repeated. + * <p> + * The getParams() method will return an instance of the ResubscribeParams class. + * <p> + * The getHandle() WorkItem method returns the reply handle which should be passed to the Agent's + * subscriptionResponse() method. + * <p> + * Use ResubscribeRequestWorkItem to enable neater access. + * </td> + * </tr> + * + * + * <tr> + * <td>UNSUBSCRIBE_REQUEST:</td> + * <td> + * The UNSUBSCRIBE_REQUEST is sent by a Console to terminate an existing subscription. The Agent application should + * terminate the given subscription if it exists, and cancel sending any further updates against it. + * <p> + * The getParams() method will return a String holding the subscriptionId. + * <p> + * The getHandle() method returns null. + * <p> + * Use UnsubscribeRequestWorkItem to enable neater access. + * </td> + * </tr> + * </tbody> + * </table> + * <p> + * The following diagram illustrates the QMF2 WorkItem class hierarchy. + * <p> + * <img alt="" src="doc-files/WorkItem.png"> + * @author Fraser Adams + */ + +public class WorkItem +{ + /** + * An Enumeration of the types of WorkItems produced on the Console or Agent. + */ + public enum WorkItemType + { + // Enumeration of the types of WorkItems produced on the Console + AGENT_ADDED, + AGENT_DELETED, + AGENT_RESTARTED, + AGENT_HEARTBEAT, + NEW_PACKAGE, + NEW_CLASS, + OBJECT_UPDATE, + EVENT_RECEIVED, + METHOD_RESPONSE, + SUBSCRIBE_RESPONSE, + SUBSCRIPTION_INDICATION, + RESUBSCRIBE_RESPONSE, + // Enumeration of the types of WorkItems produced on the Agent + METHOD_CALL, + QUERY, + SUBSCRIBE_REQUEST, + RESUBSCRIBE_REQUEST, + UNSUBSCRIBE_REQUEST; + } + + private final WorkItemType _type; + private final Handle _handle; + private final Object _params; + + /** + * Construct a WorkItem + * + * @param type the type of WorkItem specified by the WorkItemType enum + * @param handle the handle passed by async calls - the correlation ID + * @param params the payload of the WorkItem + */ + public WorkItem(WorkItemType type, Handle handle, Object params) + { + _type = type; + _handle = handle; + _params = params; + } + + /** + * Return the type of work item. + * @return the type of work item. + */ + public final WorkItemType getType() + { + return _type; + } + + /** + * Return the reply handle for an asynchronous operation, if present. + * @return the reply handle for an asynchronous operation, if present. + */ + public final Handle getHandle() + { + return _handle; + } + + /** + * Return the payload of the work item. + * @return the payload of the work item. + * <p> + * The type of this object is determined by the type of the workitem as follows: + * <pre> + * <b>Console</b> + * AGENT_ADDED: Map{"agent":Agent} - Use AgentAddedWorkItem to enable neater access + * AGENT_DELETED: Map{"agent":Agent} - Use AgentDeletedWorkItem to enable neater access + * AGENT_RESTARTED: Map{"agent":Agent} - Use AgentRestartedWorkItem to enable neater access + * AGENT_HEARTBEAT: Map{"agent":Agent} - Use AgentHeartbeatWorkItem to enable neater access + * OBJECT_UPDATE: QmfConsoleData - Use ObjectUpdateWorkItem to enable neater access + * METHOD_RESPONSE: MethodResult - Use MethodResponseWorkItem to enable neater access + * EVENT_RECEIVED: Map{"agent":Agent, "event":QmfEvent} - Use EventReceivedWorkItem to enable neater access + * SUBSCRIBE_RESPONSE: SubscribeParams - Use SubscribeResponseWorkItem to enable neater access + * SUBSCRIPTION_INDICATION: SubscribeIndication - Use SubscriptionIndicationWorkItem to enable neater access + * RESUBSCRIBE_RESPONSE: SubscribeParams - Use ResubscribeResponseWorkItem to enable neater access + * + * <b>Agent</b> + * METHOD_CALL: MethodCallParams - Use MethodCallWorkItem to enable neater access + * QUERY: QmfQuery - Use QueryWorkItem to enable neater access + * SUBSCRIBE_REQUEST: SubscriptionParams - Use SubscribeRequestWorkItem to enable neater access + * RESUBSCRIBE_REQUEST: ResubscribeParams - Use ResubscribeRequestWorkItem to enable neater access + * UNSUBSCRIBE_REQUEST: String (subscriptionId) - Use UnsubscribeRequestWorkItem to enable neater access + * </pre> + */ + @SuppressWarnings("unchecked") + public final <T> T getParams() + { + return (T)_params; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkQueue.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkQueue.java new file mode 100644 index 0000000000..75b4e5d855 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/WorkQueue.java @@ -0,0 +1,106 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.common; + +// Misc Imports +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * This is an implementation of a QMF2 WorkQueue. In practice this is likely to be used by an Agent or Console. + * + * @author Fraser Adams + */ +public class WorkQueue +{ + /** + * Used to implement a thread safe queue of WorkItem objects + */ + private BlockingQueue<WorkItem> _workQueue = new LinkedBlockingQueue<WorkItem>(); + + /** + * Return the count of pending WorkItems that can be retrieved. + * @return the count of pending WorkItems that can be retrieved. + */ + public int size() + { + return _workQueue.size(); + } + + /** + * Obtains the next pending work item - blocking version + * + * @return the next pending work item, or null if none available. + */ + public WorkItem getNextWorkitem() + { + try + { + return _workQueue.take(); + } + catch (InterruptedException ie) + { + return null; + } + } + + /** + * Obtains the next pending work item - balking version + * + * @param timeout the timeout in seconds. If timeout = 0 it returns immediately with either a WorkItem or null + * @return the next pending work item, or null if none available. + */ + public WorkItem getNextWorkitem(long timeout) + { + try + { + return _workQueue.poll(timeout, TimeUnit.SECONDS); + } + catch (InterruptedException ie) + { + return null; + } + } + + /** + * Adds a WorkItem to the WorkQueue. + * + * @param item the WorkItem passed to the WorkQueue + */ + public void addWorkItem(WorkItem item) + { + // We wrap the blocking put() method in a loop "just in case" InterruptedException occurs + // if it does we retry the put otherwise we carry on, notify then exit. + while (true) + { + try + { + _workQueue.put(item); + break; + } + catch (InterruptedException ie) + { + continue; + } + } + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Console.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Console.png Binary files differnew file mode 100644 index 0000000000..cb2a5ee800 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Console.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfData.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfData.png Binary files differnew file mode 100644 index 0000000000..2665803e39 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfData.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfEventListenerModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfEventListenerModel.png Binary files differnew file mode 100644 index 0000000000..26a5f71b56 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfEventListenerModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfQuery.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfQuery.png Binary files differnew file mode 100644 index 0000000000..9e471a08c0 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/QmfQuery.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Schema.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Schema.png Binary files differnew file mode 100644 index 0000000000..b0277f4fc5 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Schema.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Subscriptions.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Subscriptions.png Binary files differnew file mode 100644 index 0000000000..977e222129 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/Subscriptions.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkItem.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkItem.png Binary files differnew file mode 100644 index 0000000000..14c8c56d28 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkItem.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkQueueEventModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkQueueEventModel.png Binary files differnew file mode 100644 index 0000000000..fc2a722985 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/common/doc-files/WorkQueueEventModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Agent.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Agent.java new file mode 100644 index 0000000000..26c80df809 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Agent.java @@ -0,0 +1,482 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +// Misc Imports +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.SchemaClass; +import org.apache.qpid.qmf2.common.SchemaClassId; + +/** + * Local representation (proxy) of a remote Agent. + * <p> + * This class holds some state that relates to the Agent and in addition some methods may be called on the agent. + * destroy(), invokeMethod() and refresh() are actually called by a proxy class AgentProxy. AgentProxy is actually + * an interface that is implemented by Console (as that's where all the JMS stuff is), we use this approach to + * avoid introducing a circular dependency between Agent and Console. + * <p> + * The Console application maintains a list of all known remote Agents. + * Each Agent is represented by an instance of the Agent class: + * <p> + * The following diagram illustrates the interactions between the Console, AgentProxy and the client side Agent + * representation. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * @author Fraser Adams + */ +public final class Agent extends QmfData +{ + private AgentProxy _proxy; + private List<String> _packages = new ArrayList<String>(); + private Map<SchemaClassId, SchemaClass> _schemaCache = new ConcurrentHashMap<SchemaClassId, SchemaClass>(); + private long _epoch; + private long _heartbeatInterval; + private long _timestamp; + private boolean _eventsEnabled = true; + private boolean _isActive = true; + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the SchemaClass + * @param p the AgentProxy instance that implements some of the concrete behaviour of the local Agent representation. + */ + public Agent(final Map m, final AgentProxy p) + { + super(m); + // Populate attributes translating any old style keys if necessary. + _epoch = hasValue("_epoch") ? getLongValue("_epoch") : getLongValue("epoch"); + _heartbeatInterval = hasValue("_heartbeat_interval") ? getLongValue("_heartbeat_interval") : + getLongValue("heartbeat_interval"); + _timestamp = hasValue("_timestamp") ? getLongValue("_timestamp") : getLongValue("timestamp"); + _proxy = p; + } + + /** + * Sets the state of the Agent, used as an assignment operator. + * + * @param m the Map used to initialise the Agent. + */ + @SuppressWarnings("unchecked") + public void initialise(final Map m) + { + Map<String, Object> values = (Map<String, Object>)m.get("_values"); + _values = (values == null) ? m : values; + + // Populate attributes translating any old style keys if necessary. + _epoch = hasValue("_epoch") ? getLongValue("_epoch") : getLongValue("epoch"); + _heartbeatInterval = hasValue("_heartbeat_interval") ? getLongValue("_heartbeat_interval") : + getLongValue("heartbeat_interval"); + _timestamp = hasValue("_timestamp") ? getLongValue("_timestamp") : getLongValue("timestamp"); + } + + /** + * Return whether or not events are enabled for this Agent. + * @return a boolean indication of whether or not events are enabled for this Agent. + */ + public boolean eventsEnabled() + { + return _eventsEnabled; + } + + /** + * Deactivated this Agent. Called by the Console when the Agent times out. + */ + public void deactivate() + { + _isActive = false; + } + + /** + * Return the Agent instance name. + * @return the Agent instance name. + */ + public String getInstance() + { + return getStringValue("_instance"); + } + + /** + * Return the identifying name string of the Agent. + * @return the identifying name string of the Agent. This name is used to send AMQP messages directly to this agent. + */ + public String getName() + { + return getStringValue("_name"); + } + + /** + * Return the product name string of the Agent. + * @return the product name string of the Agent. + */ + public String getProduct() + { + return getStringValue("_product"); + } + + /** + * Return the Agent vendor name. + * @return the Agent vendor name. + */ + public String getVendor() + { + return getStringValue("_vendor"); + } + + /** + * Return the Epoch stamp. + * @return the Epoch stamp, used to determine if an Agent has been restarted. + */ + public long getEpoch() + { + return _epoch; + } + + /** + * Set the Epoch stamp. + * @param epoch the new Epoch stamp, used to indicate that an Agent has been restarted. + */ + public void setEpoch(long epoch) + { + _epoch = epoch; + } + + /** + * Return the time that the Agent waits between sending hearbeat messages. + * @return the time that the Agent waits between sending hearbeat messages. + */ + public long getHeartbeatInterval() + { + return _heartbeatInterval; + } + + /** + * Return the timestamp of the Agent's last update. + * @return the timestamp of the Agent's last update. + */ + public long getTimestamp() + { + return _timestamp; + } + + /** + * Return true if the agent is alive. + * @return true if the agent is alive (heartbeats have not timed out). + */ + public boolean isActive() + { + return _isActive; + } + + /** + * Request that the Agent updates the value of this object's contents. + * + * @param objectId the ObjectId being queried for.. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + * @param timeout the maximum time to wait for a response, overrides default replyTimeout. + * @return the refreshed object. + */ + public QmfConsoleData refresh(final ObjectId objectId, final String replyHandle, final int timeout) throws QmfException + { + if (isActive()) + { + return _proxy.refresh(this, objectId, replyHandle, timeout); + } + else + { + throw new QmfException("Agent.refresh() called from deactivated Agent"); + } + } + + /** + * Helper method to create a Map containing a QMF method request. + * + * @param objectId the objectId of the remote object. + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @return a Map containing a QMF method request. + */ + private Map<String, Object> createRequest(final ObjectId objectId, final String name, final QmfData inArgs) + { + // Default sizes for HashMap should be fine for request + Map<String, Object> request = new HashMap<String, Object>(); + if (objectId != null) + { + request.put("_object_id", objectId.mapEncode()); + } + request.put("_method_name", name); + if (inArgs != null) + { + request.put("_arguments", inArgs.mapEncode()); + if (inArgs.getSubtypes() != null) + { + request.put("_subtypes", inArgs.getSubtypes()); + } + } + return request; + } + + /** + * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the + * AgentProxy that knows about connections, sessions and messages. + * + * @param objectId the objectId of the remote object. + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @param timeout the maximum time to wait for a response, overrides default replyTimeout. + * @return the MethodResult. + */ + protected MethodResult invokeMethod(final ObjectId objectId, final String name, + final QmfData inArgs, final int timeout) throws QmfException + { + if (isActive()) + { + return _proxy.invokeMethod(this, createRequest(objectId, name, inArgs), null, timeout); + } + else + { + throw new QmfException("Agent.invokeMethod() called from deactivated Agent"); + } + } + + /** + * Sends an asynchronous method request to the Agent. Delegates to the AgentProxy to actually send the method as + * it's the AgentProxy that knows about connections, sessions and messages. + * + * @param objectId the objectId of the remote object. + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + */ + protected void invokeMethod(final ObjectId objectId, final String name, + final QmfData inArgs, final String replyHandle) throws QmfException + { + if (isActive()) + { + _proxy.invokeMethod(this, createRequest(objectId, name, inArgs), replyHandle, -1); + } + else + { + throw new QmfException("Agent.invokeMethod() called from deactivated Agent"); + } + } + + /** + * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the + * AgentProxy that knows about connections, sessions and messages. + * + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @return the MethodResult. + */ + public MethodResult invokeMethod(final String name, final QmfData inArgs) throws QmfException + { + return invokeMethod(null, name, inArgs, -1); + } + + /** + * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the + * AgentProxy that knows about connections, sessions and messages. + * + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @param timeout the maximum time to wait for a response, overrides default replyTimeout. + * @return the MethodResult. + */ + public MethodResult invokeMethod(final String name, final QmfData inArgs, final int timeout) throws QmfException + { + return invokeMethod(null, name, inArgs, timeout); + } + + /** + * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the + * AgentProxy that knows about connections, sessions and messages. + * + * @param name the remote method name. + * @param inArgs the formal parameters of the remote method name. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + */ + public void invokeMethod(final String name, final QmfData inArgs, final String replyHandle) throws QmfException + { + invokeMethod(null, name, inArgs, replyHandle); + } + + /** + * Remove a Subscription. Delegates to the AgentProxy to actually remove the Subscription as it's the AgentProxy + * that really knows about subscriptions. + * + * @param subscription the SubscriptionManager that we wish to remove. + */ + public void removeSubscription(final SubscriptionManager subscription) + { + _proxy.removeSubscription(subscription); + } + + /** + * Allows reception of events from this agent. + */ + public void enableEvents() + { + _eventsEnabled = true; + } + + /** + * Prevents reception of events from this agent. + */ + public void disableEvents() + { + _eventsEnabled = false; + } + + /** + * Releases this Agent instance. Once called, the Console application should not reference this instance again. + */ + public void destroy() + { + _timestamp = 0; + _proxy.destroy(this); + } + + /** + * Clears the internally cached schema. Generally done when we wich to refresh the schema information from the + * remote Agent. + */ + public void clearSchemaCache() + { + _schemaCache.clear(); + _packages.clear(); + } + + /** + * Stores the schema and package information obtained by querying the remote Agent. + * + * @param classes the list of SchemaClassIds obtained by querying the remote Agent. + */ + public void setClasses(final List<SchemaClassId> classes) + { + if (classes == null) + { + clearSchemaCache(); + return; + } + + for (SchemaClassId classId : classes) + { + _schemaCache.put(classId, SchemaClass.EMPTY_SCHEMA); + if (!_packages.contains(classId.getPackageName())) + { + _packages.add(classId.getPackageName()); + } + } + } + + /** + * Return the list of SchemaClassIds associated with this Agent. + * @return the list of SchemaClassIds associated with this Agent. + */ + public List<SchemaClassId> getClasses() + { + if (_schemaCache.size() == 0) + { + return Collections.emptyList(); + } + return new ArrayList<SchemaClassId>(_schemaCache.keySet()); + } + + /** + * Return the list of packages associated with this Agent. + * @return the list of packages associated with this Agent. + */ + public List<String> getPackages() + { + return _packages; + } + + /** + * Return the SchemaClass associated with this Agent. + * @return the list of SchemaClass associated with this Agent. + * <p> + * I <i>believe</i> that there should only be one entry in the list returned when looking up a specific chema by classId. + */ + public List<SchemaClass> getSchema(final SchemaClassId classId) + { + SchemaClass schema = _schemaCache.get(classId); + if (schema == SchemaClass.EMPTY_SCHEMA) + { + return Collections.emptyList(); + } + + List<SchemaClass> results = new ArrayList<SchemaClass>(); + results.add(schema); + return results; + } + + /** + * Set a schema keyed by SchemaClassId. + * + * @param classId the SchemaClassId indexing the particular schema. + * @param schemaList the schema being indexed. + * <p> + * I <i>believe</i> that there should only be one entry in the list returned when looking up a specific chema by classId. + */ + public void setSchema(final SchemaClassId classId, final List<SchemaClass> schemaList) + { + if (schemaList == null || schemaList.size() == 0) + { + _schemaCache.put(classId, SchemaClass.EMPTY_SCHEMA); + } + else + { + // I believe that there should only be one entry in the list returned when looking up + // a specific chema by classId + _schemaCache.put(classId, schemaList.get(0)); + } + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + System.out.println("Agent:"); + System.out.println("instance: " + getInstance()); + System.out.println("name: " + getName()); + System.out.println("product: " + getProduct()); + System.out.println("vendor: " + getVendor()); + System.out.println("epoch: " + getEpoch()); + System.out.println("heartbeatInterval: " + getHeartbeatInterval()); + System.out.println("timestamp: " + new Date(getTimestamp()/1000000l)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAccessWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAccessWorkItem.java new file mode 100644 index 0000000000..ff5668fe2e --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAccessWorkItem.java @@ -0,0 +1,80 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.HashMap; +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.QmfEvent; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Abstract class that acts as a superclass for all WorkItems that need to set and retrieve an Agent. + * <p> + * This class is a convenience class to enable neater access the the WorkItem params for this type of WorkItem. + * + * @author Fraser Adams + */ + +public abstract class AgentAccessWorkItem extends WorkItem +{ + /** + * Helper method to create the WorkItem params as a Map. + * + * @param agent the Agent associated with the WorkItem. + * @param event the QmfEvent associated with the WorkItem. + */ + protected static Map<String, Object> newParams(final Agent agent, final QmfEvent event) + { + Map<String, Object> params = new HashMap<String, Object>(); + params.put("agent", agent); + if (event != null) + { + params.put("event", event); + } + return params; + } + + /** + * Construct an AgentAccessWorkItem. Convenience constructor not in API + * + * @param type the type of WorkItem specified by the WorkItemType enum + * @param handle the handle passed by async calls - the correlation ID + * @param params the payload of the WorkItem + */ + public AgentAccessWorkItem(final WorkItemType type, final Handle handle, final Object params) + { + super(type, handle, params); + } + + /** + * Return the Agent stored in the params Map. + * @return the Agent stored in the params Map. + */ + public final Agent getAgent() + { + Map<String, Object> p = this.<Map<String, Object>>getParams(); + return (Agent)p.get("agent"); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAddedWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAddedWorkItem.java new file mode 100644 index 0000000000..6f7b1812c6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentAddedWorkItem.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * AGENT_ADDED: When the QMF Console receives the first heartbeat from an Agent, an AGENT_ADDED WorkItem + * is pushed onto the work-queue. The WorkItem's getParam() call returns a map which contains + * a reference to the new Console Agent instance. The reference is indexed from the map using + * the key string "agent". There is no handle associated with this WorkItem. + * + * Note: If a new Agent is discovered as a result of the Console findAgent() method, then no + * AGENT_ADDED WorkItem is generated for that Agent. + * </pre> + * @author Fraser Adams + */ + +public final class AgentAddedWorkItem extends AgentAccessWorkItem +{ + /** + * Construct an AgentAddedWorkItem. Convenience constructor not in API + * + * @param agent the Agent used to populate the WorkItem's param + */ + public AgentAddedWorkItem(final Agent agent) + { + super(WorkItemType.AGENT_ADDED, null, newParams(agent, null)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentDeletedWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentDeletedWorkItem.java new file mode 100644 index 0000000000..9644d205a7 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentDeletedWorkItem.java @@ -0,0 +1,50 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * AGENT_DELETED: When a known Agent stops sending heartbeat messages, the Console will time out that Agent. + * On Agent timeout, an AGENT_DELETED WorkItem is pushed onto the work-queue. The WorkItem's + * getParam() call returns a map which contains a reference to the Agent instance that has + * been deleted. The reference is indexed from the map using the key string "agent". There is + * no handle associated with this WorkItem. + * + * The Console application must release all saved references to the Agent before returning the + * WorkItem. + * </pre> + * @author Fraser Adams + */ + +public final class AgentDeletedWorkItem extends AgentAccessWorkItem +{ + /** + * Construct an AgentDeletedWorkItem. Convenience constructor not in API + * + * @param agent the Agent used to populate the WorkItem's param + */ + public AgentDeletedWorkItem(final Agent agent) + { + super(WorkItemType.AGENT_DELETED, null, newParams(agent, null)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentHeartbeatWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentHeartbeatWorkItem.java new file mode 100644 index 0000000000..4406d96567 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentHeartbeatWorkItem.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * AGENT_HEARTBEAT: When the QMF Console receives heartbeats from an Agent, an AGENT_HEARTBEAT WorkItem + * is pushed onto the work-queue. The WorkItem's getParam() call returns a map which contains + * a reference to the Console Agent instance. The reference is indexed from the map using + * the key string "agent". There is no handle associated with this WorkItem. + * + * Note: the first heartbeat results in an AGENT_ADDED WorkItem for Agent not an AGENT_HEARTBEAT. + * </pre> + * @author Fraser Adams + */ + +public final class AgentHeartbeatWorkItem extends AgentAccessWorkItem +{ + /** + * Construct an AgentHeartbeatWorkItem. Convenience constructor not in API + * + * @param agent the Agent used to populate the WorkItem's param + */ + public AgentHeartbeatWorkItem(final Agent agent) + { + super(WorkItemType.AGENT_HEARTBEAT, null, newParams(agent, null)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentProxy.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentProxy.java new file mode 100644 index 0000000000..55eb0df7b6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentProxy.java @@ -0,0 +1,85 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +// Misc Imports +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfException; + +/** + * This interface is implemented by the Console and provides a number of "Agent" related behaviours. + * <p> + * Arguably it would be possible to implement these directly in the org.apache.qpid.qmf2.console.Agent class but as + * it happens they tend to use more Console behaviours, for example refresh() and invokeMethod() are pretty much + * about constructing the appropriate JMS Message, so have more in common with the rest of + * org.apache.qpid.qmf2.console.Console. + * <p> + * The purpose of this interface is primarily about removing a circular dependency between Console and Agent so the + * Agent doesn't invoke these methods on a Console instance, rather it invokes them on an AgentProxy instance. + * <p> + * The following diagram illustrates the interactions between the Console, AgentProxy and the client side Agent + * representation. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * @author Fraser Adams + */ +public interface AgentProxy +{ + /** + * Releases the Agent instance. Once called, the console application should not reference this instance again. + * + * @param agent the Agent to be destroyed. + */ + public void destroy(Agent agent); + + /** + * Request that the Agent update the value of an object's contents. + * + * @param agent the Agent to get the refresh from. + * @param objectId the ObjectId being queried for. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + * @param timeout the maximum time to wait for a response, overrides default replyTimeout. + * @return the refreshed object. + */ + public QmfConsoleData refresh(Agent agent, ObjectId objectId, String replyHandle, int timeout); + + /** + * Invoke the named method on the named Agent. + * + * @param agent the Agent to invoke the method on. + * @param content an unordered set of key/value pairs comprising the method arguments. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + * @param timeout the maximum time to wait for a response, overrides default replyTimeout. + * @return the MethodResult. + */ + public MethodResult invokeMethod(Agent agent, Map<String, Object> content, String replyHandle, int timeout) throws QmfException; + + /** + * Remove a Subscription. + * + * @param subscription the SubscriptionManager that we wish to remove. + */ + public void removeSubscription(SubscriptionManager subscription); +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentRestartedWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentRestartedWorkItem.java new file mode 100644 index 0000000000..25bd03654a --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/AgentRestartedWorkItem.java @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * AGENT_RESTARTED: Sent when the QMF Console detects an Agent was restarted, an AGENT_RESTARTED WorkItem + * is pushed onto the work-queue. The WorkItem's getParam() call returns a map which contains + * a reference to the Console Agent instance. The reference is indexed from the map using + * the key string "agent". There is no handle associated with this WorkItem. + * </pre> + * @author Fraser Adams + */ + +public final class AgentRestartedWorkItem extends AgentAccessWorkItem +{ + /** + * Construct an AgentRestartedWorkItem. Convenience constructor not in API + * + * @param agent the Agent used to populate the WorkItem's param + */ + public AgentRestartedWorkItem(final Agent agent) + { + super(WorkItemType.AGENT_RESTARTED, null, newParams(agent, null)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Console.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Console.java new file mode 100644 index 0000000000..9a5c2e560d --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/Console.java @@ -0,0 +1,2237 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +// JMS Imports +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.MessageListener; +import javax.jms.Session; + +// Used to get the PID equivalent +import java.lang.management.ManagementFactory; + +// Simple Logging Facade 4 Java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Misc Imports +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.AMQPMessage; +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.Notifier; +import org.apache.qpid.qmf2.common.NotifierWrapper; +import org.apache.qpid.qmf2.common.NullQmfEventListener; +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfCallback; +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfEvent; +import org.apache.qpid.qmf2.common.QmfEventListener; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfQuery; +import org.apache.qpid.qmf2.common.QmfQueryTarget; +import org.apache.qpid.qmf2.common.SchemaClass; +import org.apache.qpid.qmf2.common.SchemaClassId; +import org.apache.qpid.qmf2.common.SchemaEventClass; +import org.apache.qpid.qmf2.common.SchemaObjectClass; +import org.apache.qpid.qmf2.common.WorkItem; +import org.apache.qpid.qmf2.common.WorkQueue; + +// Reuse this class as it provides a handy mechanism to parse an options String into a Map +import org.apache.qpid.messaging.util.AddressParser; + +/** + * The Console class is the top-level object used by a console application. All QMF console functionality + * is made available by this object. A console application must instatiate one of these objects. + * <p> + * If a name is supplied, it must be unique across all Consoles attached to the AMQP bus under the given + * domain. If no name is supplied, a unique name will be synthesized in the format: {@literal "qmfc-<hostname>.<pid>"} + * <p> + * <h3>Interactions with remote Agent</h3> + * As noted below, some Console methods require contacting a remote Agent. For these methods, the caller + * has the option to either block for a (non-infinite) timeout waiting for a reply, or to allow the method + * to complete asynchonously. When the asynchronous approach is used, the caller must provide a unique + * handle that identifies the request. When the method eventually completes, a WorkItem will be placed on + * the work queue. The WorkItem will contain the handle that was provided to the corresponding method call. + * <p> + * The following diagram illustrates the interactions between the Console, Agent and client side Agent proxy. + * <p> + * <img alt="" src="doc-files/Console.png"> + * <p> + * All blocking calls are considered thread safe - it is possible to have a multi-threaded implementation + * have multiple blocking calls in flight simultaneously. + * <p> + * <h3>Subscriptions</h3> + * This implementation of the QMF2 API has full support for QMF2 Subscriptions where they are supported by an Agent. + * <p> + * N.B. That the 0.12 C++ broker does not <i>actually</i> support subscriptions, however it does periodically "push" + * QmfConsoleData Object updates as _data indications. The Console class uses these to provide client side + * emulation of broker subscriptions. + * The System Property "disable_subscription_emulation" may be set to true to disable this behaviour. + * <p> + * The diagram below shows the relationship between the Console, the SubscriptionManager (which is used to manage the + * lifecycle of Subscriptions on the client side) and the local Agent representation. + * <p> + * The SubscriptionManager is also used to maintain the Subscription query to enable ManagementAgent _data indications + * to be tested in order to emulate Subscriptions to the broker ManagementAgent on the client side. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * <p> + * <h3>Receiving Asynchronous Notifications</h3> + * This implementation of the QMF2 Console actually supports two independent APIs to enable clients to receive + * Asynchronous notifications. + * <p> + * A QmfEventListener object is used to receive asynchronously delivered WorkItems. + * <p> + * This provides an alternative (simpler) API to the official QMF2 WorkQueue API that some (including the Author) + * may prefer over the official API. + * <p> + * The following diagram illustrates the QmfEventListener Event model. + * <p> + * Notes + * <ol> + * <li>This is provided as an alternative to the official QMF2 WorkQueue and Notifier Event model.</li> + * <li>Agent and Console methods are sufficiently thread safe that it is possible to call them from a callback fired + * from the onEvent() method that may have been called from the JMS MessageListener. Internally the synchronous + * and asynchronous calls are processed on different JMS Sessions to facilitate this</li> + * </ol> + * <p> + * <img alt="" src="doc-files/QmfEventListenerModel.png"> + * <p> + * The QMF2 API has a work-queue Callback approach. All asynchronous events are represented by a WorkItem object. + * When a QMF event occurs it is translated into a WorkItem object and placed in a FIFO queue. It is left to the + * application to drain this queue as needed. + * <p> + * This new API does require the application to provide a single callback. The callback is used to notify the + * application that WorkItem object(s) are pending on the work queue. This callback is invoked by QMF when one or + * more new WorkItem objects are added to the queue. To avoid any potential threading issues, the application is + * not allowed to call any QMF API from within the context of the callback. The purpose of the callback is to + * notify the application to schedule itself to drain the work queue at the next available opportunity. + * <p> + * For example, a console application may be designed using a select() loop. The application waits in the select() + * for any of a number of different descriptors to become ready. In this case, the callback could be written to + * simply make one of the descriptors ready, and then return. This would cause the application to exit the wait state, + * and start processing pending events. + * <p> + * The callback is represented by the Notifier virtual base class. This base class contains a single method. An + * application derives a custom notification handler from this class, and makes it available to the Console or + * Agent object. + * <p> + * The following diagram illustrates the Notifier and WorkQueue QMF2 API Event model. + * <p> + * Notes + * <ol> + * <li>There is an alternative (simpler but not officially QMF2) API based on implementing the QmfEventListener as + * described previously.</li> + * <li>BlockingNotifier is not part of QMF2 either but is how most people would probably write a Notifier.</li> + * <li>It's generally not necessary to use a Notifier as the Console provides a blocking getNextWorkitem() method.</li> + * </ol> + * <p> + * <img alt="" src="doc-files/WorkQueueEventModel.png"> + + + * <h3>Potential Issues with Qpid versions earlier than 0.12</h3> + * Note 1: This uses QMF2 so requires that the "--mgmt-qmf2 yes" option is applied to the broker (this is the default + * from Qpid 0.10). + * <p> + * Note 2: In order to use QMF2 the app-id field needs to be set. There appears to be no way to set the AMQP 0-10 + * specific app-id field on a message which the brokers QMFv2 implementation currently requires. + * <p> + * Gordon Sim has put together a patch for org.apache.qpid.client.message.AMQMessageDelegate_0_10 + * Found in client/src/main/java/org/apache/qpid/client/message/AMQMessageDelegate_0_10.java + * <pre> + * public void setStringProperty(String propertyName, String value) throws JMSException + * { + * checkPropertyName(propertyName); + * checkWritableProperties(); + * setApplicationHeader(propertyName, value); + * + * if ("x-amqp-0-10.app-id".equals(propertyName)) + * { + * _messageProps.setAppId(value.getBytes()); + * } + * } + * </pre> + * This gets things working. + * <p> + * A jira <a href=https://issues.apache.org/jira/browse/QPID-3302>QPID-3302</a> has been raised. + * This is fixed in Qpid 0.12. + * + * @author Fraser Adams + */ +public final class Console implements MessageListener, AgentProxy +{ + private static final Logger _log = LoggerFactory.getLogger(Console.class); + + // Attributes + // ******************************************************************************************************** + + /** + * The eventListener may be a real application QmfEventListener, a NullQmfEventListener or an application + * Notifier wrapped in a QmfEventListener. In all cases the Console may call _eventListener.onEvent() at + * various places to pass a WorkItem to an asynchronous receiver. + */ + private QmfEventListener _eventListener; + + /** + * Explicitly store Agents in a ConcurrentHashMap, as we know the MessageListener thread may modify its contents. + */ + private Map<String, Agent> _agents = new ConcurrentHashMap<String, Agent>(); + + /** + * This Map is used to look up a Subscription by consoleHandle. + */ + private Map<String, SubscriptionManager> _subscriptionByHandle = new ConcurrentHashMap<String, SubscriptionManager>(); + + /** + * This Map is used to look up a Subscription by subscriptionId + */ + private Map<String, SubscriptionManager> _subscriptionById = new ConcurrentHashMap<String, SubscriptionManager>(); + + /** + * Used to implement a thread safe queue of WorkItem objects used to implement the Notifier API + */ + private WorkQueue _workQueue = new WorkQueue(); + + /** + * The name of the broker Agent is explicitly recorded when the broker Agent is discovered, we use this so + * we can support the synonyms "broker" and "qpidd" for the broker Agent, as its full name isn't especially + * easy to use givent that it contains a UUID "instance" component. + */ + private String _brokerAgentName = null; + + /** + * A flag to indicate that an Agent has been registered, used as a condition variable. + */ + private boolean _agentAvailable = false; + + /** + * The domain string is used to construct the name of the AMQP exchange to which the component's + * name string will be bound. If not supplied, the value of the domain defaults to "default". Both + * Agents and Components must belong to the same domain in order to communicate. + */ + private String _domain; + + /** + * A QMF address is composed of two parts - an optional domain string, and a mandatory + * name string "qmf.<domain-string>.direct/<name-string>" + */ + private String _address; + + /** + * This flag enables AGENT_* work items to be sent to the Event Listener. Note this is enabled by default + */ + private boolean _discoverAgents = true; + + /** + * This Query is set by the enableAgentDiscovery() method that takes a QmfQuery as a parameter. It is used + * in conjunction with _discoverAgents to decide whether to send notifications of Agent activity + */ + private QmfQuery _agentQuery = null; + + /** + * This flag disbles asynchronous behaviour such as QMF Events, Agent discovery etc. useful in simple + * Use Cases such as getObjects() on the broker. Note that asynchronous behaviour enabled by default. + */ + private boolean _disableEvents = false; + + /** + * If the "disable_subscription_emulation" System Property is set then we disable Console side emulation + * of broker subscriptions + */ + private boolean _subscriptionEmulationEnabled = !Boolean.getBoolean("disable_subscription_emulation"); + + /** + * Various timeouts used internally. + * replyTimeout is the default maximum time we wait for synchronous responses + * agentTimeout is the maximum time we wait for any Agent activity before expiring the Agent + * subscriptionDuration is the default maximum time we keep a subscription active + */ + private int _replyTimeout = 10; + private int _agentTimeout = 60; // 1 minute + private int _subscriptionDuration = 300; // 5 minutes + + /** + * This timer is used tidy up Subscription references where a Subscription has expired. Ideally a client should + * call cancelSubscription(), but we can't rely on it. + */ + private Timer _timer; + + /** + * Various JMS related fields + */ + private Connection _connection = null; + private Session _asyncSession; + private Session _syncSession; + private MessageConsumer _eventConsumer; + private MessageConsumer _responder; + private MessageConsumer _asyncResponder; + private MessageProducer _requester; + private MessageProducer _broadcaster; + private Destination _replyAddress; + private Destination _asyncReplyAddress; + + // private implementation methods + // ******************************************************************************************************** + + /** + * Send an asynchronous _agent_locate_request to the topic broadcast address with the subject + * "console.request.agent_locate". This should cause all active Agents to respond on the async + * direct address, which gets handled by onMessage() + */ + private void broadcastAgentLocate() + { + try + { + Message request = AMQPMessage.createListMessage(_syncSession); + request.setJMSReplyTo(_asyncReplyAddress); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_agent_locate_request"); + request.setStringProperty("qpid.subject", "console.request.agent_locate"); + AMQPMessage.setList(request, Collections.emptyList()); + _broadcaster.send(request); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in broadcastAgentLocate()", jmse.getMessage()); + } + } + + /** + * Check whether any of the registered Agents has expired by comparing their timestamp against the + * current time. We explicitly use an iterator rather than a foreach loop because if the Agent has + * expired we want to remove it and the only safe way to do that whilst still iterating is to use + * iterator.remove() and the foreach loop hides the underlying iterator from us. + */ + private void handleAgentExpiry() + { + long currentTime = System.currentTimeMillis()*1000000l; + + // Use the iterator approach rather than foreach as we may want to call iterator.remove() to zap an entry + Iterator<Agent> i = _agents.values().iterator(); + while (i.hasNext()) + { + Agent agent = i.next(); + // Get the time difference in seconds between now and the last Agent update. + long diff = (currentTime - agent.getTimestamp())/1000000000l; + if (diff > _agentTimeout) + { + if (agent.getVendor().equals("apache.org") && agent.getProduct().equals("qpidd")) + { + _brokerAgentName = null; + } + agent.deactivate(); + i.remove(); + _log.info("Agent {} has expired", agent.getName()); + if (_discoverAgents && (_agentQuery == null || _agentQuery.evaluate(agent))) + { + _eventListener.onEvent(new AgentDeletedWorkItem(agent)); + } + } + } + } + + /** + * MessageListener for QMF2 Agent Events, Hearbeats and Asynchronous data indications + * + * @param message the JMS Message passed to the listener + */ + public void onMessage(Message message) + { + try + { + String agentName = QmfData.getString(message.getObjectProperty("qmf.agent")); + String content = QmfData.getString(message.getObjectProperty("qmf.content")); + String opcode = QmfData.getString(message.getObjectProperty("qmf.opcode")); + //String routingKey = ((javax.jms.Topic)message.getJMSDestination()).getTopicName(); + //String contentType = ((org.apache.qpid.client.message.AbstractJMSMessage)message).getContentType(); + +//System.out.println(); +//System.out.println("agentName = " + agentName); +//System.out.println("content = " + content); +//System.out.println("opcode = " + opcode); +//System.out.println("routingKey = " + routingKey); +//System.out.println("contentType = " + contentType); + + if (opcode.equals("_agent_heartbeat_indication") || opcode.equals("_agent_locate_response")) + { // This block handles Agent lifecycle information (discover, register, delete) + if (_agents.containsKey(agentName)) + { // This block handles Agents that have previously been registered + Agent agent = _agents.get(agentName); + long originalEpoch = agent.getEpoch(); + + // If we already know about an Agent we simply update the Agent's state using initialise() + agent.initialise(AMQPMessage.getMap(message)); + + // If the Epoch has changed it means the Agent has been restarted so we send a notification + if (agent.getEpoch() != originalEpoch) + { + agent.clearSchemaCache(); // Clear cache to force a lookup + List<SchemaClassId> classes = getClasses(agent); + getSchema(classes, agent); // Discover the schema for this Agent and cache it + _log.info("Agent {} has been restarted", agentName); + if (_discoverAgents && (_agentQuery == null || _agentQuery.evaluate(agent))) + { + _eventListener.onEvent(new AgentRestartedWorkItem(agent)); + } + } + else + { // Otherwise just send a heartbeat notification + _log.info("Agent {} heartbeat", agent.getName()); + if (_discoverAgents && (_agentQuery == null || _agentQuery.evaluate(agent))) + { + _eventListener.onEvent(new AgentHeartbeatWorkItem(agent)); + } + } + } + else + { // This block handles Agents that haven't already been registered + Agent agent = new Agent(AMQPMessage.getMap(message), this); + List<SchemaClassId> classes = getClasses(agent); + getSchema(classes, agent); // Discover the schema for this Agent and cache it + _agents.put(agentName, agent); + _log.info("Adding Agent {}", agentName); + + // If the Agent is the Broker Agent we record it as _brokerAgentName to make retrieving + // the Agent more "user friendly" than using the full Agent name. + if (agent.getVendor().equals("apache.org") && agent.getProduct().equals("qpidd")) + { + _log.info("Recording {} as _brokerAgentName", agentName); + _brokerAgentName = agentName; + } + + // Notify any waiting threads that an Agent has been registered. Note that we only notify if + // we've already found the broker Agent to avoid a race condition in addConnection(), as another + // Agent could in theory trigger this block first. In addConnection() we *explicitly* want to + // wait for the broker Agent to become available. + if (_brokerAgentName != null) + { + synchronized(this) + { + _agentAvailable = true; + notifyAll(); + } + } + + if (_discoverAgents && (_agentQuery == null || _agentQuery.evaluate(agent))) + { + _eventListener.onEvent(new AgentAddedWorkItem(agent)); + } + } + + // The broker Agent sends periodic heartbeats and that Agent should *always* be available given + // a running broker, so we should get here every "--mgmt-pub-interval" seconds or so, so it's + // a good place to periodically check for the expiry of any other Agents. + handleAgentExpiry(); + return; + } + + if (!_agents.containsKey(agentName)) + { + _log.info("Ignoring Event from unregistered Agent {}", agentName); + return; + } + + Agent agent = _agents.get(agentName); + if (!agent.eventsEnabled()) + { + _log.info("{} has disabled Event reception, ignoring Event", agentName); + return; + } + + // If we get to here the Agent from whence the Event came should be registered and should + // have Event reception enabled, so we should be able to send events to the EventListener + + Handle handle = new Handle(message.getJMSCorrelationID()); + if (opcode.equals("_method_response") || opcode.equals("_exception")) + { + if (AMQPMessage.isAMQPMap(message)) + { + _eventListener.onEvent( + new MethodResponseWorkItem(handle, new MethodResult(AMQPMessage.getMap(message))) + ); + } + else + { + _log.info("onMessage() Received Method Response message in incorrect format"); + } + } + + // Query Response. The only asynchronous query response we expect to see is the result of an async + // refresh() call on QmfConsoleData so the number of results in the returned list *should* be one. + if (opcode.equals("_query_response") && content.equals("_data")) + { + if (AMQPMessage.isAMQPList(message)) + { + List<Map> list = AMQPMessage.getList(message); + for (Map m : list) + { + _eventListener.onEvent(new ObjectUpdateWorkItem(handle, new QmfConsoleData(m, agent))); + } + } + else + { + _log.info("onMessage() Received Query Response message in incorrect format"); + } + } + + // This block handles responses to createSubscription and refreshSubscription + if (opcode.equals("_subscribe_response")) + { + if (AMQPMessage.isAMQPMap(message)) + { + String correlationId = message.getJMSCorrelationID(); + SubscribeParams params = new SubscribeParams(correlationId, AMQPMessage.getMap(message)); + String subscriptionId = params.getSubscriptionId(); + + if (subscriptionId != null && correlationId != null) + { + SubscriptionManager subscription = _subscriptionById.get(subscriptionId); + if (subscription == null) + { // This is a createSubscription response so the correlationId should be the consoleHandle + subscription = _subscriptionByHandle.get(correlationId); + if (subscription != null) + { + _subscriptionById.put(subscriptionId, subscription); + subscription.setSubscriptionId(subscriptionId); + subscription.setDuration(params.getLifetime()); + String replyHandle = subscription.getReplyHandle(); + if (replyHandle == null) + { + subscription.signal(); + } + else + { + _eventListener.onEvent(new SubscribeResponseWorkItem(new Handle(replyHandle), params)); + } + } + } + else + { // This is a refreshSubscription response + params.setConsoleHandle(subscription.getConsoleHandle()); + subscription.setDuration(params.getLifetime()); + subscription.refresh(); + _eventListener.onEvent(new SubscribeResponseWorkItem(handle, params)); + } + } + } + else + { + _log.info("onMessage() Received Subscribe Response message in incorrect format"); + } + } + + // Subscription Indication - in other words the asynchronous results of a Subscription + if (opcode.equals("_data_indication") && content.equals("_data")) + { + if (AMQPMessage.isAMQPList(message)) + { + String consoleHandle = handle.getCorrelationId(); + if (consoleHandle != null && _subscriptionByHandle.containsKey(consoleHandle)) + { // If we have a valid consoleHandle the data has come from a "real" Subscription. + List<Map> list = AMQPMessage.getList(message); + List<QmfConsoleData> resultList = new ArrayList<QmfConsoleData>(list.size()); + for (Map m : list) + { + resultList.add(new QmfConsoleData(m, agent)); + } + _eventListener.onEvent( + new SubscriptionIndicationWorkItem(new SubscribeIndication(consoleHandle, resultList)) + ); + } + else if (_subscriptionEmulationEnabled && agentName.equals(_brokerAgentName)) + { // If the data has come from is the broker Agent we emulate a Subscription on the Console + for (SubscriptionManager subscription : _subscriptionByHandle.values()) + { + QmfQuery query = subscription.getQuery(); + if (subscription.getAgent().getName().equals(_brokerAgentName) && + query.getTarget() == QmfQueryTarget.OBJECT) + { // Only evaluate broker Agent subscriptions with QueryTarget == OBJECT on the Console. + long objectEpoch = 0; + consoleHandle = subscription.getConsoleHandle(); + List<Map> list = AMQPMessage.getList(message); + List<QmfConsoleData> resultList = new ArrayList<QmfConsoleData>(list.size()); + for (Map m : list) + { // Evaluate the QmfConsoleData object against the query + QmfConsoleData object = new QmfConsoleData(m, agent); + if (query.evaluate(object)) + { + long epoch = object.getObjectId().getAgentEpoch(); + objectEpoch = (epoch > objectEpoch && !object.isDeleted()) ? epoch : objectEpoch; + resultList.add(object); + } + } + + if (resultList.size() > 0) + { // If there are any results available after evaluating the query we deliver them + // via a SubscribeIndicationWorkItem. + + // Before we send the WorkItem we take a peek at the Agent Epoch value that forms + // part of the ObjectID and compare it against the current Epoch value. If they + // are different we send an AgentRestartedWorkItem. We *normally* check for Epoch + // changes when we receive heartbeat indications, but unfortunately the broker + // ManagementAgent pushes data *before* it pushes heartbeats. Its more useful + // however for clients to know that an Agent has been restarted *before* they get + // data from the restarted Agent (in case they need to reset any state). + if (objectEpoch > agent.getEpoch()) + { + agent.setEpoch(objectEpoch); + agent.clearSchemaCache(); // Clear cache to force a lookup + List<SchemaClassId> classes = getClasses(agent); + getSchema(classes, agent); // Discover the schema for this Agent and cache it + _log.info("Agent {} has been restarted", agentName); + if (_discoverAgents && (_agentQuery == null || _agentQuery.evaluate(agent))) + { + _eventListener.onEvent(new AgentRestartedWorkItem(agent)); + } + } + + _eventListener.onEvent( + new SubscriptionIndicationWorkItem( + new SubscribeIndication(consoleHandle, resultList)) + ); + } + } + } + } + } + else + { + _log.info("onMessage() Received Subscribe Indication message in incorrect format"); + } + } + + // The results of an Event delivered from an Agent + if (opcode.equals("_data_indication") && content.equals("_event")) + { // There are differences in the type of message sent by Qpid 0.8 and 0.10 onwards. + if (AMQPMessage.isAMQPMap(message)) + { // 0.8 broker passes Events as amqp/map encoded as MapMessages (we convert into java.util.Map) + _eventListener.onEvent(new EventReceivedWorkItem(agent, new QmfEvent(AMQPMessage.getMap(message)))); + } + else if (AMQPMessage.isAMQPList(message)) + { // 0.10 and above broker passes Events as amqp/list encoded as BytesMessage (needs decoding) + // 0.20 encodes amqp/list in a MapMessage!!?? AMQPMessage hopefully abstracts this detail. + List<Map> list = AMQPMessage.getList(message); + for (Map m : list) + { + _eventListener.onEvent(new EventReceivedWorkItem(agent, new QmfEvent(m))); + } + } + else + { + _log.info("onMessage() Received Event message in incorrect format"); + } + } + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in onMessage()", jmse.getMessage()); + } + } // end of onMessage() + + /** + * Retrieve the schema for a List of classes. + * This method explicitly retrieves the schema from the remote Agent and is generally used for schema + * discovery when an Agent is added or updated. + * + * @param classes the list of SchemaClassId of the classes who's schema we want to retrieve + * @param agent the Agent we want to retrieve the schema from + */ + private List<SchemaClass> getSchema(final List<SchemaClassId> classes, final Agent agent) + { + List<SchemaClass> results = new ArrayList<SchemaClass>(); + for (SchemaClassId classId : classes) + { + agent.setSchema(classId, Collections.<SchemaClass>emptyList()); // Clear Agent's schema value for classId + results.addAll(getSchema(classId, agent)); + } + return results; + } + + /** + * Perform a query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects. + * If replyHandle is null this method will block until the agent replies, or the timeout expires. + * Once the timeout expires, all data retrieved to date is returned. If replyHandle is non-null an + * asynchronous request is performed + * + * @param agent the Agent being queried + * @param query the ObjectId or SchemaClassId being queried for. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses + * @param timeout the time to wait for a reply from the Agent, a value of -1 means use the default timeout + * @return a List of QMF Objects describing that class + */ + private List<QmfConsoleData> getObjects(final Agent agent, final QmfData query, + final String replyHandle, int timeout) + { + String agentName = agent.getName(); + timeout = (timeout < 1) ? _replyTimeout : timeout; + List<QmfConsoleData> results = Collections.emptyList(); + try + { + Destination destination = (replyHandle == null) ? _replyAddress : _asyncReplyAddress; + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(destination); + request.setJMSCorrelationID(replyHandle); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_query_request"); + request.setStringProperty("qpid.subject", agentName); + + // Create a QMF Query for an "OBJECT" target using either a schema ID or object ID + String queryType = (query instanceof SchemaClassId) ? "_schema_id" : "_object_id"; + request.setObject("_what", "OBJECT"); + request.setObject(queryType, query.mapEncode()); + + // Wrap request & response in synchronized block in case any other threads invoke a request + // it would be somewhat unfortunate if their response got interleaved with ours!! + synchronized(this) + { + _requester.send(request); + if (replyHandle == null) + { + boolean lastResult = true; + ArrayList<QmfConsoleData> partials = new ArrayList<QmfConsoleData>(); + do + { // Wrap in a do/while loop to cater for the case where the Agent may send partial results. + Message response = _responder.receive(timeout*1000); + if (response == null) + { + _log.info("No response received in getObjects()"); + return partials; + } + + lastResult = !response.propertyExists("partial"); + + if (AMQPMessage.isAMQPList(response)) + { + List<Map> mapResults = AMQPMessage.getList(response); + partials.ensureCapacity(partials.size() + mapResults.size()); + for (Map content : mapResults) + { + partials.add(new QmfConsoleData(content, agent)); + } + } + else if (AMQPMessage.isAMQPMap(response)) + { + // Error responses are returned as MapMessages, though they are being ignored here. + //QmfData exception = new QmfData(AMQPMessage.getMap(response)); + //System.out.println(agentName + " " + exception.getStringValue("error_text")); + } + else + { + _log.info("getObjects() Received response message in incorrect format"); + } + } while (!lastResult); + results = partials; + } + } + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in getObjects()", jmse.getMessage()); + } + return results; + } + + // methods implementing AgentProxy interface + // ******************************************************************************************************** + + /** + * Releases the specified Agent instance. Once called, the console application should not reference this + * instance again. + * <p> + * Intended to by called by the AgentProxy. Shouldn't generally be called directly by Console applications. + * + * @param agent the Agent that we wish to destroy. + */ + public void destroy(final Agent agent) + { + handleAgentExpiry(); + } + + /** + * Request that the Agent update the value of an object's contents. + * <p> + * Intended to by called by the AgentProxy. Shouldn't generally be called directly by Console applications. + * + * @param agent the Agent to get the refresh from. + * @param objectId the ObjectId being queried for + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses + * @param timeout the time to wait for a reply from the Agent, a value of -1 means use the default timeout + * @return the refreshed object + */ + public QmfConsoleData refresh(final Agent agent, final ObjectId objectId, final String replyHandle, final int timeout) + { + List<QmfConsoleData> objects = getObjects(agent, objectId, replyHandle, timeout); + return (objects.size() == 0) ? null : objects.get(0); + } + + /** + * Invoke the named method on the named Agent. + * <p> + * Intended to by called by the AgentProxy. Shouldn't generally be called directly by Console applications. + * + * @param agent the Agent to invoke the method on. + * @param content an unordered set of key/value pairs comprising the method arguments. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses + * @param timeout the time to wait for a reply from the Agent, a value of -1 means use the default timeout + * @return the method response Arguments in Map form + */ + public MethodResult invokeMethod(final Agent agent, final Map<String, Object> content, + final String replyHandle, int timeout) throws QmfException + { + if (!agent.isActive()) + { + throw new QmfException("Called invokeMethod() with inactive agent"); + } + String agentName = agent.getName(); + timeout = (timeout < 1) ? _replyTimeout : timeout; + try + { + Destination destination = (replyHandle == null) ? _replyAddress : _asyncReplyAddress; + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(destination); + request.setJMSCorrelationID(replyHandle); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_method_request"); + request.setStringProperty("qpid.subject", agentName); + + for (Map.Entry<String, Object> entry : content.entrySet()) + { + request.setObject(entry.getKey(), entry.getValue()); + } + + // Wrap request & response in synchronized block in case any other threads invoke a request + // it would be somewhat unfortunate if their response got interleaved with ours!! + synchronized(this) + { + _requester.send(request); + if (replyHandle == null) + { // If this is a synchronous request get the response + Message response = _responder.receive(timeout*1000); + if (response == null) + { + _log.info("No response received in invokeMethod()"); + throw new QmfException("No response received for Console.invokeMethod()"); + } + MethodResult result = new MethodResult(AMQPMessage.getMap(response)); + QmfException exception = result.getQmfException(); + if (exception != null) + { + throw exception; + } + return result; + } + } + // If this is an asynchronous request return without waiting for a response + return null; + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in invokeMethod()", jmse.getMessage()); + throw new QmfException(jmse.getMessage()); + } + } + + /** + * Remove a Subscription. + * + * @param subscription the SubscriptionManager that we wish to remove. + */ + public void removeSubscription(final SubscriptionManager subscription) + { + String consoleHandle = subscription.getConsoleHandle(); + String subscriptionId = subscription.getSubscriptionId(); + if (consoleHandle != null) + { + _subscriptionByHandle.remove(consoleHandle); + } + if (subscriptionId != null) + { + _subscriptionById.remove(subscriptionId); + } + } + + // QMF API Methods + // ******************************************************************************************************** + + /** + * Constructor that provides defaults for name and domain and has no Notifier/Listener + * <p> + * Warning!! If more than one Console is needed in a process be sure to use the Constructor that allows one to + * supply a name as <hostname>.<pid> isn't unique enough and will result in "odd results" (trust me!!). + */ + public Console() throws QmfException + { + this(null, null, null, null); + } + + /** + * Constructor that provides defaults for name and domain and takes a Notifier/Listener. + * <p> + * Warning!! If more than one Console is needed in a process be sure to use the Constructor that allows one to + * supply a name as <hostname>.<pid> isn't unique enough and will result in "odd results" (trust me!!). + * + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. This API may be simpler and more convenient + * than the QMF2 Notifier API for many applications. + */ + public Console(final QmfCallback notifier) throws QmfException + { + this(null, null, notifier, null); + } + + /** + * Main constructor, creates a Console, but does NOT start it, that requires us to do addConnection() + * + * @param name if no name is supplied we synthesise one in the format: <pre>"qmfc-<hostname>.<pid>"</pre> + * if we can, otherwise we create a name using a randomUUID. + * <p> + * Warning!! If more than one Console is needed in a process be sure to supply a name as + * <hostname>.<pid> isn't unique enough and will result in "odd results" (trust me!!). + * @param domain the QMF "domain". A QMF address is composed of two parts - an optional domain string, and a + * mandatory name string <pre>"qmf.<domain-string>.direct/<name-string>"</pre> + * The domain string is used to construct the name of the AMQP exchange to which the component's + * name string will be bound. If not supplied, the value of the domain defaults to "default". Both + * Agents and Components must belong to the same domain in order to communicate. + * @param notifier this may be either a QMF2 API Notifier object OR a QMFEventListener. + * <p> + * The latter is an alternative API that avoids the need for an explicit Notifier thread to be created the + * EventListener is called from the JMS MessageListener thread. This API may be simpler and more convenient + * than the QMF2 Notifier API for many applications. + * @param options a String representation of a Map containing the options in the form + * <pre>"{replyTimeout:<value>, agentTimeout:<value>, subscriptionDuration:<value>}"</pre> + * they are all optional and may appear in any order. + * <pre> + * <b>replyTimeout</b>=<default for all blocking calls> + * <b>agentTimeout</b>=<default timeout for agent heartbeat>, + * <b>subscriptionDuration</b>=<default lifetime of a subscription> + * </pre> + */ + public Console(String name, final String domain, + final QmfCallback notifier, final String options) throws QmfException + { + if (name == null) + { + // ManagementFactory.getRuntimeMXBean().getName()) returns the name representing the running virtual machine. + // The returned name string can be any arbitrary string and a Java virtual machine implementation can choose + // to embed platform-specific useful information in the returned name string. + // As it happens on Linux the format for this is PID@hostname + String vmName = ManagementFactory.getRuntimeMXBean().getName(); + String[] split = vmName.split("@"); + if (split.length == 2) + { + name = "qmfc-" + split[1] + "." + split[0]; + } + else + { + name = "qmfc-" + UUID.randomUUID(); + } + } + + _domain = (domain == null) ? "default" : domain; + _address = "qmf." + _domain + ".direct" + "/" + name; + + if (notifier == null) + { + _eventListener = new NullQmfEventListener(); + } + else if (notifier instanceof Notifier) + { + _eventListener = new NotifierWrapper((Notifier)notifier, _workQueue); + } + else if (notifier instanceof QmfEventListener) + { + _eventListener = (QmfEventListener)notifier; + } + else + { + throw new QmfException("QmfCallback listener must be either a Notifier or QmfEventListener"); + } + + if (options != null) + { // We wrap the Map in a QmfData object to avoid potential class cast issues with the parsed options + QmfData optMap = new QmfData(new AddressParser(options).map()); + if (optMap.hasValue("replyTimeout")) + { + _replyTimeout = (int)optMap.getLongValue("replyTimeout"); + } + + if (optMap.hasValue("agentTimeout")) + { + _agentTimeout = (int)optMap.getLongValue("agentTimeout"); + } + + if (optMap.hasValue("subscriptionDuration")) + { + _subscriptionDuration = (int)optMap.getLongValue("subscriptionDuration"); + } + } + } + + /** + * Release the Console's resources. + */ + public void destroy() + { + try + { + if (_connection != null) + { + removeConnection(_connection); + } + } + catch (QmfException qmfe) + { + // Ignore as we've already tested for _connection != null this should never occur + } + } + + /** + * Connect the console to the AMQP cloud. + * + * @param conn a javax.jms.Connection + */ + public void addConnection(final Connection conn) throws QmfException + { + addConnection(conn, ""); + } + + /** + * Connect the console to the AMQP cloud. + * <p> + * This is an extension to the standard QMF2 API allowing the user to specify address options in order to allow + * finer control over the Console's request and reply queues, e.g. explicit name, non-default size or durability. + * + * @param conn a javax.jms.Connection + * @param addressOptions options String giving finer grained control of the receiver queue. + * <p> + * As an example the following gives the Console's queues the name test-console, size = 500000000 and ring policy. + * <pre> + * " ; {link: {name:'test-console', x-declare: {arguments: {'qpid.policy_type': ring, 'qpid.max_size': 500000000}}}}" + * </pre> + * Note that the Console uses several queues so this will actually create a test-console queue plus a + * test-console-async queue and a test-console-event queue. + * <p> + * If a name parameter is not present temporary queues will be created, but the other options will still be applied. + */ + public void addConnection(final Connection conn, final String addressOptions) throws QmfException + { + // Make the test and set of _connection synchronized just in case multiple threads attempt to add a _connection + // to the same Console instance at the same time. + synchronized(this) + { + if (_connection != null) + { + throw new QmfException("Multiple connections per Console is not supported"); + } + _connection = conn; + } + try + { + String syncReplyAddressOptions = addressOptions; + String asyncReplyAddressOptions = addressOptions; + String eventAddressOptions = addressOptions; + + if (!addressOptions.equals("")) + { // If there are address options supplied we need to check if a name parameter is present. + String[] split = addressOptions.split("name"); + if (split.length == 2) + { // If options contains a name parameter we extract it and create variants for async and event queues. + split = split[1].split("[,}]"); // Look for the end of the key/value block + String nameValue = split[0].replaceAll("[ :'\"]", ""); // Remove initial colon, space any any quotes. + // Hopefully at this point nameValue is actually the value of the name parameter. + asyncReplyAddressOptions = asyncReplyAddressOptions.replace(nameValue, nameValue + "-async"); + eventAddressOptions = eventAddressOptions.replace(nameValue, nameValue + "-event"); + } + } + + String topicBase = "qmf." + _domain + ".topic"; + _syncSession = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Create a MessageProducer for the QMF topic address used to broadcast requests + Destination topicAddress = _syncSession.createQueue(topicBase); + _broadcaster = _syncSession.createProducer(topicAddress); + + // If Asynchronous Behaviour is enabled we create the Queues used to receive async responses + // Data Indications, QMF Events, Heartbeats etc. from the broker (or other Agents). + if (!_disableEvents) + { + // TODO it should be possible to bind _eventConsumer and _asyncResponder to the same queue + // if I can figure out the correct AddressString to use, probably not a big deal though. + + _asyncSession = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Set up MessageListener on the Event Address + Destination eventAddress = _asyncSession.createQueue(topicBase + "/agent.ind.#" + eventAddressOptions); + _eventConsumer = _asyncSession.createConsumer(eventAddress); + _eventConsumer.setMessageListener(this); + + // Create the asynchronous JMSReplyTo _replyAddress and MessageConsumer + _asyncReplyAddress = _asyncSession.createQueue(_address + ".async" + asyncReplyAddressOptions); + _asyncResponder = _asyncSession.createConsumer(_asyncReplyAddress); + _asyncResponder.setMessageListener(this); + } + + // I've extended the synchronized block to include creating the _requester and _responder. I don't believe + // that this is strictly necessary, but it stops findbugs moaning about inconsistent synchronization + // so makes sense if only to get that warm and fuzzy feeling of keeping findbugs happy :-) + synchronized(this) + { + // Create a MessageProducer for the QMF direct address, mainly used for request/response + Destination directAddress = _syncSession.createQueue("qmf." + _domain + ".direct"); + _requester = _syncSession.createProducer(directAddress); + + // Create the JMSReplyTo _replyAddress and MessageConsumer + _replyAddress = _syncSession.createQueue(_address + syncReplyAddressOptions); + _responder = _syncSession.createConsumer(_replyAddress); + + _connection.start(); + + // If Asynchronous Behaviour is disabled we create an Agent instance to represent the broker + // ManagementAgent the only info that needs to be populated is the _name and we can use the + // "broker" synonym. We populate this fake Agent so getObjects() behaviour is consistent whether + // we've any received *real* Agent updates or not. + if (_disableEvents) + { + _brokerAgentName = "broker"; + Map<String, String> map = new HashMap<String, String>(); + map.put("_name", _brokerAgentName); + Agent agent = new Agent(map, this); + _agents.put(_brokerAgentName, agent); + _agentAvailable = true; + } + else + { + // If Asynchronous Behaviour is enabled Broadcast an Agent Locate message to get Agent info quickly. + broadcastAgentLocate(); + } + + // Wait until the Broker Agent has been located (this should generally be pretty quick) + while (!_agentAvailable) + { + long startTime = System.currentTimeMillis(); + try + { + wait(_replyTimeout*1000); + } + catch (InterruptedException ie) + { + continue; + } + // Measure elapsed time to test against spurious wakeups and ensure we really have timed out + long elapsedTime = (System.currentTimeMillis() - startTime)/1000; + if (!_agentAvailable && elapsedTime >= _replyTimeout) + { + _log.info("Broker Agent not found"); + throw new QmfException("Broker Agent not found"); + } + } + + // Timer used for tidying up Subscriptions. + _timer = new Timer(true); + } + } + catch (JMSException jmse) + { + // If we can't create the QMF Destinations there's not much else we can do + _log.info("JMSException {} caught in addConnection()", jmse.getMessage()); + throw new QmfException("Failed to create sessions or destinations " + jmse.getMessage()); + } + } + + /** + * Remove the AMQP connection from the console. Un-does the addConnection() operation, and releases + * any Agents associated with the connection. All blocking methods are unblocked and given a failure + * status. All outstanding asynchronous operations are cancelled without producing WorkItems. + * + * @param conn a javax.jms.Connection + */ + public void removeConnection(final Connection conn) throws QmfException + { + if (conn != _connection) + { + throw new QmfException("Attempt to delete unknown connection"); + } + + try + { + _timer.cancel(); + _connection.close(); // Should we close() the connection here or just stop() it ??? + } + catch (JMSException jmse) + { + throw new QmfException("Failed to remove connection, caught JMSException " + jmse.getMessage()); + } + _connection = null; + } + + /** + * Get the AMQP address this Console is listening to. + * + * @return the console's replyTo address. Note that there are actually two, there's a synchronous one + * which is the return address for synchronous request/response type invocations and there's an + * asynchronous address with a ".async" suffix which is the return address for asynchronous invocations + */ + public String getAddress() + { + return _address; + } + + /** + * Query for the presence of a specific agent in the QMF domain. Returns a class Agent if the agent is + * present. If the agent is not already known to the console, this call will send a query for the agent + * and block (with default timeout override) waiting for a response. + * + * @param agentName the name of the Agent to be returned. + * <p> + * "broker" or "qpidd" may be used as synonyms for the broker Agent name and the method will try to match + * agentName against the Agent name, the Agent product name and will also check if the Agent name contains + * the agentName String. + * <p> + * Checking against a partial match is useful because the full Agent name has a UUID representing + * the "instance" so it's hard to know the full name without having actually retrieved the Agent. + * @return the found Agent instance or null if an Agent of the given name could not be found + */ + public Agent findAgent(final String agentName) + { + return findAgent(agentName, _replyTimeout); + } + + /** + * Query for the presence of a specific agent in the QMF domain. Returns a class Agent if the agent is + * present. If the agent is not already known to the console, this call will send a query for the agent + * and block (with specified timeout override) waiting for a response. + * + * @param agentName the name of the Agent to be returned. + * <p> + * "broker" or "qpidd" may be used as synonyms for the broker Agent name and the method will try to match + * agentName against the Agent name, the Agent product name and will also check if the Agent name contains + * the agentName String. + * <p> + * Checking against a partial match is useful because the full Agent name has a UUID representing + * the "instance" so it's hard to know the full name without having actually retrieved the Agent. + * @param timeout the time (in seconds) to wait for the Agent to be found + * @return the found Agent instance or null if an Agent of the given name could not be found + */ + public Agent findAgent(final String agentName, final int timeout) + { + Agent agent = getAgent(agentName); + if (agent == null) + { + broadcastAgentLocate(); + long startTime = System.currentTimeMillis(); + do + { + agent = getAgent(agentName); + if (agent != null) + { + return agent; + } + + synchronized(this) + { + try + { // At worst this behaves as a 1 second sleep, but will return sooner if an Agent gets registered + wait(1000); + } + catch (InterruptedException ie) + { + } + } + } while ((System.currentTimeMillis() - startTime)/1000 < _replyTimeout); + } + return agent; + } + + /** + * Called to enable the asynchronous Agent Discovery process. Once enabled, AGENT_ADDED and AGENT_DELETED + * work items can arrive on the WorkQueue. + * <p> + * Note that in this implementation Agent Discovery is enabled by default. Note too that enableAgentDiscovery() + * or disableAgentDiscovery() should be called before addConnection(), as this starts a MessageListener Thread + * that could place events on the work queue. + */ + public void enableAgentDiscovery() + { + _discoverAgents = true; + _agentQuery = null; + } + + /** + * Called to enable the asynchronous Agent Discovery process. Once enabled, AGENT_ADDED and AGENT_DELETED + * work items can arrive on the WorkQueue. The supplied query will be used to filter agent notifications. + * <p> + * Note that in this implementation Agent Discovery is enabled by default. Note too that enableAgentDiscovery() + * or disableAgentDiscovery() should be called before addConnection(), as this starts a MessageListener Thread + * that could place events on the work queue. + * + * @param query the query used to filter agent notifications. + */ + public void enableAgentDiscovery(final QmfQuery query) + { + _discoverAgents = true; + _agentQuery = query; + } + + /** + * Called to disable the async Agent Discovery process enabled by calling enableAgentDiscovery(). + * <p> + * Note that in this implementation Agent Discovery is enabled by default. Note too that enableAgentDiscovery() + * or disableAgentDiscovery() should be called before addConnection() as this starts a MessageListener Thread + * that could place events on the work queue. + */ + public void disableAgentDiscovery() + { + _discoverAgents = false; + _agentQuery = null; + } + + /** + * Called to disable asynchronous behaviour such as QMF Events, Agent discovery etc. useful in simple + * Use Cases such as getObjects() on the broker. Note that asynchronous behaviour enabled by default. + * <p> + * Note too that disableEvents() should be called <b>before</b> addConnection() as this + * starts the MessageListener Thread and creates the additional queues used for Asynchronous Behaviour. + * <p> + * This method is <b>not</b> an official method specified in the QMF2 API, however it is a useful extension + * as Consoles that only call getObjects() on the broker ManagementAgent is an extremely common scenario. + */ + public void disableEvents() + { + _disableEvents = true; + } + + /** + * Return the count of pending WorkItems that can be retrieved. + * @return the count of pending WorkItems that can be retrieved. + */ + public int getWorkitemCount() + { + return _workQueue.size(); + } + + /** + * Obtains the next pending work item - blocking version. + * <p> + * The blocking getNextWorkitem() can be used without the need for a Notifier as it will block until + * a new item gets added to the work queue e.g. the following usage pattern. + * <pre> + * while ((wi = console.getNextWorkitem()) != null) + * { + * System.out.println("WorkItem type: " + wi.getType()); + * } + * </pre> + * @return the next pending work item, or null if none available. + */ + public WorkItem getNextWorkitem() + { + return _workQueue.getNextWorkitem(); + } + + /** + * Obtains the next pending work item - balking version. + * <p> + * The balking getNextWorkitem() is generally used with a Notifier which can be used as a gate to determine + * if any work items are available. e.g. the following usage pattern. + * <pre> + * while (true) + * { + * notifier.waitForWorkItem(); // Assuming a BlockingNotifier has been used here + * System.out.println("WorkItem available, count = " + console.getWorkitemCount()); + * + * WorkItem wi; + * while ((wi = console.getNextWorkitem(0)) != null) + * { + * System.out.println("WorkItem type: " + wi.getType()); + * } + * } + * </pre> + * Note that it is possible for the getNextWorkitem() loop to retrieve multiple items from the workQueue + * and for the Console to add new items as the loop is looping, thus when it finally exits and goes + * back to the outer loop notifier.waitForWorkItems() may return immediately as it had been notified + * whilst we were in the getNextWorkitem() loop. This will be evident by a getWorkitemCount() of 0 + * after returning from waitForWorkItem(). + * <p> + * This is the expected behaviour, but illustrates the need to check for nullness of the value returned + * by getNextWorkitem(), or alternatively to use getWorkitemCount() to put getNextWorkitem() in a + * bounded loop. + * + * @param timeout the timeout in seconds. If timeout = 0 it returns immediately with either a WorkItem or null + * @return the next pending work item, or null if none available. + */ + public WorkItem getNextWorkitem(final long timeout) + { + return _workQueue.getNextWorkitem(timeout); + } + + /** + * Releases a WorkItem instance obtained by getNextWorkItem(). Called when the application has finished + * processing the WorkItem. + */ + public void releaseWorkitem() + { + // To be honest I'm not clear what the intent of this method actually is. One thought is that it's here + // to support equivalent behaviour to the Python Queue.task_done() which is used by queue consumer threads. + // For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing + // on the task is complete. + // + // The problem with that theory is there is no equivalent QMF2 API call that would invoke the + // Queue.join() which is used in conjunction with Queue.task_done() to enable a synchronisation gate to + // be implemented to wait for completion of all worker thread. + // + // I'm a bit stumped and there's no obvious Java equivalent on BlockingQueue, so for now this does nothing. + } + + /** + * Returns a list of all known Agents + * <p> + * Note that this call is synchronous and non-blocking. It only returns locally cached data and will + * not send any messages to the remote agent. + * + * @return a list of available Agents + */ + public List<Agent> getAgents() + { + return new ArrayList<Agent>(_agents.values()); + } + + /** + * Return the named Agent, if known. + * + * @param agentName the name of the Agent to be returned. + * <p> + * "broker" or "qpidd" may be used as synonyms for the broker Agent name and the method will try to match + * agentName against the Agent name, the Agent product name and will also check if the Agent name contains + * the agentName String. + * <p> + * Checking against a partial match is useful because the full Agent name has a UUID representing + * the "instance" so it's hard to know the full name without having actually retrieved the Agent. + * @return the found Agent instance or null if an Agent of the given name could not be found + */ + public Agent getAgent(final String agentName) + { + if (agentName == null) + { + return null; + } + + // First we check if the Agent name is one of the aliases of the broker Agent + if (_brokerAgentName != null) + { + if (agentName.equals("broker") || agentName.equals("qpidd") || agentName.equals(_brokerAgentName)) + { + return _agents.get(_brokerAgentName); + } + } + + for (Agent agent : getAgents()) + { + String product = agent.getProduct(); + String name = agent.getName(); + if (agentName.equals(product) || agentName.equals(name) || name.contains(agentName)) + { + return agent; + } + } + + return null; + } + + /** + * Return a list of all known Packages. + * @return a list of all known Packages. + */ + public List<String> getPackages() + { + List<String> results = new ArrayList<String>(); + for (Agent agent : getAgents()) + { + results.addAll(getPackages(agent)); + } + return results; + } + + /** + * Return a list of Packages for the specified Agent. + * @param agent the Agent being queried. + * @return a list of Packages for the specified Agent. + */ + public List<String> getPackages(final Agent agent) + { + return agent.getPackages(); + } + + /** + * Return a List of SchemaClassId for all available Schema. + * @return a List of SchemaClassId for all available Schema. + */ + public List<SchemaClassId> getClasses() + { + List<SchemaClassId> results = new ArrayList<SchemaClassId>(); + for (Agent agent : getAgents()) + { + results.addAll(getClasses(agent)); + } + return results; + } + + /** + * Return a list of SchemaClassIds for all available Schema for the specified Agent. + * @param agent the Agent being queried + * @return a list of SchemaClassIds for all available Schema for the specified Agent. + */ + public List<SchemaClassId> getClasses(final Agent agent) + { + // First look to see if there are cached results and if there are return those. + List<SchemaClassId> results = agent.getClasses(); + if (results.size() > 0) + { + return results; + } + + String agentName = agent.getName(); + results = new ArrayList<SchemaClassId>(); + try + { + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(_replyAddress); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_query_request"); + request.setStringProperty("qpid.subject", agentName); + + // Create a QMF Query for an "SCHEMA_ID" target + request.setObject("_what", "SCHEMA_ID"); + // Wrap request & response in synchronized block in case any other threads invoke a request + // it would be somewhat unfortunate if their response got interleaved with ours!! + synchronized(this) + { + _requester.send(request); + Message response = _responder.receive(_replyTimeout*1000); + if (response == null) + { + _log.info("No response received in getClasses()"); + return Collections.emptyList(); + } + + if (AMQPMessage.isAMQPList(response)) + { + List<Map> mapResults = AMQPMessage.getList(response); + for (Map content : mapResults) + { +//new SchemaClassId(content).listValues(); + results.add(new SchemaClassId(content)); + } + } + else if (AMQPMessage.isAMQPMap(response)) + { + // Error responses are returned as MapMessages, though they are being ignored here. + //System.out.println("Console.getClasses() no results for " + agentName); + //QmfData exception = new QmfData(AMQPMessage.getMap(response)); + //System.out.println(agentName + " " + exception.getStringValue("error_text")); + } + else + { + _log.info("getClasses() Received response message in incorrect format"); + } + } + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in getClasses()", jmse.getMessage()); + } + agent.setClasses(results); + return results; + } + + /** + * Return a list of all available class SchemaClass across all known Agents. If an optional Agent is + * provided, restrict the returned schema to those supported by that Agent. + * <p> + * This call will return cached information if it is available. If not, it will send a query message + * to the remote agent and block waiting for a response. The timeout argument specifies the maximum time + * to wait for a response from the agent. + * + * @param schemaClassId the SchemaClassId we wish to return schema information for. + */ + public List<SchemaClass> getSchema(final SchemaClassId schemaClassId) + { + List<SchemaClass> results = new ArrayList<SchemaClass>(); + List<Agent> agentList = getAgents(); + for (Agent agent : agentList) + { + results.addAll(getSchema(schemaClassId, agent)); + } + return results; + } + + /** + * Return a list of all available class SchemaClass from a specified Agent. + * <p> + * This call will return cached information if it is available. If not, it will send a query message + * to the remote agent and block waiting for a response. The timeout argument specifies the maximum time + * to wait for a response from the agent. + * + * @param schemaClassId the SchemaClassId we wish to return schema information for. + * @param agent the Agent we want to retrieve the schema from + */ + public List<SchemaClass> getSchema(final SchemaClassId schemaClassId, final Agent agent) + { + // First look to see if there are cached results and if there are return those. + List<SchemaClass> results = agent.getSchema(schemaClassId); + if (results.size() > 0) + { + return results; + } + + String agentName = agent.getName(); +//System.out.println("getSchema for agent " + agentName); + results = new ArrayList<SchemaClass>(); + try + { + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(_replyAddress); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_query_request"); + request.setStringProperty("qpid.subject", agentName); + + // Create a QMF Query for an "SCHEMA" target + request.setObject("_what", "SCHEMA"); + request.setObject("_schema_id", schemaClassId.mapEncode()); + + // Wrap request & response in synchronized block in case any other threads invoke a request + // it would be somewhat unfortunate if their response got interleaved with ours!! + synchronized(this) + { + _requester.send(request); + Message response = _responder.receive(_replyTimeout*1000); + if (response == null) + { + _log.info("No response received in getSchema()"); + return Collections.emptyList(); + } + + if (AMQPMessage.isAMQPList(response)) + { + List<Map> mapResults = AMQPMessage.getList(response); + for (Map content : mapResults) + { + SchemaClass schema = new SchemaObjectClass(content); + if (schema.getClassId().getType().equals("_event")) + { + schema = new SchemaEventClass(content); + } +//schema.listValues(); + results.add(schema); + } + } + else if (AMQPMessage.isAMQPMap(response)) + { + // Error responses are returned as MapMessages, though they are being ignored here. + //System.out.println("Console.getSchema() no results for " + agentName); + //QmfData exception = new QmfData(AMQPMessage.getMap(response)); + //System.out.println(agentName + " " + exception.getStringValue("error_text")); + } + else + { + _log.info("getSchema() Received response message in incorrect format"); + } + } + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in getSchema()", jmse.getMessage()); + } + agent.setSchema(schemaClassId, results); + return results; + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param className the schema class name we're looking up objects for. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String className) + { + return getObjects(new SchemaClassId(className)); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param className the schema class name we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String className, final int timeout) + { + return getObjects(new SchemaClassId(className), timeout); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param className the schema class name we're looking up objects for. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String className, final List<Agent> agentList) + { + return getObjects(new SchemaClassId(className), agentList); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param className the schema class name we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String className, final int timeout, final List<Agent> agentList) + { + return getObjects(new SchemaClassId(className), timeout, agentList); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param packageName the schema package name we're looking up objects for. + * @param className the schema class name we're looking up objects for. + * @return a List of QMF Objects describing that class + */ + public List<QmfConsoleData> getObjects(final String packageName, final String className) + { + return getObjects(new SchemaClassId(packageName, className)); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param packageName the schema package name we're looking up objects for. + * @param className the schema class name we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String packageName, final String className, final int timeout) + { + return getObjects(new SchemaClassId(packageName, className), timeout); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param packageName the schema package name we're looking up objects for. + * @param className the schema class name we're looking up objects for. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String packageName, final String className, final List<Agent> agentList) + { + return getObjects(new SchemaClassId(packageName, className), agentList); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param packageName the schema package name we're looking up objects for. + * @param className the schema class name we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final String packageName, final String className, + final int timeout, final List<Agent> agentList) + { + return getObjects(new SchemaClassId(packageName, className), timeout, agentList); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param query the SchemaClassId or ObjectId we're looking up objects for. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final QmfData query) + { + return getObjects(query, _replyTimeout, getAgents()); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param query the SchemaClassId or ObjectId we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final QmfData query, final int timeout) + { + return getObjects(query, timeout, getAgents()); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param query the SchemaClassId or ObjectId we're looking up objects for. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final QmfData query, final List<Agent> agentList) + { + return getObjects(query, _replyTimeout, agentList); + } + + /** + * Perform a blocking query for QmfConsoleData objects. Returns a list (possibly empty) of matching objects + * This method will block until all known Agents reply, or the timeout expires. Once the timeout expires, all + * data retrieved to date is returned. + * + * @param query the SchemaClassId or ObjectId we're looking up objects for. + * @param timeout overrides the default replyTimeout. + * @param agentList if this parameter is supplied then the query is sent to only those Agents. + * @return a List of QMF Objects describing that class. + */ + public List<QmfConsoleData> getObjects(final QmfData query, final int timeout, final List<Agent> agentList) + { + List<QmfConsoleData> results = new ArrayList<QmfConsoleData>(); + for (Agent agent : agentList) + { + results.addAll(getObjects(agent, query, null, timeout)); + } + return results; + } + + /** + * Creates a subscription to the agent using the given Query. + * <p> + * The consoleHandle is an application-provided handle that will accompany each subscription update sent from + * the Agent. Subscription updates will appear as SUBSCRIPTION_INDICATION WorkItems on the Console's work queue. + * + * @param agent the Agent on which to create the subscription. + * @param query the Query to perform on the Agent + * @param consoleHandle an application-provided handle that will accompany each subscription update sent + * from the Agent. + */ + public SubscribeParams createSubscription(final Agent agent, final QmfQuery query, + final String consoleHandle) throws QmfException + { + return createSubscription(agent, query, consoleHandle, null); + } + + /** + * Creates a subscription to the agent using the given Query. + * <p> + * The consoleHandle is an application-provided handle that will accompany each subscription update sent from + * the Agent. Subscription updates will appear as SUBSCRIPTION_INDICATION WorkItems on the Console's work queue. + * <p> + * The publishInterval is the requested time interval in seconds on which the Agent should publish updates. + * <p> + * The lifetime parameter is the requested time interval in seconds for which this subscription should remain in + * effect. Both the requested lifetime and publishInterval may be overridden by the Agent, as indicated in the + * subscription response. + * <p> + * This method may be called asynchronously by providing a replyHandle argument. When called + * asynchronously, the result of this method call is returned in a SUBSCRIBE_RESPONSE WorkItem with a + * handle matching the value of replyHandle. + * <p> + * Timeout can be used to override the console's default reply timeout. + * <p> + * When called synchronously, this method returns a SubscribeParams object containing the result of the + * subscription request. + * + * @param agent the Agent on which to create the subscription. + * @param query the Query to perform on the Agent + * @param consoleHandle an application-provided handle that will accompany each subscription update sent + * from the Agent. + * @param options a String representation of a Map containing the options in the form + * <pre>"{lifetime:<value>, publishInterval:<value>, replyHandle:<value>, timeout:<value>}"</pre> + * they are optional and may appear in any order. + * <pre> + * <b>lifetime</b> the requested time interval in seconds for which this subscription should remain in effect. + * <b>publishInterval</b> the requested time interval in seconds on which the Agent should publish updates + * <b>replyHandle</b> the correlation handle used to tie asynchronous method requests with responses. + * <b>timeout</b> the time to wait for a reply from the Agent. + * </pre> + */ + public synchronized SubscribeParams createSubscription(final Agent agent, final QmfQuery query, + final String consoleHandle, final String options) throws QmfException + { + if (consoleHandle == null) + { + throw new QmfException("Called createSubscription() with null consoleHandle"); + } + if (_subscriptionByHandle.get(consoleHandle) != null) + { + throw new QmfException("Called createSubscription() with a consoleHandle that is already in use"); + } + if (agent == null) + { + throw new QmfException("Called createSubscription() with null agent"); + } + if (!agent.isActive()) + { + throw new QmfException("Called createSubscription() with inactive agent"); + } + String agentName = agent.getName(); + + // Initialise optional values to defaults; + long lifetime = _subscriptionDuration; + long publishInterval = 10000; + long timeout = _replyTimeout; + String replyHandle = null; + + if (options != null) + { // We wrap the Map in a QmfData object to avoid potential class cast issues with the parsed options + QmfData optMap = new QmfData(new AddressParser(options).map()); + if (optMap.hasValue("lifetime")) + { + lifetime = optMap.getLongValue("lifetime"); + } + + if (optMap.hasValue("publishInterval")) + { // Multiply publishInterval by 1000 because the QMF2 protocol spec says interval is + // "The request time (in milliseconds) between periodic updates of data in this subscription" + publishInterval = 1000*optMap.getLongValue("publishInterval"); + } + + if (optMap.hasValue("timeout")) + { + timeout = optMap.getLongValue("timeout"); + } + + if (optMap.hasValue("replyHandle")) + { + replyHandle = optMap.getStringValue("replyHandle"); + } + } + + try + { + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(_asyncReplyAddress); // Deliberately forcing all replies to the _asyncReplyAddress + request.setJMSCorrelationID(consoleHandle); // Deliberately using consoleHandle not replyHandle here + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_subscribe_request"); + request.setStringProperty("qpid.subject", agentName); + + request.setObject("_query", query.mapEncode()); + request.setObject("_interval", publishInterval); + request.setObject("_duration", lifetime); + + SubscriptionManager subscription = + new SubscriptionManager(agent, query, consoleHandle, replyHandle, publishInterval, lifetime); + _subscriptionByHandle.put(consoleHandle, subscription); + _timer.schedule(subscription, 0, publishInterval); + + if (_subscriptionEmulationEnabled && agentName.equals(_brokerAgentName)) + { // If the Agent is the broker Agent we emulate the Subscription on the Console + String subscriptionId = UUID.randomUUID().toString(); + _subscriptionById.put(subscriptionId, subscription); + subscription.setSubscriptionId(subscriptionId); + final SubscribeParams params = new SubscribeParams(consoleHandle, subscription.mapEncode()); + if (replyHandle == null) + { + return params; + } + else + { + final String handle = replyHandle; + Thread thread = new Thread() + { + public void run() + { + _eventListener.onEvent(new SubscribeResponseWorkItem(new Handle(handle), params)); + } + }; + thread.start(); + } + return null; + } + + _requester.send(request); + if (replyHandle == null) + { // If this is an synchronous request get the response + subscription.await(timeout*1000); + if (subscription.getSubscriptionId() == null) + { + _log.info("No response received in createSubscription()"); + throw new QmfException("No response received for Console.createSubscription()"); + } + return new SubscribeParams(consoleHandle, subscription.mapEncode()); + } + + // If this is an asynchronous request return without waiting for a response + return null; + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in createSubscription()", jmse.getMessage()); + throw new QmfException(jmse.getMessage()); + } + } // end of createSubscription() + + /** + * Renews a subscription identified by SubscriptionId. + * + * @param subscriptionId the ID of the subscription to be refreshed + */ + public void refreshSubscription(final String subscriptionId) throws QmfException + { + refreshSubscription(subscriptionId, null); + } + + /** + * Renews a subscription identified by SubscriptionId. + * <p> + * The Console may request a new subscription duration by providing a requested lifetime. This method may be called + * asynchronously by providing a replyHandle argument. + * <p> + * When called asynchronously, the result of this method call is returned in a SUBSCRIBE_RESPONSE WorkItem. + * <p> + * Timeout can be used to override the console's default reply timeout. + * <p> + * When called synchronously, this method returns a class SubscribeParams object containing the result of the + * subscription request. + * + * @param subscriptionId the ID of the subscription to be refreshed + * @param options a String representation of a Map containing the options in the form + * <pre>"{lifetime:<value>, replyHandle:<value>, timeout:<value>}"</pre> + * they are optional and may appear in any order. + * <pre> + * <b>lifetime</b> requests a new subscription duration. + * <b>replyHandle</b> the correlation handle used to tie asynchronous method requests with responses. + * <b>timeout</b> the time to wait for a reply from the Agent. + * </pre> + */ + public SubscribeParams refreshSubscription(String subscriptionId, final String options) throws QmfException + { + if (subscriptionId == null) + { + throw new QmfException("Called refreshSubscription() with null subscriptionId"); + } + SubscriptionManager subscription = _subscriptionById.get(subscriptionId); + if (subscription == null) + { + throw new QmfException("Called refreshSubscription() with invalid subscriptionId"); + } + String consoleHandle = subscription.getConsoleHandle(); + Agent agent = subscription.getAgent(); + if (!agent.isActive()) + { + throw new QmfException("Called refreshSubscription() with inactive agent"); + } + String agentName = agent.getName(); + + // Initialise optional values to defaults; + long lifetime = 0; + long timeout = _replyTimeout; + String replyHandle = null; + + if (options != null) + { // We wrap the Map in a QmfData object to avoid potential class cast issues with the parsed options + QmfData optMap = new QmfData(new AddressParser(options).map()); + if (optMap.hasValue("lifetime")) + { + lifetime = optMap.getLongValue("lifetime"); + } + + if (optMap.hasValue("timeout")) + { + timeout = optMap.getLongValue("timeout"); + } + + if (optMap.hasValue("replyHandle")) + { + replyHandle = optMap.getStringValue("replyHandle"); + } + } + + try + { + Destination destination = (replyHandle == null) ? _replyAddress : _asyncReplyAddress; + MapMessage request = _syncSession.createMapMessage(); + request.setJMSReplyTo(destination); + request.setJMSCorrelationID(replyHandle); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_subscribe_refresh_indication"); + request.setStringProperty("qpid.subject", agentName); + + request.setObject("_subscription_id", subscriptionId); + if (lifetime > 0) + { + request.setObject("_duration", lifetime); + } + + // Wrap request & response in synchronized block in case any other threads invoke a request + // it would be somewhat unfortunate if their response got interleaved with ours!! + synchronized(this) + { + if (_subscriptionEmulationEnabled && agentName.equals(_brokerAgentName)) + { // If the Agent is the broker Agent we emulate the Subscription on the Console + subscription.refresh(); + final SubscribeParams params = new SubscribeParams(consoleHandle, subscription.mapEncode()); + if (replyHandle == null) + { + return params; + } + else + { + final String handle = replyHandle; + Thread thread = new Thread() + { + public void run() + { + _eventListener.onEvent(new SubscribeResponseWorkItem(new Handle(handle), params)); + } + }; + thread.start(); + } + return null; + } + + _requester.send(request); + if (replyHandle == null) + { // If this is an synchronous request get the response + Message response = _responder.receive(timeout*1000); + if (response == null) + { + subscription.cancel(); + _log.info("No response received in refreshSubscription()"); + throw new QmfException("No response received for Console.refreshSubscription()"); + } + SubscribeParams result = new SubscribeParams(consoleHandle, AMQPMessage.getMap(response)); + subscriptionId = result.getSubscriptionId(); + if (subscriptionId == null) + { + subscription.cancel(); + } + else + { + subscription.setDuration(result.getLifetime()); + subscription.refresh(); + } + return result; + } + } + // If this is an asynchronous request return without waiting for a response + return null; + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in refreshSubscription()", jmse.getMessage()); + throw new QmfException(jmse.getMessage()); + } + } // end of refreshSubscription() + + /** + * Terminates the given subscription. + * + * @param subscriptionId the ID of the subscription to be cancelled + */ + public void cancelSubscription(final String subscriptionId) throws QmfException + { + if (subscriptionId == null) + { + throw new QmfException("Called cancelSubscription() with null subscriptionId"); + } + SubscriptionManager subscription = _subscriptionById.get(subscriptionId); + if (subscription == null) + { + throw new QmfException("Called cancelSubscription() with invalid subscriptionId"); + } + String consoleHandle = subscription.getConsoleHandle(); + Agent agent = subscription.getAgent(); + if (!agent.isActive()) + { + throw new QmfException("Called cancelSubscription() with inactive agent"); + } + String agentName = agent.getName(); + + try + { + MapMessage request = _syncSession.createMapMessage(); + request.setStringProperty("x-amqp-0-10.app-id", "qmf2"); + request.setStringProperty("method", "request"); + request.setStringProperty("qmf.opcode", "_subscribe_cancel_indication"); + request.setStringProperty("qpid.subject", agentName); + request.setObject("_subscription_id", subscriptionId); + + synchronized(this) + { + if (!_subscriptionEmulationEnabled || !agentName.equals(_brokerAgentName)) + { + _requester.send(request); + } + } + subscription.cancel(); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in cancelSubscription()", jmse.getMessage()); + } + } +} diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/EventReceivedWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/EventReceivedWorkItem.java new file mode 100644 index 0000000000..311acf3dbb --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/EventReceivedWorkItem.java @@ -0,0 +1,63 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfEvent; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * EVENT_RECEIVED: When an Agent generates a QmfEvent an EVENT_RECEIVED WorkItem is pushed onto the work-queue. + * The WorkItem's getParam() call returns a map which contains a reference to the Console Agent + * instance that generated the Event and a reference to the QmfEvent itself. The Agent reference + * is indexed from the map using the key string "agent, The QmfEvent reference is indexed from + * the map using the key string "event". There is no handle associated with this WorkItem. + * </pre> + * @author Fraser Adams + */ + +public final class EventReceivedWorkItem extends AgentAccessWorkItem +{ + /** + * Construct a EventReceivedWorkItem. Convenience constructor not in API + * + * @param agent the Agent used to populate the WorkItem's param + * @param event the QmfEvent used to populate the WorkItem's param + */ + public EventReceivedWorkItem(final Agent agent, final QmfEvent event) + { + super(WorkItemType.EVENT_RECEIVED, null, newParams(agent, event)); + } + + /** + * Return the QmfEvent stored in the params Map. + * @return the QmfEvent stored in the params Map. + */ + public QmfEvent getEvent() + { + Map<String, Object> p = this.<Map<String, Object>>getParams(); + return (QmfEvent)p.get("event"); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResponseWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResponseWorkItem.java new file mode 100644 index 0000000000..01dde0b6b6 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResponseWorkItem.java @@ -0,0 +1,65 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * METHOD_RESPONSE: The METHOD_RESPONSE WorkItem is generated in response to an asynchronous invokeMethod made + * by a QmfConsoleData object. + * + * The getParams() method of a METHOD_RESPONSE WorkItem will return a MethodResult object. + * The getHandle() method returns the reply handle provided to the method call. + * This handle is merely the handle used for the asynchronous response, it is not associated + * with the QmfConsoleData in any other way. + * </pre> + * @author Fraser Adams + */ + +public final class MethodResponseWorkItem extends WorkItem +{ + /** + * Construct a MethodResponseWorkItem. Convenience constructor not in API + * + * @param handle the reply handle used to associate requests and responses + * @param params the MethodCallParams used to populate the WorkItem's param + */ + public MethodResponseWorkItem(final Handle handle, final MethodResult params) + { + super(WorkItemType.METHOD_RESPONSE, handle, params); + } + + /** + * Return the MethodResult stored in the params. + * @return the MethodResult stored in the params. + */ + public MethodResult getMethodResult() + { + return (MethodResult)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResult.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResult.java new file mode 100644 index 0000000000..8822e02752 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/MethodResult.java @@ -0,0 +1,151 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfException; + +/** + * The value(s) returned to the Console when the method call completes are represented by the MethodResult class. + * <p> + * The MethodResult class indicates whether the method call succeeded or not, and, on success, provides access to all + * data returned by the method call. + * <p> + * Returned data is provided in QmfData map indexed by the name of the parameter. The QmfData map contains only those + * parameters that are classified as "output" by the SchemaMethod. + * <p> + * Should a method call result in a failure, this failure is indicated by the presence of an error object in + * the MethodResult. This object is represented by a QmfException object, which contains a description of the + * reason for the failure. There are no returned parameters when a method call fails. + * <p> + * Although not part of the QMF2 API I've made MethodResult extend QmfData so we can directly access the argument + * or exception values of the MethodResult object, which tends to neaten up client code. + * + * @author Fraser Adams + */ +public final class MethodResult extends QmfData +{ + private QmfData _arguments = null; + private QmfData _exception = null; + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the MethodResult. + */ + @SuppressWarnings("unchecked") + public MethodResult(final Map m) + { + super(m); + _exception = this; + String opcode = (m == null || !m.containsKey("qmf.opcode")) ? "none" : (String)m.get("qmf.opcode"); + if (m.size() == 0) + { // Valid response from a method returning void + _values = m; + _arguments = this; + _exception = null; + } + else if (opcode.equals("_method_response")) + { + Map args = (Map)m.get("_arguments"); + if (args != null) + { + _values = args; + _arguments = this; + _exception = null; + } + } + else if (!opcode.equals("_exception")) + { + setValue("error_text", "Invalid response received, opcode: " + opcode); + } + } + + /** + * Return true if the method call executed without error. + * @return true if the method call executed without error. + */ + public boolean succeeded() + { + return (_exception == null); + } + + /** + * Return the QmfData error object if method fails, else null. + * @return the QmfData error object if method fails, else null. + */ + public QmfData getException() + { + return _exception; + } + + /** + * Return a map of "name"=<value> pairs of all returned arguments. + * @return a map of "name"=<value> pairs of all returned arguments. + */ + public QmfData getArguments() + { + return _arguments; + } + + /** + * Return value of argument named "name". + * @return value of argument named "name". + */ + public Object getArgument(final String name) + { + if (_arguments == this) + { + return getValue(name); + } + return null; + } + + /** + * Return a QmfException object. + * @return a QmfException object. + * <p> + * If the QmfData exception object contains a String property "error_text" or "message" return a QmfException object + * who's message is set to this value else return null; + */ + public QmfException getQmfException() + { + if (_exception == this) + { + if (hasValue("error_text")) + { + return new QmfException(getStringValue("error_text")); + } + + if (hasValue("message")) + { + return new QmfException(getStringValue("message")); + } + } + return null; + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/ObjectUpdateWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/ObjectUpdateWorkItem.java new file mode 100644 index 0000000000..13a197b121 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/ObjectUpdateWorkItem.java @@ -0,0 +1,65 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * OBJECT_UPDATE: The OBJECT_UPDATE WorkItem is generated in response to an asynchronous refresh made by + * a QmfConsoleData object. + * + * The getParams() method of an OBJECT_UPDATE WorkItem will return a QmfConsoleData. + * The getHandle() method returns the reply handle provided to the refresh() method call. + * This handle is merely the handle used for the asynchronous response, it is not associated + * with the QmfConsoleData in any other way. + * </pre> + * @author Fraser Adams + */ + +public final class ObjectUpdateWorkItem extends WorkItem +{ + /** + * Construct a ObjectUpdateWorkItem. Convenience constructor not in API + * + * @param handle the reply handle used to associate requests and responses + * @param params the QmfConsoleData used to populate the WorkItem's param + */ + public ObjectUpdateWorkItem(final Handle handle, final QmfConsoleData params) + { + super(WorkItemType.OBJECT_UPDATE, handle, params); + } + + /** + * Return the QmfConsoleData stored in the params. + * @return the QmfConsoleData stored in the params. + */ + public QmfConsoleData getQmfConsoleData() + { + return (QmfConsoleData)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/QmfConsoleData.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/QmfConsoleData.java new file mode 100644 index 0000000000..10d764d3d0 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/QmfConsoleData.java @@ -0,0 +1,277 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +// Misc Imports +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.ObjectId; +import org.apache.qpid.qmf2.common.QmfData; +import org.apache.qpid.qmf2.common.QmfException; +import org.apache.qpid.qmf2.common.QmfManaged; +import org.apache.qpid.qmf2.common.SchemaClassId; + +/** + * Subclass of QmfManaged to provide a Console specific representation of management data. + * <p> + * The Console application represents a managed data object by the QmfConsoleData class. The Console has "read only" + * access to the data values in the data object via this class. The Console can also invoke the methods defined by + * the object via this class. + * <p> + * The actual data stored in this object is cached from the Agent. In order to update the cached values, + * the Console invokes the instance's refresh() method. + * <p> + * Note that the refresh() and invokeMethod() methods require communication with the remote Agent. As such, they + * may block. For these two methods, the Console has the option of blocking in the call until the call completes. + * Optionally, the Console can receive a notification asynchronously when the operation is complete. + * + * @author Fraser Adams + */ +public class QmfConsoleData extends QmfManaged +{ + private final Agent _agent; + private long _updateTimestamp; + private long _createTimestamp; + private long _deleteTimestamp; + + /** + * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map. + * + * @param m the map used to construct the SchemaClass. + * @param a the Agent that manages this object. + */ + public QmfConsoleData(final Map m, final Agent a) + { + super(m); + long currentTime = System.currentTimeMillis()*1000000l; + _updateTimestamp = m.containsKey("_update_ts") ? getLong(m.get("_update_ts")) : currentTime; + _createTimestamp = m.containsKey("_create_ts") ? getLong(m.get("_create_ts")) : currentTime; + _deleteTimestamp = m.containsKey("_delete_ts") ? getLong(m.get("_delete_ts")) : currentTime; + _agent = a; + } + + /** + * Sets the state of the QmfConsoleData, used as an assignment operator. + * + * @param m the Map used to initialise the QmfConsoleData + */ + @SuppressWarnings("unchecked") + public void initialise(final Map m) + { + Map<String, Object> values = (Map<String, Object>)m.get("_values"); + _values = (values == null) ? m : values; + + Map<String, String> subtypes = (Map<String, String>)m.get("_subtypes"); + _subtypes = subtypes; + + setSchemaClassId(new SchemaClassId((Map)m.get("_schema_id"))); + setObjectId(new ObjectId((Map)m.get("_object_id"))); + + long currentTime = System.currentTimeMillis()*1000000l; + _updateTimestamp = m.containsKey("_update_ts") ? getLong(m.get("_update_ts")) : currentTime; + _createTimestamp = m.containsKey("_create_ts") ? getLong(m.get("_create_ts")) : currentTime; + _deleteTimestamp = m.containsKey("_delete_ts") ? getLong(m.get("_delete_ts")) : currentTime; + } + + /** + * Sets the state of the QmfConsoleData, used as an assignment operator. + * + * @param rhs the QmfConsoleData used to initialise the QmfConsoleData + */ + public void initialise(final QmfConsoleData rhs) + { + _values = rhs._values; + _subtypes = rhs._subtypes; + setSchemaClassId(rhs.getSchemaClassId()); + setObjectId(rhs.getObjectId()); + _updateTimestamp = rhs._updateTimestamp; + _createTimestamp = rhs._createTimestamp; + _deleteTimestamp = rhs._deleteTimestamp; + } + + /** + * Return a list of timestamps describing the lifecycle of the object. + * @return a list of timestamps describing the lifecycle of the object. + * <p> + * All timestamps are represented by the AMQP timestamp type recorded in nanoseconds since the epoch. + * <pre> + * [0] = time of last update from Agent, + * [1] = creation timestamp + * [2] = deletion timestamp, or zero if not deleted. + * </pre> + */ + public final long[] getTimestamps() + { + long[] timestamps = {_updateTimestamp, _createTimestamp, _deleteTimestamp}; + return timestamps; + } + + /** + * Return the creation timestamp. + * @return the creation timestamp. Timestamps are recorded in nanoseconds since the epoch. + */ + public final long getCreateTime() + { + return _createTimestamp; + } + + /** + * Return the update timestamp. + * @return the update timestamp. Timestamps are recorded in nanoseconds since the epoch. + */ + public final long getUpdateTime() + { + return _updateTimestamp; + } + + /** + * Return the deletion timestamp, or zero if not deleted. + * @return the deletion timestamp, or zero if not deleted. Timestamps are recorded in nanoseconds since the epoch. + */ + public final long getDeleteTime() + { + return _deleteTimestamp; + } + + /** + * Return true if deletion timestamp not zero. + * @return true if deletion timestamp not zero. + */ + public final boolean isDeleted() + { + return getDeleteTime() != 0; + } + + /** + * Request that the Agent updates the value of this object's contents. + */ + public final void refresh() throws QmfException + { + refresh(-1); + } + + /** + * Request that the Agent updates the value of this object's contents. + * + * @param timeout the maximum time in seconds to wait for a response, overrides default replyTimeout. + */ + public final void refresh(final int timeout) throws QmfException + { + if (_agent == null) + { + throw new QmfException("QmfConsoleData.refresh() called with null Agent"); + } + QmfConsoleData newContents = _agent.refresh(getObjectId(), null, timeout); + if (newContents == null) + { + _deleteTimestamp = System.currentTimeMillis()*1000000l; + } + else + { + // Save the original values of create and delete timestamps as the ManagementAgent doesn't return + // these on a call to getObjects(ObjectId); + long createTimestamp = _createTimestamp; + long deleteTimestamp = _deleteTimestamp; + initialise(newContents); + _createTimestamp = createTimestamp; + _deleteTimestamp = deleteTimestamp; + } + } + + /** + * Request that the Agent updates the value of this object's contents asynchronously. + * + * @param replyHandle the correlation handle used to tie asynchronous refresh requests with responses. + */ + public final void refresh(final String replyHandle) throws QmfException + { + if (_agent == null) + { + throw new QmfException("QmfConsoleData.refresh() called with null Agent"); + } + _agent.refresh(getObjectId(), replyHandle, -1); + } + + /** + * Invoke the named method on this instance. + * + * @param name name of the method to invoke. + * @param inArgs inArgs an unordered set of key/value pairs comprising the method arguments. + * @return the MethodResult. + */ + public final MethodResult invokeMethod(final String name, final QmfData inArgs) throws QmfException + { + if (_agent == null) + { + throw new QmfException("QmfConsoleData.invokeMethod() called with null Agent"); + } + return _agent.invokeMethod(getObjectId(), name, inArgs, -1); + } + + /** + * Invoke the named method on this instance. + * + * @param name name of the method to invoke. + * @param inArgs inArgs an unordered set of key/value pairs comprising the method arguments. + * @param timeout the maximum time in seconds to wait for a response, overrides default replyTimeout. + * @return the MethodResult. + */ + public final MethodResult invokeMethod(final String name, final QmfData inArgs, final int timeout) throws QmfException + { + if (_agent == null) + { + throw new QmfException("QmfConsoleData.invokeMethod() called with null Agent"); + } + return _agent.invokeMethod(getObjectId(), name, inArgs, timeout); + } + + /** + * Invoke the named method asynchronously on this instance. + * + * @param name name of the method to invoke. + * @param inArgs inArgs an unordered set of key/value pairs comprising the method arguments. + * @param replyHandle the correlation handle used to tie asynchronous method requests with responses. + */ + public final void invokeMethod(final String name, final QmfData inArgs, final String replyHandle) throws QmfException + { + if (_agent == null) + { + throw new QmfException("QmfConsoleData.invokeMethod() called with null Agent"); + } + _agent.invokeMethod(getObjectId(), name, inArgs, replyHandle); + } + + /** + * Helper/debug method to list the QMF Object properties and their type. + */ + @Override + public void listValues() + { + super.listValues(); + System.out.println("_create_ts: " + new Date(getCreateTime()/1000000l)); + System.out.println("_update_ts: " + new Date(getUpdateTime()/1000000l)); + System.out.println("_delete_ts: " + new Date(getDeleteTime()/1000000l)); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeIndication.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeIndication.java new file mode 100644 index 0000000000..8277e3c0e1 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeIndication.java @@ -0,0 +1,66 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.List; + +/** + * Holds the result of a subscription data indication from the Agent. + * + * @author Fraser Adams + */ +public final class SubscribeIndication +{ + private final String _consoleHandle; + private final List<QmfConsoleData> _data; + + /** + * Construct a SubscribeIndication from a consoleHandle and list of QmfConsoleData. + * @param consoleHandle the handle containing the correlation ID. + * @param data the list of QmfConsoleData to pass to the Console application. + */ + public SubscribeIndication(final String consoleHandle, final List<QmfConsoleData> data) + { + _consoleHandle = consoleHandle; + _data = data; + } + + /** + * Return the console handle as passed to the createSubscription() call. + * @return the console handle as passed to the createSubscription() call. + */ + public String getConsoleHandle() + { + return _consoleHandle; + } + + /** + * Return a list containing all updated QmfData objects associated with the Subscripion. + * @return a list containing all updated QmfData objects associated with the Subscripion. + */ + public List<QmfConsoleData> getData() + { + return _data; + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeParams.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeParams.java new file mode 100644 index 0000000000..2e15a3153b --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeParams.java @@ -0,0 +1,130 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfData; + +/** + * Holds the result of a subscription request made by this Console. + * <p> + * The SubscriptionId object must be used when the subscription is refreshed or cancelled - it must be passed to the + * Console's refreshSubscription() and cancel_subscription() methods. The value of the SubscriptionId does not + * change over the lifetime of the subscription. + * <p> + * The console handle will be provided by the Agent on each data indication event that corresponds to this subscription. + * It should not change for the lifetime of the subscription. + * <p> + * The getHandle() method returns the reply handle provided to the createSubscription() method call. This handle + * is merely the handle used for the asynchronous response, it is not associated with the subscription in any other way. + * <p> + * Once a subscription is created, the Agent that maintains the subscription will periodically issue updates for the + * subscribed data. This update will contain the current values of the subscribed data, and will appear as the first + * SUBSCRIPTION_INDICATION WorkItem for this subscription. + * + * @author Fraser Adams + */ +public final class SubscribeParams extends QmfData +{ + private String _consoleHandle; + + /** + * Construct SubscribeParams from a consoleHandle and the Map encoded representation. + * @param consoleHandle the console handle as passed to the createSubscription() call. + * @param m a Map containing the Map encoded representation of this SubscribeParams. + */ + public SubscribeParams(final String consoleHandle, final Map m) + { + super(m); + _consoleHandle = consoleHandle; + } + + /** + * If the subscription is successful, this method returns a SubscriptionId object. + * Should the subscription fail, this method returns null, and getError() can be used to obtain an + * application-specific QmfData error object. + * + * @return a SubscriptionId object. + */ + public String getSubscriptionId() + { + if (hasValue("_subscription_id")) + { + return getStringValue("_subscription_id"); + } + return null; + } + + /** + * Return the time interval in seconds on which the Agent will publish updates for this subscription. + * @return the time interval in seconds on which the Agent will publish updates for this subscription. + */ + public long getPublishInterval() + { + return getLongValue("_interval"); + } + + /** + * Return the lifetime in seconds for the subscription. + * @return the lifetime in seconds for the subscription. The subscription will automatically expire after + * this interval if not renewed by the console. + */ + public long getLifetime() + { + return getLongValue("_duration"); + } + + /** + * Return the QmfData error object if method fails, else null. + * @return the QmfData error object if method fails, else null. + */ + public QmfData getError() + { + if (getSubscriptionId() == null) + { + return this; + } + return null; + } + + /** + * Sets the consoleHandle. + * @param consoleHandle the new console handle. + */ + public void setConsoleHandle(final String consoleHandle) + { + _consoleHandle = consoleHandle; + } + + /** + * Return the console handle as passed to the createSubscription() call. + * @return the console handle as passed to the createSubscription() call. + */ + public String getConsoleHandle() + { + return _consoleHandle; + } +} + + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeResponseWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeResponseWorkItem.java new file mode 100644 index 0000000000..968ef0a6a2 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscribeResponseWorkItem.java @@ -0,0 +1,80 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * SUBSCRIBE_RESPONSE: The SUBSCRIBE_RESPONSE WorkItem returns the result of a subscription request made by + * this Console. This WorkItem is generated when the Console's createSubscription() is + * called in an asychronous manner, rather than pending for the result. + * + * The getParams() method of a SUBSCRIBE_RESPONSE WorkItem will return an instance of the + * SubscribeParams class. + * + * The SubscriptionId object must be used when the subscription is refreshed or cancelled. + * It must be passed to the Console's refresh_subscription() and cancelSubscription() methods. + * The value of the SubscriptionId does not change over the lifetime of the subscription. + * + * The console handle will be provided by the Agent on each data indication event that + * corresponds to this subscription. It should not change for the lifetime of the subscription. + * + * The getHandle() method returns the reply handle provided to the createSubscription() + * method call. This handle is merely the handle used for the asynchronous response, it is + * not associated with the subscription in any other way. + * + * Once a subscription is created, the Agent that maintains the subscription will periodically + * issue updates for the subscribed data. This update will contain the current values of the + * subscribed data, and will appear as the first SUBSCRIPTION_INDICATION WorkItem for this + * subscription. + * </pre> + * @author Fraser Adams + */ + +public final class SubscribeResponseWorkItem extends WorkItem +{ + /** + * Construct a SubscribeResponseWorkItem. Convenience constructor not in API + * + * @param handle the reply handle used to associate requests and responses + * @param params the SubscribeParams used to populate the WorkItem's param + */ + public SubscribeResponseWorkItem(final Handle handle, final SubscribeParams params) + { + super(WorkItemType.SUBSCRIBE_RESPONSE, handle, params); + } + + /** + * Return the SubscribeParams stored in the params. + * @return the SubscribeParams stored in the params. + */ + public SubscribeParams getSubscribeParams() + { + return (SubscribeParams)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionIndicationWorkItem.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionIndicationWorkItem.java new file mode 100644 index 0000000000..d9123e9a2c --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionIndicationWorkItem.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +import java.util.Map; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.Handle; +import org.apache.qpid.qmf2.common.WorkItem; + +/** + * Descriptions below are taken from <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> + * <pre> + * SUBSCRIPTION_INDICATION: The SUBSCRIPTION_INDICATION WorkItem signals the arrival of an update to subscribed + * data from the Agent. + * + * The getParams() method of a SUBSCRIPTION_INDICATION WorkItem will return an instance + * of the SubscribeIndication class. The getHandle() method returns null. + * </pre> + * @author Fraser Adams + */ + +public final class SubscriptionIndicationWorkItem extends WorkItem +{ + /** + * Construct a SubscriptionIndicationWorkItem. Convenience constructor not in API + * + * @param params the SubscribeParams used to populate the WorkItem's param + */ + public SubscriptionIndicationWorkItem(final SubscribeIndication params) + { + super(WorkItemType.SUBSCRIPTION_INDICATION, null, params); + } + + /** + * Return the SubscribeIndication stored in the params. + * @return the SubscribeIndication stored in the params. + */ + public SubscribeIndication getSubscribeIndication() + { + return (SubscribeIndication)getParams(); + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionManager.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionManager.java new file mode 100644 index 0000000000..69889d85de --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/SubscriptionManager.java @@ -0,0 +1,255 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.console; + +// Simple Logging Facade 4 Java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Misc Imports +import java.util.HashMap; +import java.util.Map; +import java.util.TimerTask; + +// QMF2 Imports +import org.apache.qpid.qmf2.common.QmfQuery; + +/** + * A SubscriptionManager represents a running Subscription on the Console. + * <p> + * The main reason we have SubscriptionManagers as TimerTasks is to enable proper cleanup of the references stored in + * the subscriptionByHandle and subscriptionById Maps. Ideally these will be cleaned up by a client calling + * cancelSubscription but we can't rely on that as the client may forget or the Agent may not respond. + * <p> + * The SubscriptionManager acts like a client/Console side representation of a Subscription running on an Agent. + * As mentioned above its primary purpose is to enable references to Subscriptions maintained by the Console to + * be cleaned up should the Subscription time out rather than being cancelled, however as a side effect it is + * used to enable emulation of Subscriptions to the broker ManagementAgent, which does not yet natively implement + * Subscription. + * <p> + * To emulate Subscriptions the Console receives the periodic _data indications pushed by the ManagementAgent. The + * Console then iterates through Subscriptions referencing the broker Agent and evaluates their queries against + * the QmfConsoleData returned by the _data indication. Any QmfConsoleData that match the query are passed to the + * client application with the consoleHandle of the matching Subscription. + * <p> + * The following diagram illustrates the Subscription relationships with the Console and local Agent proxy. + * <p> + * <img alt="" src="doc-files/Subscriptions.png"> + * + * @author Fraser Adams + */ +public final class SubscriptionManager extends TimerTask +{ + private static final Logger _log = LoggerFactory.getLogger(SubscriptionManager.class); + + private final Agent _agent; + private long _startTime = System.currentTimeMillis(); + private String _subscriptionId; + private String _consoleHandle; + private String _replyHandle; + private QmfQuery _query; + private long _duration = 0; + private long _interval = 0; + private boolean _waiting = true; + + /** + * Construct a Console side proxy of a Subscription. Primarily to manage references to the Subscription. + * + * @param agent the Agent from which the Subscription has been requested + * @param query the QmfQuery that the Subscription will run + * @param consoleHandle the handle that uniquely identifies the Subscription + * @param interval the interval between subscription updates + * @param duration the duration of the subscription (assuming it doesn't get refreshed) + */ + SubscriptionManager(final Agent agent, final QmfQuery query, final String consoleHandle, + final String replyHandle, final long interval, final long duration) + { + _agent = agent; + _query = query; + _consoleHandle = consoleHandle; + _replyHandle = replyHandle; + _interval = interval; + _duration = duration; + _log.debug("Creating SubscriptionManager {}, on Agent {}",_consoleHandle, _agent.getName()); + } + + /** + * This method gets called periodically by the Timer scheduling this TimerTask. + * <p> + * First a check is made to see if the Subscription has expired, if it has then it is cancelled. + */ + public void run() + { + long elapsed = (long)Math.round((System.currentTimeMillis() - _startTime)/1000.0f); + if (elapsed >= _duration || !_agent.isActive()) + { + _log.debug("Subscription {} has expired, removing", _subscriptionId); + // The Subscription has expired so cancel it + cancel(); + } + } + + /** + * Causes the current thread to wait until it is signalled or times out. + * <p> + * This method is primarily used as a means to enable a synchronous call to createSubscription(). + * For most synchronous calls we simply use the receive() call on the synchronous session, but we can't do that + * for createSubscription() as we specifically need to use the replyTo on the asynchronous session as once + * subscriptions are created the results are asynchronously pushed. This means we have to get the response to + * createSession() on the asynchronous replyTo then signal the (blocked) main thread that the response has + * been received. + * + * @param timeout the maximum time to wait to be signalled. + */ + public synchronized void await(final long timeout) + { + while (_waiting) + { + long _startTime = System.currentTimeMillis(); + try + { + wait(timeout); + } + catch (InterruptedException ie) + { + continue; + } + // Measure elapsed time to test against spurious wakeups and ensure we really have timed out + long elapsedTime = (System.currentTimeMillis() - _startTime); + if (elapsedTime >= timeout) + { + break; + } + } + _waiting = true; + } + + /** + * Wakes up all waiting threads. + */ + public synchronized void signal() + { + _waiting = false; + notifyAll(); + } + + /** + * Refresh the subscription by zeroing its elapsed time. + */ + public void refresh() + { + _log.debug("Refreshing Subscription {}", _subscriptionId); + _startTime = System.currentTimeMillis(); + } + + /** + * Cancel the Subscription, tidying references up and cancelling the TimerTask. + */ + @Override + public boolean cancel() + { + _log.debug("Cancelling Subscription {}, {}", _consoleHandle, _subscriptionId); + _agent.removeSubscription(this); + signal(); // Just in case anything is blocking on this Subscription. + return super.cancel(); // Cancel the TimerTask + } + + /** + * Set the SubscriptionId. + * @param subscriptionId the new SubscriptionId of this Subscription. + */ + public void setSubscriptionId(final String subscriptionId) + { + _subscriptionId = subscriptionId; + } + + /** + * return the SubscriptionId of this Subscription. + * @return the SubscriptionId of this Subscription. + */ + public String getSubscriptionId() + { + return _subscriptionId; + } + + /** + * Return the consoleHandle of this Subscription. + * @return the consoleHandle of this Subscription. + */ + public String getConsoleHandle() + { + return _consoleHandle; + } + + /** + * Return the replyHandle of this Subscription. + * @return the replyHandle of this Subscription. + */ + public String getReplyHandle() + { + return _replyHandle; + } + + /** + * Return the Agent running this Subscription. + * @return the Agent running this Subscription. + */ + public Agent getAgent() + { + return _agent; + } + + /** + * Set the Subscription lifetime in seconds. + * + * @param duration the new Subscription lifetime in seconds + */ + public void setDuration(final long duration) + { + _duration = duration; + } + + /** + * Return The Subscription's QmfQuery. + * @return The Subscription's QmfQuery. + */ + public QmfQuery getQuery() + { + return _query; + } + + /** + * Create a Map encoded version. + * <p> + * When we do a synchronous createSubscription the Subscription itself holds the info needed to populate + * the SubscriptionParams result. We encode the info in a Map to pass to the SubscribeParams Constructor + */ + public Map<String, Object> mapEncode() + { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("_interval", _interval); + map.put("_duration", _duration); + map.put("_subscription_id", _subscriptionId); + return map; + } +} + + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Console.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Console.png Binary files differnew file mode 100644 index 0000000000..cb2a5ee800 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Console.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/QmfEventListenerModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/QmfEventListenerModel.png Binary files differnew file mode 100644 index 0000000000..26a5f71b56 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/QmfEventListenerModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Subscriptions.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Subscriptions.png Binary files differnew file mode 100644 index 0000000000..01772c6d45 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/Subscriptions.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/WorkQueueEventModel.png b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/WorkQueueEventModel.png Binary files differnew file mode 100644 index 0000000000..fc2a722985 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/console/doc-files/WorkQueueEventModel.png diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/ConnectionHelper.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/ConnectionHelper.java new file mode 100644 index 0000000000..b5655dda29 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/ConnectionHelper.java @@ -0,0 +1,862 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.util; + +// JMS Imports +import javax.jms.ConnectionFactory; +import javax.jms.Connection; +import javax.jms.JMSException; + +// JNDI Imports +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +// Simple Logging Facade 4 Java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Misc Imports +import java.util.Map; +import java.util.Properties; + +// Reuse this class as it provides a handy mechanism to parse an options String into a Map +import org.apache.qpid.messaging.util.AddressParser; + +/** + * The Qpid M4 Java and C++ clients and the Python QMF tools all use different URL formats. + * This class provides helper methods to support a variety of URL formats and connection options + * in order to provide flexibility when creating connections. + * <p> + * Much of the following information is taken from <a href="https://cwiki.apache.org/qpid/url-format-proposal.html"> + * New URL format for AMQP + Qpid</a> + * <p> + * <h3>AMQP 0-10 format</h3> + * C++ uses the AMQP 0-10 format: section 9.1.2 as follows: + * <pre> + * amqp_url = "amqp:" prot_addr_list + * prot_addr_list = [prot_addr ","]* prot_addr + * prot_addr = tcp_prot_addr | tls_prot_addr + * + * tcp_prot_addr = tcp_id tcp_addr + * tcp_id = "tcp:" | "" + * tcp_addr = [host [":" port] ] + * host = <as per <a href="http://www.ietf.org/rfc/rfc3986.txt">rfc3986</a>> + * port = number + * </pre> + * The AMQP 0-10 format only provides protocol address information for a (list of) brokers. + * <p> + * <p> + * + * <h3>Python tool BrokerURL format</h3> + * The Python tools bundled with Qpid such as qpid-config use a "BrokerURL" format with the following Address syntax: + * <pre> + * [<user>/<pass>@]<hostname> | <ip-address>[:<port>] + * </pre> + * + * <h3>Qpid M4 Java Connection URL format</h3> + * The Qpid M4 Java format provides additional options for connection options (user, password, vhost etc.) + * Documentation for this format may be found here: <a href="https://cwiki.apache.org/qpid/connection-url-format.html"> + * Qpid M4 Java Connection URL Format</a> + * <p> + * Java ConnectionURLs look like this: + * <pre> + * amqp://[<user>:<pass>@][<clientid>]/<virtualhost>[?<option>='<value>'[&<option>='<value>']] + * </pre> + * This syntax is very powerful, but it can also be fairly complex to work with, especially when one realises + * that one of the options in the above syntax is brokerlist='<broker url>' where broker url is itself a URL + * of the format: + * <pre> + * <transport>://<host>[:<port>][?<option>='<value>'[&<option>='<value>']] + * </pre> + * so one may see ConnectionURLs that look like: + * <pre> + * {@literal amqp://guest:guest@clientid/test?brokerlist='tcp://localhost:5672?retries='10'&connectdelay='1000''} + * </pre> + * + * <p> + * <p> + * <h3>Extended AMQP 0-10 URL format</h3> + * There is a proposal to extend the AMQP 0-10 URL syntax to include user:pass@ style authentication + * information, virtual host and extensible name/value options. It also makes the implied extension points of + * the original grammar more explicit. + * <pre> + * amqp_url = "amqp://" [ userinfo "@" ] addr_list [ vhost ] + * addr_list = addr *( "," addr ) + * addr = prot_addr [ options ] + * prot_addr = tcp_prot_addr | other_prot_addr + * vhost = "/" *pchar [ options ] + * + * tcp_prot_addr = tcp_id tcp_addr + * tcp_id = "tcp:" / "" ; tcp is the default + * tcp_addr = [ host [ ":" port ] ] + * + * other_prot_addr = other_prot_id ":" *pchar + * other_prot_id = scheme + * + * options = "?" option *( ";" option ) + * option = name "=" value + * name = *pchar + * value = *pchar + * </pre> + * + * <h3>Incompatibility with AMQP 0-10 format</h3> + * This syntax is backward compatible with AMQP 0-10 with one exception: AMQP 0-10 did not have an initial + * // after amqp: The justification was that that the // form is only used for URIs with hierarchical structure + * <p> + * However it's been pointed out that in fact the URL does already specify a 1-level hierarchy of address / vhost. + * In the future the hierarchy could be extended to address objects within a vhost such as queues, exchanges etc. + * So this proposal adopts amqp:// syntax. + * <p> + * It's easy to write a backward-compatible parser by relaxing the grammar as follows: + * <pre> + * amqp_url = "amqp:" [ "//" ] [ userinfo "@" ] addr_list [ vhost ] + * </pre> + * + * <h3>Differences from Qpid M4 Java Connection URL format</h3> + * Addresses are at the start of the URL rather than in the "brokerlist" option. + * <p> + * Option format is {@literal ?foo=bar;x=y } rather than {@literal ?foo='bar'&x='y'}. The use of "'" quotes is not common for URI query + * strings. The use of "&" as a separator creates problems + * <p> + * user, pass and clientid are options rather than having a special place at the front of the URL. clientid is + * a Qpid proprietary property and user/pass are not relevant in all authentication schemes. + * <p> + * Qpid M4 Java URLs requires the brokerlist option, so this is an easy way to detect a Qpid M4 URL vs. an + * Extended AMQP 0-10 URL and parse accordingly. + * + * <h3>Options</h3> + * Some of the URL forms are fairly limited in terms of options, so it is useful to be able to pass options as + * an additional string, though it's important to note that if multiple brokers are supplied in the AMQP 0.10 format + * the same options will be applied to all brokers. + * <p> + * The option format is the same as that of the C++ qpid::messaging Connection class. for example: "{reconnect: true, + * tcp-nodelay: true}": + * <p> + * <table summary="Connection Options" width="100%" border="1"><thead> + * <tr><th>option name</th><th>value type</th><th>semantics</th></tr></thead><tbody> + * <tr> + * <td><code class="literal">maxprefetch</code></td> + * <td>integer</td> + * <td>The maximum number of pre-fetched messages per destination.</td> + * </tr> + * <tr> + * <td><code class="literal">sync_publish</code></td> + * <td>{'persistent' | 'all'}</td> + * <td>A sync command is sent after every persistent message to guarantee that it has been received; if the + * value is 'persistent', this is done only for persistent messages.</td> + * </tr> + * <tr> + * <td><code class="literal">sync_ack</code></td> + * <td>boolean</td> + * <td>A sync command is sent after every acknowledgement to guarantee that it has been received.</td> + * </tr> + * <tr> + * <td><code class="literal">use_legacy_map_msg_format</code></td> + * <td>boolean</td> + * <td>If you are using JMS Map messages and deploying a new client with any JMS client older than 0.8 release, + * you must set this to true to ensure the older clients can understand the map message encoding.</td> + * </tr> + * <tr> + * <td><code class="literal">failover</code></td> + * <td>{'roundrobin' | 'singlebroker' | 'nofailover' | 'failover_exchange'}</td> + * <td>If roundrobin is selected it will try each broker given in the broker list. If failover_exchange is + * selected it connects to the initial broker given in the broker URL and will receive membership updates + * via the failover exchange. </td> + * </tr> + * <tr> + * <td><code class="literal">cyclecount</code></td> + * <td>integer</td> + * <td>For roundrobin failover cyclecount is the number of times to loop through the list of available brokers + * before failure.</td> + * </tr> + * <tr> + * <td><code class="literal">username</code></td> + * <td>string</td> + * <td>The username to use when authenticating to the broker.</td> + * </tr> + * <tr> + * <td><code class="literal">password</code></td> + * <td>string</td> + * <td>The password to use when authenticating to the broker.</td> + * </tr> + * <tr> + * <td><code class="literal">sasl_mechanisms</code></td> + * <td>string</td> + * <td>The specific SASL mechanisms to use when authenticating to the broker. The value is a space separated list.</td> + * </tr> + * <tr> + * <td><code class="literal">sasl_mechs</code></td> + * <td>string</td> + * <td>The specific SASL mechanisms to use when authenticating to the broker. The value is a space separated + * is a space separated list. This is simply a synonym for sasl_mechanisms above</td> + * </tr> + * <tr> + * <td><code class="literal">sasl_encryption</code></td> + * <td>boolean</td> + * <td>If <code class="literal">sasl_encryption='true'</code>, the JMS client attempts to negotiate a security + * layer with the broker using GSSAPI to encrypt the connection. Note that for this to happen, GSSAPI must + * be selected as the sasl_mech.</td> + * </tr> + * <tr> + * <td><code class="literal">ssl</code></td> + * <td>boolean</td> + * <td>If <code class="literal">ssl='true'</code>, the JMS client will encrypt the connection using SSL.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect</code></td> + * <td>boolean</td> + * <td>Transparently reconnect if the connection is lost.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect_timeout</code></td> + * <td>integer</td> + * <td>Total number of seconds to continue reconnection attempts before giving up and raising an exception.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect_limit</code></td> + * <td>integer</td> + * <td>Maximum number of reconnection attempts before giving up and raising an exception.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect_interval_min</code></td> + * <td>integer representing time in seconds</td> + * <td> Minimum number of seconds between reconnection attempts. The first reconnection attempt is made + * immediately; if that fails, the first reconnection delay is set to the value of <code class="literal"> + * reconnect_interval_min</code>; if that attempt fails, the reconnect interval increases exponentially + * until a reconnection attempt succeeds or <code class="literal">reconnect_interval_max</code> is reached.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect_interval_max</code></td> + * <td>integer representing time in seconds</td> + * <td>Maximum reconnect interval.</td> + * </tr> + * <tr> + * <td><code class="literal">reconnect_interval</code></td> + * <td>integer representing time in seconds</td> + * <td>Sets both <code class="literal">reconnection_interval_min</code> and <code class="literal"> + * reconnection_interval_max</code> to the same value. The default value is 5 seconds</td> + * </tr> + * <tr> + * <td><code class="literal">heartbeat</code></td> + * <td>integer representing time in seconds</td> + * <td>Requests that heartbeats be sent every N seconds. If two successive heartbeats are missed the connection is + * considered to be lost.</td> + * </tr> + * <tr> + * <td><code class="literal">protocol</code></td> + * <td>string</td> + * <td>Sets the underlying protocol used. The default option is 'tcp'. To enable ssl, set to 'ssl'. The C++ client + * additionally supports 'rdma'. </td> + * </tr> + * <tr> + * <td><code class="literal">tcp-nodelay</code></td> + * <td>boolean</td> + * <td>Set tcp no-delay, i.e. disable Nagle algorithm.</td> + * </tr> + * <tr> + * <td><code class="literal">sasl_protocol</code></td> + * <td>string</td> + * <td>Used only for Kerberos. <code class="literal">sasl_protocol</code> must be set to the principal for the + * qpidd broker, e.g. <code class="literal">qpidd/</code></td> + * </tr> + * <tr> + * <td><code class="literal">sasl_server</code></td> + * <td>string</td> + * <td>For Kerberos, sasl_mechs must be set to GSSAPI, <code class="literal">sasl_server</code> must be set to + * the host for the SASL server, e.g. <code class="literal">sasl.com.</code></td> + * </tr> + * <tr> + * <td><code class="literal">trust_store</code></td> + * <td>string</td> + * <td>path to Keberos trust store</td> + * </tr> + * <tr> + * <td><code class="literal">trust_store_password</code></td> + * <td>string</td> + * <td>Kerberos trust store password</td> + * </tr> + * <tr> + * <td><code class="literal">key_store</code></td> + * <td>string</td> + * <td>path to Kerberos key store </td> + * </tr> + * <tr> + * <td><code class="literal">key_store_password</code></td> + * <td>string</td> + * <td>Kerberos key store password</td> + * </tr> + * <tr> + * <td><code class="literal">ssl_cert_alias</code></td> + * <td>string</td> + * <td>If multiple certificates are present in the keystore, the alias will be used to extract the correct + * certificate.</td> + * </tr> + * </tbody></table> + * + * <h3>Other features of this class</h3> + * Whilst it is generally the norm to use JNDI to specify Connections, Destinations etc. it is also often quite useful + * to specify Connections programmatically, for example when writing a tool one may wish to specify the broker via the + * command line to enable the tool to connect to different broker instances. + * To facilitate this this class provides a basic createConnection() method that takes a URL and returns a JMS + * Connection. + * + * @author Fraser Adams + */ +public final class ConnectionHelper +{ + private static final Logger _log = LoggerFactory.getLogger(ConnectionHelper.class); + + /** + * Make constructor private as the class comprises only static helper methods. + */ + private ConnectionHelper() + { + } + + /** + * Create a ConnectionURL from the proposed Extended AMQP 0.10 URL format. This is experimental and may or may + * not work. Options are assumed to be the same as the Java Connection URL, which will probably not be the case + * if this URL form is ultimately adopted. For example the example URLs have "amqp://host1,host2?retry=2,host3" + * whereas the Java Connection URL uses &retries=2 + * + * I'm not overly keen on this code it looks pretty inelegant and I'm slightly embarrassed by it, but it + * is really just an experiment. + * + * @param url the input URL. + * @param username the username. + * @param password the password. + * @return a String containing the Java Connection URL. + */ + private static String parseExtendedAMQPURL(String url, String username, String password) + { + String vhost = ""; // Specifying an empty vhost uses default Virtual Host. + String urlOptions = ""; + String brokerList = ""; + + url = url.substring(7); // Chop off "amqp://" + String[] split = url.split("@"); // First break out the userinfo if present + String remainder = split[0]; + if (split.length == 2) + { // Extract username and password from the userinfo field + String[] userinfo = split[0].split(":"); + remainder = split[1]; + + username = userinfo[0]; + if (userinfo.length == 2) + { + password = userinfo[1]; + } + } + + // Replace foo=baz with foo='baz'. There's probably a more elegant way to do this using a fancy + // regex, but unfortunately I'm not terribly good at regexes so this is the brute force approach :-( + // OTOH it's probably more readable and obvious than a regex to do the same thing would be. + split = remainder.split("="); + StringBuilder buf = new StringBuilder(split[0]); + for (int i = 1; i < split.length; i++) + { + String substring = "='" + split[i]; + if (substring.contains(";")) + { + substring = substring.replaceFirst(";", "'&"); // Note we also replace the option separator here + } + else if (substring.contains("/")) + { + substring = substring.replaceFirst("/", "'/"); + } + else if (substring.contains(",")) + { + substring = substring.replaceFirst(",", "',"); + } + else + { + substring = substring + "'"; + } + buf.append(substring); + } + remainder = buf.toString(); + + // Now split into addrList and vhost parts (see Javadoc for the grammar of this URL format) + split = remainder.split("/"); // vhost starts with a mandatory '/' character + String[] addrSplit = split[0].split(","); // prot_addrs are comma separated + boolean firstBroker = true; + buf = new StringBuilder(); + for (String broker : addrSplit) + { // Iterate through the address list creating brokerList style URLs + broker = broker.trim(); + String protocol = "tcp"; // set default protocol + String[] components = broker.split(":"); + + // Note protocols other than tcp and vm are not supported by the Connection URL so the results + // are pretty much undefined if other protocols are passed on the input URL. + if (components.length == 1) + { // Assume protocol = tcp and broker = hostname + } + else if (components.length == 2) + { // Probably host:port but could be some other protocol in and Extended AMQP 0.10 URL + try + { // Somewhat ugly, but effective test to check if the second component is an integer + Integer.parseInt(components[1]); + // If the above succeeds the components are likely host:port + broker = components[0] + ":" + components[1]; + } + catch (NumberFormatException nfe) + { // If the second component isn't an integer then it's likely a wacky protocol... + protocol = components[0]; + broker = components[1]; + } + } + else if (components.length == 3) + { + protocol = components[0]; + broker = components[1] + ":" + components[2]; + } + + if (firstBroker) + { + buf.append(protocol + "://" + broker); + } + else + { // https://cwiki.apache.org/qpid/connection-url-format.html says "A minimum of one broker url is + // required additional URLs are semi-colon(';') delimited." + buf.append(";" + protocol + "://" + broker); + } + firstBroker = false; + } + brokerList = "'" + buf.toString() + "'"; + + if (split.length == 2) + { // Extract the vhost and any connection level options + vhost = split[1]; + String[] split2 = vhost.split("\\?"); // Look for options + vhost = split2[0]; + if (split2.length == 2) + { + urlOptions = "&" + split2[1]; + } + } + + String connectionURL = "amqp://" + username + ":" + password + "@QpidJMS/" + vhost + "?brokerlist=" + + brokerList + urlOptions; + return connectionURL; + } + + /** + * If no explicit username is supplied then explicitly set sasl mechanism to ANONYMOUS. If this isn't done + * The default is PLAIN which causes the broker to fail with "warning Failed to retrieve sasl username". + * + * @param username the previously extracted username. + * @param brokerListOptions the brokerList options extracted so far. + * @return the brokerList options adjusted with sasl_mechs='ANONYMOUS' if no username has been supplied. + */ + private static String adjustBrokerListOptions(final String username, final String brokerListOptions) + { + if (username.equals("")) + { + if (brokerListOptions.equals("")) + { + return "?sasl_mechs='ANONYMOUS'"; + } + else + { + if (brokerListOptions.contains("sasl_mechs")) + { + return brokerListOptions; + } + else + { + return brokerListOptions + "&sasl_mechs='ANONYMOUS'"; + } + } + } + else + { + return brokerListOptions; + } + } + + /** + * Create a ConnectionURL from the input "generic" URL. + * + * @param url the input URL. + * @param username the username. + * @param password the password. + * @param urlOptions the pre-parsed set of connection level options. + * @param brokerListOptions the pre-parsed set of specific brokerList options. + * @return a String containing the Java Connection URL. + */ + private static String parseURL(String url, String username, String password, + String urlOptions, String brokerListOptions) + { + if (url.startsWith("amqp://")) + { // Somewhat experimental. This new format is only a "proposed" format + return parseExtendedAMQPURL(url, username, password); + } + + String vhost = ""; // Specifying an empty vhost uses default Virtual Host. + String brokerList = ""; + if (url.startsWith("amqp:")) + { // AMQP 0.10 URL format + url = url.substring(5); // Chop off "amqp:" + String[] addrSplit = url.split(","); // prot_addrs are comma separated + boolean firstBroker = true; + brokerListOptions = adjustBrokerListOptions(username, brokerListOptions); + StringBuilder buf = new StringBuilder(); + for (String broker : addrSplit) + { // Iterate through the address list creating brokerList style URLs + broker = broker.trim(); + if (broker.startsWith("tcp:")) + { // Only tcp is supported in an AMQP 0.10 prot_addr so we *should* only have to account for + // a "tcp:" prefix when normalising broker URLs + broker = broker.substring(4); // Chop off "tcp:" + } + + if (firstBroker) + { + buf.append("tcp://" + broker + brokerListOptions); + } + else + { // https://cwiki.apache.org/qpid/connection-url-format.html says "A minimum of one broker url is + // required additional URLs are semi-colon(';') delimited." + buf.append(";tcp://" + broker + brokerListOptions); + } + firstBroker = false; + } + brokerList = "'" + buf.toString() + "'"; + } + else if (url.contains("@")) + { // BrokerURL format as used in the Python tools. + String[] split = url.split("@"); + url = split[1]; + + split = split[0].split("[/:]"); // Accept both <username>/<password> and <username>:<password> + username = split[0]; + + if (split.length == 2) + { + password = split[1]; + } + + brokerListOptions = adjustBrokerListOptions(username, brokerListOptions); + brokerList = "'tcp://" + url + brokerListOptions + "'"; + } + else + { // Basic host:port format + brokerListOptions = adjustBrokerListOptions(username, brokerListOptions); + brokerList = "'tcp://" + url + brokerListOptions + "'"; + } + + String connectionURL = "amqp://" + username + ":" + password + "@QpidJMS/" + vhost + "?brokerlist=" + + brokerList + urlOptions; + return connectionURL; + } + + + /** + * Creates a Java Connection URL from one of the other supported URL formats. + * + * @param url an AMQP 0.10 URL, an extended AMQP 0-10 URL, a Broker URL or a Connection URL (the latter is simply + * returned untouched). + * @return a String containing the Java Connection URL. + */ + public static String createConnectionURL(String url) + { + return createConnectionURL(url, null); + } + + /** + * Creates a Java Connection URL from one of the other supported URL formats plus options. + * + * @param url an AMQP 0.10 URL, an extended AMQP 0-10 URL, a Broker URL or a Connection URL (the latter is simply + * returned untouched). + * @param opts a String containing the options encoded using the same form as the C++ qpid::messaging + * Connection class. + * @return a String containing the Java Connection URL. + */ + public static String createConnectionURL(String url, String opts) + { + // This method is actually mostly about parsing the options, when the options are extracted it delegates + // to parseURL() to do the actual URL parsing. + + // If a Java Connection URL has been passed in we simply return it. + if (url.startsWith("amqp://") && url.contains("brokerlist")) + { + return url; + } + + // Initialise options to default values + String username = ""; + String password = ""; + String urlOptions = ""; + String brokerListOptions = ""; + + // Get options from option String + if (opts != null && opts.startsWith("{") && opts.endsWith("}")) + { + // Connection URL Options + String maxprefetch = ""; + String sync_publish = ""; + String sync_ack = ""; + String use_legacy_map_msg_format = ""; + String failover = ""; + + // Broker List Options + String heartbeat = ""; + String retries = ""; + String connectdelay = ""; + String connecttimeout = ""; + + String tcp_nodelay = ""; + + String sasl_mechs = ""; + String sasl_encryption = ""; + String sasl_protocol = ""; + String sasl_server = ""; + + String ssl = ""; + String ssl_verify_hostname = ""; + String ssl_cert_alias = ""; + + String trust_store = ""; + String trust_store_password = ""; + String key_store = ""; + String key_store_password = ""; + + Map options = new AddressParser(opts).map(); + + if (options.containsKey("maxprefetch")) + { + maxprefetch = "&maxprefetch='" + options.get("maxprefetch").toString() + "'"; + } + + if (options.containsKey("sync_publish")) + { + sync_publish = "&sync_publish='" + options.get("sync_publish").toString() + "'"; + } + + if (options.containsKey("sync_ack")) + { + sync_ack = "&sync_ack='" + options.get("sync_ack").toString() + "'"; + } + + if (options.containsKey("use_legacy_map_msg_format")) + { + use_legacy_map_msg_format = "&use_legacy_map_msg_format='" + + options.get("use_legacy_map_msg_format").toString() + "'"; + } + + if (options.containsKey("failover")) + { + if (options.containsKey("cyclecount")) + { + failover = "&failover='" + options.get("failover").toString() + "?cyclecount='" + + options.get("cyclecount").toString() + "''"; + } + else + { + failover = "&failover='" + options.get("failover").toString() + "'"; + } + } + + if (options.containsKey("username")) + { + username = options.get("username").toString(); + } + + if (options.containsKey("password")) + { + password = options.get("password").toString(); + } + + if (options.containsKey("reconnect")) + { + String value = options.get("reconnect").toString(); + if (value.equalsIgnoreCase("true")) + { + retries = "&retries='" + Integer.MAX_VALUE + "'"; + connectdelay = "&connectdelay='5000'"; + } + } + + if (options.containsKey("reconnect_limit")) + { + retries = "&retries='" + options.get("reconnect_limit").toString() + "'"; + } + + if (options.containsKey("reconnect_interval")) + { + connectdelay = "&connectdelay='" + options.get("reconnect_interval").toString() + "000'"; + } + + if (options.containsKey("reconnect_interval_min")) + { + connectdelay = "&connectdelay='" + options.get("reconnect_interval_min").toString() + "000'"; + } + + if (options.containsKey("reconnect_interval_max")) + { + connectdelay = "&connectdelay='" + options.get("reconnect_interval_max").toString() + "000'"; + } + + if (options.containsKey("reconnect_timeout")) + { + connecttimeout = "&connecttimeout='" + options.get("reconnect_timeout").toString() + "000'"; + } + + if (options.containsKey("heartbeat")) + { + heartbeat = "&heartbeat='" + options.get("heartbeat").toString() + "'"; + } + + if (options.containsKey("tcp-nodelay")) + { + tcp_nodelay = "&tcp_nodelay='" + options.get("tcp-nodelay").toString() + "'"; + } + + if (options.containsKey("sasl_mechanisms")) + { + sasl_mechs = "&sasl_mechs='" + options.get("sasl_mechanisms").toString() + "'"; + } + + if (options.containsKey("sasl_mechs")) + { + sasl_mechs = "&sasl_mechs='" + options.get("sasl_mechs").toString() + "'"; + } + + if (options.containsKey("sasl_encryption")) + { + sasl_encryption = "&sasl_encryption='" + options.get("sasl_encryption").toString() + "'"; + } + + if (options.containsKey("sasl_protocol")) + { + sasl_protocol = "&sasl_protocol='" + options.get("sasl_protocol").toString() + "'"; + } + + if (options.containsKey("sasl_server")) + { + sasl_server = "&sasl_server='" + options.get("sasl_server").toString() + "'"; + } + + if (options.containsKey("trust_store")) + { + trust_store = "&trust_store='" + options.get("trust_store").toString() + "'"; + } + + if (options.containsKey("trust_store_password")) + { + trust_store_password = "&trust_store_password='" + options.get("trust_store_password").toString() + "'"; + } + + if (options.containsKey("key_store")) + { + key_store = "&key_store='" + options.get("key_store").toString() + "'"; + } + + if (options.containsKey("key_store_password")) + { + key_store_password = "&key_store_password='" + options.get("key_store_password").toString() + "'"; + } + + if (options.containsKey("protocol")) + { + String value = options.get("protocol").toString(); + if (value.equalsIgnoreCase("ssl")) + { + ssl = "&ssl='true'"; + if (options.containsKey("ssl_verify_hostname")) + { + ssl_verify_hostname = "&ssl_verify_hostname='" + options.get("ssl_verify_hostname").toString() + "'"; + } + + if (options.containsKey("ssl_cert_alias")) + { + ssl_cert_alias = "&ssl_cert_alias='" + options.get("ssl_cert_alias").toString() + "'"; + } + } + } + + urlOptions = maxprefetch + sync_publish + sync_ack + use_legacy_map_msg_format + failover; + + brokerListOptions = heartbeat + retries + connectdelay + connecttimeout + tcp_nodelay + + sasl_mechs + sasl_encryption + sasl_protocol + sasl_server + + ssl + ssl_verify_hostname + ssl_cert_alias + + trust_store + trust_store_password + key_store + key_store_password; + + if (brokerListOptions.startsWith("&")) + { + brokerListOptions = "?" + brokerListOptions.substring(1); + } + } + + return parseURL(url, username, password, urlOptions, brokerListOptions); + } + + /** + * Creates a JMS Connection from one of the supported URL formats. + * + * @param url an AMQP 0.10 URL, an extended AMQP 0-10 URL, a Broker URL or a Connection URL. + * @return a javax.jms.Connection. + */ + public static Connection createConnection(String url) + { + return createConnection(url, null); + } + + /** + * Creates a JMS Connection from one of the supported URL formats plus options. + * + * @param url an AMQP 0.10 URL, an extended AMQP 0-10 URL, a Broker URL or a Connection URL. + * @param opts a String containing the options encoded using the same form as the C++ qpid::messaging + * Connection class. + * @return a javax.jms.Connection. + */ + public static Connection createConnection(String url, String opts) + { + String connectionUrl = createConnectionURL(url, opts); + _log.info("ConnectionHelper.createConnection() {}", connectionUrl); + + // Initialise JNDI names etc into properties + Properties props = new Properties(); + props.setProperty("java.naming.factory.initial", "org.apache.qpid.jndi.PropertiesFileInitialContextFactory"); + props.setProperty("connectionfactory.ConnectionFactory", connectionUrl); + + Connection connection = null; + try + { + Context jndi = new InitialContext(props); + ConnectionFactory connectionFactory = (ConnectionFactory)jndi.lookup("ConnectionFactory"); + connection = connectionFactory.createConnection(); + } + catch (NamingException ne) + { + _log.info("NamingException {} caught in createConnection()", ne.getMessage()); + } + catch (JMSException jmse) + { + _log.info("JMSException {} caught in createConnection()", jmse.getMessage()); + } + + return connection; + } +} + diff --git a/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/GetOpt.java b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/GetOpt.java new file mode 100644 index 0000000000..d38fa03b5a --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2/src/main/java/org/apache/qpid/qmf2/util/GetOpt.java @@ -0,0 +1,209 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.qmf2.util; + +// Misc Imports +import java.util.List; +import java.util.ArrayList; + +/** + * Basic Java port of the python getopt function. + * + * Takes a standard args String array plus short form and long form options lists. + * Searches the args for options and any option arguments and stores these in optList + * any remaining args are stored in encArgs. + * + * <p> + * Example usage (paraphrased from QpidConfig): + * <pre>{@code + * String[] longOpts = {"help", "durable", "bindings", "broker-addr=", "file-count=", + * "file-size=", "max-queue-size=", "max-queue-count=", "limit-policy=", + * "order=", "sequence", "ive", "force", "force-if-not-empty", + * "force-if-used", "alternate-exchange=", "passive", "timeout=", "file=", "flow-stop-size=", + * "flow-resume-size=", "flow-stop-count=", "flow-resume-count=", "argument="}; + * + * try + * { + * GetOpt getopt = new GetOpt(args, "ha:bf:", longOpts); + * List<String[]> optList = getopt.getOptList(); + * String[] cargs = {}; + * cargs = getopt.getEncArgs().toArray(cargs); + * + * for (String[] opt : optList) + * { + * //System.out.println(opt[0] + ":" + opt[1]); + * if (opt[0].equals("-a") || opt[0].equals("--broker-addr")) + * { + * _host = opt[1]; + * } + * // Just a sample - more parsing would follow.... + * } + * + * int nargs = cargs.length; + * if (nargs == 0) + * { + * overview(); + * } + * else + * { + * String cmd = cargs[0]; + * String modifier = ""; + * + * if (nargs > 1) + * { + * modifier = cargs[1]; + * } + * + * if (cmd.equals("exchanges")) + * { + * if (_recursive) + * { + * exchangeListRecurse(modifier); + * } + * else + * { + * exchangeList(modifier); + * } + * } + * // Just a sample - more parsing would follow.... + * } + * } + * catch (IllegalArgumentException e) + * { + * System.err.println(e.toString()); + * usage(); + * } + * } + * </pre> + * + * @author Fraser Adams + */ +public final class GetOpt +{ + private List<String[]> _optList = new ArrayList<String[]>(); + private List<String> _encArgs = new ArrayList<String>(); + + /** + * Returns the options and option arguments as a list containing a String array. The first element of the array + * contains the option and the second contains any arguments if present. + * + * @return the options and option arguments + */ + public List<String[]> getOptList() + { + return _optList; + } + + /** + * Returns any remaining arguments. This is any argument from a command line that doesn't begin "--" or "-". + * + * @return any remaining arguments not made available by getOptList(). + */ + public List<String> getEncArgs() + { + return _encArgs; + } + + /** + * Takes a standard args String array plus short form and long form options lists. + * Searches the args for options and any option arguments and stores these in optList + * any remaining args are stored in encArgs. + * + * @param args standard arg String array + * @param opts short form option list of the form "ab:cd:" where b and d are options with arguments. + * @param longOpts long form option list as an array of strings. "option=" signifies an options with arguments. + */ + public GetOpt(String[] args, String opts, String[] longOpts) throws IllegalArgumentException + { + int argslength = args.length; + for (int i = 0; i < argslength; i++) + { + String arg = args[i]; + if (arg.startsWith("--")) + { + String extractedOption = arg.substring(2); + int nargs = _optList.size(); + for (String j : longOpts) + { + String k = j.substring(0, j.length() - 1); + if (j.equals(extractedOption) || k.equals(extractedOption)) + { // Handle where option and value are space separated + String arg0 = arg; + String arg1 = ""; + if (i < argslength - 1 && k.equals(extractedOption)) + { + i++; + arg1 = args[i]; + } + String[] option = {arg0, arg1}; + _optList.add(option); + } + else + { // Handle where option and value are separated by '=' + String[] split = arg.split("=", 2); // Split on the first occurrence of '=' + String arg0 = split[0]; + String arg1 = ""; + if (split.length == 2) + { + j = j.substring(0, j.length() - 1); + arg1 = split[1]; + } + + if (arg0.substring(2).equals(j)) + { + String[] option = {arg0, arg1}; + _optList.add(option); + } + } + } + + if (nargs == _optList.size()) + { + throw new IllegalArgumentException("Unknown Option " + arg); + } + } + else if (arg.startsWith("-")) + { + String extractedOption = arg.substring(1); + int index = opts.indexOf(extractedOption); + if (index++ != -1) + { + String arg1 = ""; + if (i < argslength - 1 && index < opts.length() && opts.charAt(index) == ':') + { + i++; + arg1 = args[i]; + } + String[] option = {arg, arg1}; + _optList.add(option); + } + else + { + throw new IllegalArgumentException("Unknown Option " + arg); + } + } + else + { + _encArgs.add(arg); + } + } + } +} |