001: /*=============================================================================
002: * Copyright Texas Instruments, Inc., 2001. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.chimera;
020:
021: import ti.chimera.registry.*;
022: import ti.exceptions.ProgrammingErrorException;
023:
024: import java.util.*;
025:
026: /**
027: * The registry is a mechanism that allows different parts of the system
028: * share data. It is traditionally used to store registered plugins and
029: * services, but other parts of the system can use the registry to store
030: * or share data. The registry uses a publish-subscribe model, where in
031: * order to track the value of a node, the user subscribes to the node,
032: * and whenever the value of the node changes the most recent value is
033: * published to the subscriber's callback function. The registry will
034: * ensure that changes are published in the order they occur, and that
035: * all changes will be published, even if they've already been super-
036: * ceded by subsequent changes.
037: * <p>
038: * The registry is organized as a hierarchical tree, where "nodes" are
039: * either parent nodes (directories) or data nodes. Directory nodes
040: * are just data nodes whose data is a table mapping child names to
041: * nodes. A <code>path</code> is a <code>/</code> delimited string that
042: * specifies how to find a node from the root of the tree. Paths can
043: * contain <code>..</code> to refer to one level up in the tree, or
044: * <code>.</code> to refer to the current level in the tree.
045: * <p>
046: * The registry can be accessed via {@link ti.chimera.Main#getRegistry}
047: * or from the script environment variable <code>registry</code>.
048: *
049: * @author Rob Clark
050: * @version 0.1
051: * @see ti.chimera.Main#getRegistry
052: */
053: public class Registry extends RegistryCore {
054: /*=======================================================================*/
055: /**
056: * Class Constructor.
057: */
058: Registry(Main main) {
059: super (main);
060: }
061:
062: /*
063: * Convenience API:
064: */
065:
066: /*=======================================================================*/
067: /**
068: * Subscribe to receive notification of changes to the value of the node
069: * at the specified path. This subscription is relative to the specified
070: * <code>path</code>, and not the node that is currently located at that
071: * path. If there is currently a node at this path, then the subscriber
072: * will be immediately called with the current value, otherwise the
073: * subscriber will be called once the node is created. If the node at
074: * this path is removed, and then at some later time another node is
075: * {@link #link}ed in to this path, then the subscriber will subscribed
076: * to that node (and immediately called with it's value), and so on.
077: * <p>
078: * If <code>contract</code> is not <code>null</code>, then only subscribe
079: * to the node if it has a compatible contract.
080: *
081: * @param path the path to the node to subscribe to
082: * @param contract the optional contract, or <code>null</code>
083: * @param subscriber the subscriber
084: */
085: public void subscribeToValue(String path,
086: final NodeContract contract, final NodeSubscriber subscriber) {
087: if (subscriber == null)
088: throw new ProgrammingErrorException(
089: "illegal argument: subscriber is null");
090: else if (path == null)
091: throw new ProgrammingErrorException(
092: "illegal argument: path is null");
093:
094: NodeSeeker ns;
095:
096: subscribeToValueSubscriberMap.put(subscriber,
097: ns = new NodeSeeker(path) {
098:
099: void publishFromSoughtNode(Node node, Object value) {
100: if (((contract == null) ? NodeContract.NULL_CONTRACT
101: : contract).accepts(value))
102: subscriber.publish(node, value);
103: }
104:
105: });
106:
107: ns.start();
108: }
109:
110: private Map subscribeToValueSubscriberMap = new Hashtable();
111:
112: /**
113: * Unsubscribe from specified path. If not subscribed, do nothing.
114: *
115: * @param subscriber the subscriber
116: */
117: public void unsubscribeFromValue(NodeSubscriber subscriber) {
118: NodeSeeker ns = (NodeSeeker) (subscribeToValueSubscriberMap
119: .remove(subscriber));
120:
121: if (ns != null)
122: ns.dispose();
123: }
124:
125: /*=======================================================================*/
126: /**
127: * Subscribe to the creation of a node. The subscriber will be called
128: * whenever a node is {@link #link}ed in to the specified path. If the
129: * node already exists, the subscriber will be called (pretty much)
130: * immediately.
131: *
132: * @param path the path to the node to subscribe to
133: * @param subscriber the subscriber
134: */
135: public void subscribeToCreation(String path,
136: final NodeCreationSubscriber subscriber) {
137: if (subscriber == null)
138: throw new ProgrammingErrorException(
139: "illegal argument: subscriber is null");
140: else if (path == null)
141: throw new ProgrammingErrorException(
142: "illegal argument: path is null");
143:
144: final String basename = basename(path);
145: NodeSeeker ns;
146:
147: subscribeToCreationSubscriberMap.put(subscriber,
148: ns = new NodeSeeker(dirname(path)) {
149:
150: private DirectoryTable lastDt;
151:
152: void publishFromSoughtNode(Node node, Object value) {
153: DirectoryTable newDt = (DirectoryTable) value;
154:
155: for (Iterator added = newDt.notIn(lastDt); added
156: .hasNext();) {
157: if (added.next().equals(basename)) {
158: subscriber.nodeCreated(newDt
159: .get(basename));
160: break;
161: }
162: }
163:
164: lastDt = newDt;
165: }
166:
167: });
168:
169: ns.start();
170: }
171:
172: private Map subscribeToCreationSubscriberMap = new Hashtable();
173:
174: /**
175: * Unsubscribe to the creation of a node. If not subscribed, do nothing.
176: *
177: * @param subscriber the subscriber
178: */
179: public void unsubscribeFromCreation(
180: NodeCreationSubscriber subscriber) {
181: NodeSeeker ns = (NodeSeeker) (subscribeToCreationSubscriberMap
182: .remove(subscriber));
183:
184: if (ns != null)
185: ns.dispose();
186: }
187:
188: /*=======================================================================*/
189: /**
190: * Subscribe to the deletion of a node. The subscriber will be called
191: * whenever a node is {@link #unlink}ed from the specified path.
192: *
193: * @param path the path to the node to subscribe to
194: * @param subscriber the subscriber
195: */
196: public void subscribeToDeletion(String path,
197: final NodeDeletionSubscriber subscriber) {
198: if (subscriber == null)
199: throw new ProgrammingErrorException(
200: "illegal argument: subscriber is null");
201: else if (path == null)
202: throw new ProgrammingErrorException(
203: "illegal argument: path is null");
204:
205: final String basename = basename(path);
206: NodeSeeker ns;
207:
208: subscribeToDeletionSubscriberMap.put(subscriber,
209: ns = new NodeSeeker(dirname(path)) {
210:
211: private DirectoryTable lastDt;
212:
213: void publishFromSoughtNode(Node node, Object value) {
214: DirectoryTable newDt = (DirectoryTable) value;
215: DirectoryTable lastDt = this .lastDt;
216: this .lastDt = newDt;
217:
218: if (lastDt != null) {
219: for (Iterator removed = lastDt.notIn(newDt); removed
220: .hasNext();) {
221: if (removed.next().equals(basename)) {
222: subscriber.nodeDeleted(lastDt
223: .get(basename));
224: break;
225: }
226: }
227: }
228: }
229:
230: });
231:
232: ns.start();
233: }
234:
235: private Map subscribeToDeletionSubscriberMap = new Hashtable();
236:
237: /**
238: * Unsubscribe to the deletion of a node. If not subscribed, do nothing.
239: *
240: * @param subscriber the subscriber
241: */
242: public void unsubscribeFromDeletion(
243: NodeDeletionSubscriber subscriber) {
244: NodeSeeker ns = (NodeSeeker) (subscribeToDeletionSubscriberMap
245: .remove(subscriber));
246:
247: if (ns != null)
248: ns.dispose();
249: }
250:
251: /*
252: * Plugin/Service API:
253: */
254:
255: /*=======================================================================*/
256: /**
257: * Register a plugin. This is how a plugin is added to the system. Once
258: * a plugin is added, the plugin can be resolved by {@link #getPlugin} and
259: * a service provided by a plugin can be resolved by {@link #getService}.
260: * The registry will handle starting a plugin if needed.
261: *
262: * @param plugin the plugin to register
263: * @see #getPlugin
264: * @see #getService
265: */
266: public void register(Plugin plugin) {
267: checkName(plugin.getName());
268: main.debug(1, "registering plugin: \"" + plugin.getName()
269: + "\"");
270:
271: String path = "/Plugins/" + plugin.getName();
272:
273: // remove old node at same path, if there is one:
274: try {
275: unlink(path, true);
276: } catch (RegistryException e) {
277: }
278:
279: // create plugin directory:
280: mkdir(path);
281: mkdir(path + "/Services");
282:
283: // link in plugin instance:
284: try {
285: link(new Node(plugin, new TypeNodeContract(Plugin.class),
286: "The \"" + plugin.getName() + "\" instance"), path
287: + "/instance");
288: } catch (RegistryException e) {
289: throw new ProgrammingErrorException(e);
290: }
291:
292: // populate directory node with services:
293: for (Iterator itr = plugin.getServiceFactories(); itr.hasNext();) {
294: Plugin.ServiceFactory f = ((Plugin.ServiceFactory) (itr
295: .next()));
296: main.debug(1, "registering service: \"" + f.getName()
297: + "\" provided by \"" + plugin.getName() + "\"");
298:
299: Node node = new Node(f, new TypeNodeContract(
300: Plugin.ServiceFactory.class),
301: "The service-factory for the \"" + f.getName()
302: + "\" service");
303:
304: // remove old service node at same path, if there is one:
305: try {
306: if (exists("/Services/" + f.getName()))
307: unlink("/Services/" + f.getName());
308: } catch (RegistryException e) {
309: throw new ProgrammingErrorException(e);
310: }
311:
312: try {
313: link(node, path + "/Services/" + f.getName());
314: link(node, "/Services/" + f.getName());
315: } catch (RegistryException e) {
316: throw new ProgrammingErrorException(e);
317: }
318: }
319: }
320:
321: /*=======================================================================*/
322: /**
323: * Get service provided by a plugin by name. This will automatically
324: * start the plugin providing the service if it has not already been
325: * started.
326: *
327: * @param name the name of the plugin to find
328: * @see #getServiceNames
329: */
330: public Service getService(String name) {
331: checkName(name);
332: try {
333: return ((Plugin.ServiceFactory) (resolve("/Services/"
334: + name).getValue())).getService();
335: } catch (RegistryException e) {
336: return null;
337: }
338: }
339:
340: private static void checkName(String name) {
341: if (name.indexOf('/') != -1)
342: throw new ProgrammingErrorException(
343: "invalid service name: \"" + name + "\"");
344: }
345:
346: /*=========================================================================*/
347: /**
348: * The convenience API uses NodeSubscribers to "find" a particular node,
349: * even if it doesn't currently exist. In the case of the creation/
350: * deletion subscriber API, the node-seeker is looking for the parent
351: * directory of the node of interest to the user. In the case of the
352: * value subscriber, the node-seeker is seeking the actual node that the
353: * user is interested in. Whatever the case, all of these node-seekers
354: * share the same node-seeking algorithm, and only differ in what they
355: * do once they've found their node of interest.
356: */
357: abstract class NodeSeeker {
358: /**
359: * The path we are trying to subscribe to.
360: */
361: private String soughtPath;
362:
363: /**
364: * The root of the chain of subscribers to each parent directory leading
365: * up to the node at the sought-path
366: */
367: private NodeSeekerSubscriber root;
368:
369: /**
370: * Class Constructor.
371: *
372: * @param soughtPath the path to the node this seeker is seeking
373: */
374: public NodeSeeker(String soughtPath) {
375: this .soughtPath = normalize(soughtPath);
376: }
377:
378: /**
379: * We have to create the root *after* the parent class constructor is
380: * completed, to work around some "quirks" about how inner classes
381: * work (ie. by copying final vars in to instance of innser class)
382: */
383: void start() {
384: root = new NodeSeekerSubscriber(Registry.this .root, "/");
385: }
386:
387: /**
388: * Called when the user unsubscribes, and this NodeSeeker
389: * should clean up all it's subscribers.
390: */
391: void dispose() {
392: root.dispose();
393: }
394:
395: /**
396: * When a <code>publish</code> happens from the node at the path of
397: * interest (ie. the sought node), we call this method which should
398: * be implemented by the derived class to do whatever is necessary.
399: */
400: abstract void publishFromSoughtNode(Node node, Object value);
401:
402: /**
403: * An instance of this subscriber is created for each parent directory
404: * leading up to and including the node at the sought path. When any of
405: * these subscribers detect that their next in the chain is removed, it
406: * messages the next in chain by calling {@link #dispose}. Conversely
407: * when a node detects that a child has been added that is the next in
408: * the chain, it creates a new subscriber for that child.
409: */
410: class NodeSeekerSubscriber implements NodeSubscriber {
411: private Node node;
412: private String path;
413: private String childName; // the name of the child we are watching for
414:
415: private NodeSeekerSubscriber next; // node corresponding to childName
416: private DirectoryTable lastDt;
417:
418: NodeSeekerSubscriber(Node node, String path) {
419: this .node = node;
420: this .path = path;
421:
422: if (!path.equals(soughtPath)) {
423: int idx = soughtPath
424: .indexOf('/', path.length() + 1);
425: if (idx == -1)
426: idx = soughtPath.length();
427: childName = basename(soughtPath.substring(path
428: .length(), idx));
429: }
430:
431: node.subscribe(this );
432: }
433:
434: public void publish(Node node, Object value) {
435: if (childName == null) {
436: publishFromSoughtNode(node, value);
437: } else {
438: DirectoryTable newDt = (DirectoryTable) value;
439:
440: if (lastDt != null) {
441: for (Iterator removed = lastDt.notIn(newDt); removed
442: .hasNext();) {
443: if (removed.next().equals(childName)) {
444: next.dispose();
445: next = null;
446: }
447: }
448: }
449:
450: for (Iterator added = newDt.notIn(lastDt); added
451: .hasNext();) {
452: String name = (String) (added.next());
453:
454: if (name.equals(childName)) {
455: if (next != null)
456: throw new ProgrammingErrorException(
457: "shouldn't be possible!");
458:
459: next = new NodeSeekerSubscriber(newDt
460: .get(name), normalize(path + "/"
461: + childName));
462: }
463: }
464:
465: lastDt = newDt;
466: }
467: }
468:
469: void dispose() {
470: if (next != null)
471: next.dispose();
472: node.unsubscribe(this );
473: }
474: }
475: }
476: }
477:
478: /*
479: * Local Variables:
480: * tab-width: 2
481: * indent-tabs-mode: nil
482: * mode: java
483: * c-indentation-style: java
484: * c-basic-offset: 2
485: * eval: (c-set-offset 'substatement-open '0)
486: * eval: (c-set-offset 'case-label '+)
487: * eval: (c-set-offset 'inclass '+)
488: * eval: (c-set-offset 'inline-open '0)
489: * End:
490: */
|