001: /*
002: * The contents of this file are subject to the Sapient Public License
003: * Version 1.0 (the "License"); you may not use this file except in compliance
004: * with the License. You may obtain a copy of the License at
005: * http://carbon.sf.net/License.html.
006: *
007: * Software distributed under the License is distributed on an "AS IS" basis,
008: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
009: * the specific language governing rights and limitations under the License.
010: *
011: * The Original Code is The Carbon Component Framework.
012: *
013: * The Initial Developer of the Original Code is Sapient Corporation
014: *
015: * Copyright (C) 2003 Sapient Corporation. All Rights Reserved.
016: */
017:
018: package org.sape.carbon.core.config.node;
019:
020: import java.util.Collection;
021: import java.util.Collections;
022: import java.util.HashMap;
023: import java.util.HashSet;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030:
031: import org.sape.carbon.core.config.InvalidConfigurationException;
032: import org.sape.carbon.core.config.node.event.NodeEventListener;
033: import org.sape.carbon.core.exception.ExceptionUtility;
034: import org.sape.carbon.core.exception.InvalidParameterException;
035:
036: /**
037: * Abstract implementation of the <code>Node</code> interface. This
038: * implementation is designed to be extended with the logic specific to the
039: * backing data store placed in the <code>destroyBackingData</code> method.
040: *
041: * Copyright 2002 Sapient
042: * @since carbon 1.0
043: * @author Douglas Voet, February 2002
044: * @version $Revision: 1.28 $($Author: dvoet $ / $Date: 2003/10/17 14:40:55 $)
045: */
046: public abstract class AbstractNode implements Node {
047:
048: /**
049: * Provides a handle to Apache-commons logger
050: */
051: private Log log = LogFactory.getLog(this .getClass());
052:
053: /** this Node's name */
054: private final String name;
055:
056: /** link to the Node's parent */
057: private final Node parent;
058:
059: /** this node's removed status */
060: private boolean removed = false;
061:
062: /**
063: * This lock is used to ensure that nodes are not added or
064: * loaded concurrently. Any time a node is put into childNodes, this lock
065: * guarantees that the node is constructed once and only once.
066: * addOrLoadChildLock is usually used in conjunction with a lock on this.
067: * A lock on addOrLoadChildLock keeps other threads from loading or adding
068: * new nodes while a lock on this ensures that access to childNodes is
069: * thread-safe.
070: */
071: private final Object addOrLoadChildLock;
072:
073: /**
074: * This object is syncrhonized upon whenever this node's state is read
075: * or altered. The state of this node includes the removed flag and
076: * the childNodes map, but not children themselves. State does not
077: * include this node's name or parent as these references are final.
078: * State also does not include the nodeListeners set as it synchronizes
079: * itself.
080: */
081: private final Object readOrAlterNodeLock;
082:
083: /**
084: * Map of child Nodes keyed by the Node's name (String). Final because
085: * the reference to the map should never change for this node.
086: *
087: * @link aggregation
088: * @associates <{Node}>
089: */
090: protected final Map childNodes = new HashMap();
091:
092: /**
093: * Contains the list of node listeners for this node.
094: */
095: protected final Set nodeListeners = Collections
096: .synchronizedSet(new HashSet());
097:
098: /**
099: * Constructor
100: *
101: * @param parent the node's parent
102: * @param name the node's name
103: *
104: * @throws InvalidParameterException if name is null
105: */
106: public AbstractNode(Node parent, String name) {
107: this (parent, name, new Object(), new Object());
108: }
109:
110: /**
111: * Constructor
112: *
113: * @param parent the node's parent
114: * @param name the node's name
115: * @param readOrAlterNodeLock the lock object that will be synchronized
116: * upon for all operations that access or modify this node's state
117: *
118: * @throws InvalidParameterException if name is null or readOrAlterNodeLock
119: * is null
120: */
121: public AbstractNode(Node parent, String name,
122: Object readOrAlterNodeLock, Object addOrLoadChildLock) {
123:
124: // parameter check
125: if (name == null) {
126: throw new InvalidParameterException(this .getClass(),
127: "name parameter cannot be null");
128: }
129:
130: if (readOrAlterNodeLock == null) {
131: throw new InvalidParameterException(this .getClass(),
132: "readOrAlterNodeLock parameter cannot be null");
133: }
134:
135: if (addOrLoadChildLock == null) {
136: throw new InvalidParameterException(this .getClass(),
137: "addOrLoadChildLock parameter cannot be null");
138: }
139:
140: if (addOrLoadChildLock == readOrAlterNodeLock) {
141: throw new InvalidParameterException(this .getClass(),
142: "readOrAlterNodeLock and addOrLoadChildLock cannot be the same object");
143: }
144:
145: this .parent = parent;
146: this .name = name;
147: this .readOrAlterNodeLock = readOrAlterNodeLock;
148: this .addOrLoadChildLock = addOrLoadChildLock;
149: }
150:
151: /**
152: * @see Node#getName()
153: */
154: public String getName() {
155: return this .name;
156: }
157:
158: /**
159: * @see Node#getParent()
160: */
161: public Node getParent() {
162: return this .parent;
163: }
164:
165: /**
166: * This implementation is recursive.
167: * If the parent is not null, it returns the parent's absolute name
168: * followed by Node.DELIMITER and this node's name
169: *
170: * @see Node#getAbsoluteName()
171: */
172: public String getAbsoluteName() {
173:
174: if (getParent() == null) {
175: return this .name;
176: } else {
177: return getParent().getAbsoluteName() + Node.DELIMITER
178: + this .name;
179: }
180: }
181:
182: /**
183: * @see Node#getAllowsChildren()
184: */
185: public boolean getAllowsChildren() {
186: return true;
187: }
188:
189: /**
190: * @see Node#remove()
191: *
192: * synchronized to ensure no one is fetching or adding children while
193: * this method is removing them
194: */
195: public int remove() throws NodeRemovalException {
196: int removedNodes = 0;
197:
198: if (!isRemoved()) {
199: // remove all the children
200: // note that this is not synchronized because that would cause
201: // a cascade of locks down the config hierachy and open a deadlock
202: // vulnerability
203: Node[] children = fetchChildren();
204:
205: for (int i = 0; i < children.length; i++) {
206: removedNodes += children[i].remove();
207: }
208:
209: synchronized (getReadOrAlterNodeLock()) {
210: if (backingDataExists()) {
211: // remove this node
212: destroyBackingData();
213: removedNodes++;
214: }
215: this .childNodes.clear();
216: setRemoved();
217: }
218: }
219:
220: return removedNodes;
221: }
222:
223: /**
224: * Returns the name of the node.
225: *
226: * @return the name of the node
227: */
228: public String toString() {
229: return getName();
230: }
231:
232: /**
233: * @see Node#isRemoved()
234: */
235: public boolean isRemoved() {
236: synchronized (getReadOrAlterNodeLock()) {
237: return this .removed;
238: }
239: }
240:
241: /**
242: * This implementation refreshes all children and toggles the isRemoved
243: * flag depending on the return value of the backingDataExists method.
244: *
245: * @see Node#refresh()
246: */
247: public void refresh() {
248: if (!isRemoved()) {
249:
250: // localChildNodesMap is a local copy of this.childNodes that is
251: // used so that children can be refreshed in an unsynchronized
252: // context
253: Map localChildNodesMap;
254:
255: synchronized (getReadOrAlterNodeLock()) {
256: localChildNodesMap = new HashMap(this .childNodes);
257:
258: if (!backingDataExists()) {
259: // backing data is not there anymore!
260: setRemoved();
261: }
262:
263: }
264: removeRemovedChildren();
265:
266: // tell all the children to refresh themselves
267: // note that if this node has been removed, refreshing all
268: // the children should cause them to be marked as removed
269: // as well
270: Iterator childIterator = localChildNodesMap.values()
271: .iterator();
272: while (childIterator.hasNext()) {
273: ((Node) childIterator.next()).refresh();
274: }
275: }
276: }
277:
278: /**
279: * synchronized to ensure no one is adding or removing children
280: * while childName is being fetched
281: *
282: * @see Node#fetchChild(String)
283: */
284: public Node fetchChild(String childName)
285: throws NodeNotFoundException {
286:
287: if (isRemoved()) {
288: throw new NodeRemovedException(this .getClass(), this );
289: }
290:
291: if (childName == null) {
292: throw new InvalidParameterException(this .getClass(),
293: "childName cannot be null");
294: }
295:
296: Node child;
297: synchronized (getReadOrAlterNodeLock()) {
298: child = (Node) this .childNodes.get(childName);
299: }
300:
301: // note that the call to child.isRemoved() requires a lock on child
302: // which means that we cannot call it while synchronized on this without
303: // openning a deadlock vulnerability
304: while (child != null && child.isRemoved()) {
305: synchronized (getReadOrAlterNodeLock()) {
306: Node doubleCheckChild = (Node) this .childNodes
307: .get(childName);
308: if (child == doubleCheckChild) {
309: // doubleCheckChild is the same child that we checked was
310: // removed, this means that no one else loaded it behind
311: // our back so we can go ahead and remove it and continue
312: // as if the child did not exist in the first place
313: this .childNodes.remove(childName);
314: child = null;
315: } else {
316: // the doubleCheckChild is different than child so someone
317: // must have loaded it behind our back, so, continue with
318: // child = doubleCheckChild and recheck
319: child = doubleCheckChild;
320: }
321: }
322:
323: }
324:
325: if (child == null) {
326: // we must synchronize on an object other than this here because
327: // we must ensure that the child is loaded only once, but we
328: // cannot maintain a lock on this while loading the child because
329: // loading a child might require traversing the config hierachy
330: // which requires other locks and opens a deadlock vulnerability
331: synchronized (getAddOrLoadChildLock()) {
332: synchronized (getReadOrAlterNodeLock()) {
333: // need to check if childName exists in childNodes one more
334: // time because another thread could loaded childName
335: // before we synchronized on loadChildLock
336: child = (Node) this .childNodes.get(childName);
337: }
338:
339: if (child == null) {
340: // OK, the child still has not been loaded, we are safe to
341: // load it
342: child = loadChild(childName);
343:
344: // add it to childNodes
345: synchronized (getReadOrAlterNodeLock()) {
346: this .childNodes.put(childName, child);
347: }
348: }
349: }
350: }
351:
352: return child;
353: }
354:
355: /**
356: * @see Node#fetchChildren()
357: */
358: public Node[] fetchChildren() {
359: if (isRemoved()) {
360: throw new NodeRemovedException(this .getClass(), this );
361: }
362:
363: removeRemovedChildren();
364: Collection cachedChildNames;
365: synchronized (getReadOrAlterNodeLock()) {
366: cachedChildNames = this .childNodes.keySet();
367: }
368:
369: // fetch non-cached children
370: Set childNamesToLoad = getAllChildNames();
371: childNamesToLoad.removeAll(cachedChildNames);
372: Iterator childNameIterator = childNamesToLoad.iterator();
373: while (childNameIterator.hasNext()) {
374: try {
375: fetchChild((String) childNameIterator.next());
376: } catch (NodeNotFoundException nnfe) {
377: // This would happen only if the impl of getAllChildNames()
378: // returns invalid data or if the node is invalid
379: if (log.isInfoEnabled()) {
380: log.info("Caught NodeNotFoundException: "
381: + ExceptionUtility
382: .printStackTracesToString(nnfe));
383: }
384: // continue to load other children
385: } catch (InvalidConfigurationException ice) {
386: // This would happen only if the node is invalid
387: if (log.isInfoEnabled()) {
388: log.info("Caught InvalidConfigurationException: "
389: + ExceptionUtility
390: .printStackTracesToString(ice));
391: }
392: // continue to load other children
393: }
394: }
395:
396: // create return array, but be sure not to return any removed nodes
397: Collection children = new HashSet(this .childNodes.values());
398: Iterator childrenIterator = children.iterator();
399:
400: while (childrenIterator.hasNext()) {
401: Node child = (Node) childrenIterator.next();
402: if (child.isRemoved()) {
403: childrenIterator.remove();
404: }
405: }
406:
407: return (Node[]) children.toArray(new Node[children.size()]);
408: }
409:
410: /**
411: * @see Folder#containsChild(String)
412: *
413: * synchronized to ensure no one is adding or removing children
414: * while childName is being checking to see if childName exists
415: */
416: public boolean containsChild(String childName) {
417: if (isRemoved()) {
418: throw new NodeRemovedException(this .getClass(), this );
419: }
420:
421: removeRemovedChildren();
422: boolean containsChild;
423: synchronized (getReadOrAlterNodeLock()) {
424: containsChild = this .childNodes.containsKey(childName);
425: }
426:
427: if (!containsChild) {
428: containsChild = getAllChildNames().contains(childName);
429: }
430:
431: return containsChild;
432: }
433:
434: /**
435: * @see org.sape.carbon.core.config.node.Node#addNodeListener(NodeEventListener)
436: */
437: public void addNodeListener(NodeEventListener listener) {
438: this .nodeListeners.add(listener);
439: }
440:
441: /**
442: * This method is called by <code>remove</code> to destroy the data
443: * backing this node in the data source. Implementations of this method
444: * should remove all traces of the <code>Node</code>'s existence
445: *
446: * @throws NodeRemovalException indicates an error removing the node
447: */
448: protected abstract void destroyBackingData()
449: throws NodeRemovalException;
450:
451: /**
452: * Called by fetchChildren to get the names of all the children from
453: * the backing data store. If a name appears in the returned Set,
454: * calling fetchChild with the name can not result in a
455: * NodeNotFoundException being thrown.
456: *
457: * @return Set of <code>String</code>s, the names of all the children
458: */
459: protected abstract Set getAllChildNames();
460:
461: /**
462: * Loads the child specified by nodeName from the backing data store.
463: * Called by fetchChild when the child has not yet been cached.
464: *
465: * @param nodeName the name of the node to load
466: * @return Node the loaded node
467: *
468: * @throws NodeNotFoundException if the backing data for the specifed
469: * node could not be found in the data store.
470: */
471: protected abstract Node loadChild(String nodeName)
472: throws NodeNotFoundException;
473:
474: /**
475: * Method called from the refresh method to see if the backing data
476: * still exists. If it exists, the isRemoved flag is set to false, if
477: * it does not, the isRemoved flag is set to true.
478: * @return boolean true if the backing date exists, false otherwise
479: */
480: protected abstract boolean backingDataExists();
481:
482: /**
483: * Removes all nodes from this.childNodes for which isRemoved returns true.
484: */
485: protected void removeRemovedChildren() {
486: Map childNodesLocalCopy;
487: synchronized (getReadOrAlterNodeLock()) {
488: childNodesLocalCopy = new HashMap(this .childNodes);
489: }
490:
491: Iterator childIterator = childNodesLocalCopy.values()
492: .iterator();
493: while (childIterator.hasNext()) {
494: Node child = (Node) childIterator.next();
495: if (child.isRemoved()) {
496: childIterator.remove();
497: }
498: }
499:
500: synchronized (getReadOrAlterNodeLock()) {
501: this .childNodes.keySet().retainAll(
502: childNodesLocalCopy.keySet());
503: }
504: }
505:
506: /**
507: * Sets the node state to be removed.
508: * Note that this method is not synchronized but must be called from
509: * section of code synchronized on getReadOrAlterNodeLock() object.
510: */
511: protected void setRemoved() {
512: this .removed = true;
513: }
514:
515: /**
516: * Notifies all listenters that this node has been changed.
517: * Note that this should be not be called from a synchronized context.
518: *
519: * @since carbon 1.1
520: */
521: protected void issueNodeChangedEvent() {
522: // get the listeners as an array in order to get a local copy and
523: // not have to synchronize during the iteration and calls to the
524: // nodeChanged method
525: NodeEventListener[] listeners = (NodeEventListener[]) this .nodeListeners
526: .toArray(new NodeEventListener[0]);
527:
528: for (int i = 0; i < listeners.length; i++) {
529: listeners[i].nodeChanged(this );
530: }
531: }
532:
533: /**
534: * Notifies all listenters that this node has been removed.
535: * Note that this should be not be called from a synchronized context.
536: *
537: * @since carbon 1.1
538: */
539: protected void issueNodeRemovedEvent() {
540: // get the listeners as an array in order to get a local copy and
541: // not have to synchronize during the iteration and calls to the
542: // nodeRemoved method
543: NodeEventListener[] listeners = (NodeEventListener[]) this .nodeListeners
544: .toArray(new NodeEventListener[0]);
545:
546: for (int i = 0; i < listeners.length; i++) {
547: listeners[i].nodeRemoved(getAbsoluteName());
548: }
549: }
550:
551: /**
552: * Gets the lock object to synchronize upon when reading or altering the
553: * removed flag or the childNodes Map.
554: * <p>
555: * This method is final because the design of the abstract node
556: * implementations requires that the return value of this method never
557: * change once this object is constructed. The return value can be set
558: * in one of the constructors.
559: *
560: * @see AbstractNode#AbstractNode(Node, String, Object, Object)
561: *
562: * @return the lock object
563: *
564: * @since carbon 2.1
565: */
566: protected final Object getReadOrAlterNodeLock() {
567: return this .readOrAlterNodeLock;
568: }
569:
570: /**
571: * Gets the lock object to synchronized upon when creating new children
572: * or loading existing children. This lock ensures that child nodes are
573: * created only once.
574: * <p>
575: * This method is final because the design of the abstract node
576: * implementations requires that the return value of this method never
577: * change once this object is constructed. The return value can be set
578: * in one of the constructors.
579: *
580: * @see AbstractNode#AbstractNode(Node, String, Object, Object)
581: *
582: * @return the lock object
583: *
584: * @since carbon 2.1
585: */
586: protected final Object getAddOrLoadChildLock() {
587: return this.addOrLoadChildLock;
588: }
589: }
|