001: /*
002: * All content copyright (c) 2003-2006 Terracotta, Inc., except as may otherwise be noted in a separate copyright notice. All rights reserved.
003: */
004: package com.tc.config.schema.dynamic;
005:
006: import org.apache.xmlbeans.XmlException;
007: import org.apache.xmlbeans.XmlObject;
008:
009: import com.tc.config.schema.context.ConfigContext;
010: import com.tc.config.schema.listen.ConfigurationChangeListener;
011: import com.tc.util.Assert;
012:
013: import java.lang.reflect.InvocationTargetException;
014: import java.lang.reflect.Method;
015:
016: /**
017: * A {@link ConfigItem} that uses XPaths to find its data. Caches the current value for efficiency, and provides for
018: * specification of a default, which it will use if the value is <code>null</code>.
019: * </p>
020: * <p>
021: * Subclasses must take care of extracting the actual required value from the {@link XmlObject}.
022: * </p>
023: * <p>
024: * Normally, this class would be doing too much stuff — it handles defaults, caching, and data extraction, all at
025: * once. However, because of the restrictions of Java's type system, doing it any other way seems to lead to an
026: * explosion of classes. If you can figure out a way to factor it that splits up this class but doesn't lead to a class
027: * explosion, by all means, do so.
028: */
029: public abstract class XPathBasedConfigItem implements ConfigItem,
030: ConfigurationChangeListener {
031:
032: private final ConfigContext context;
033: private final String xpath;
034:
035: private Object defaultValue;
036: private boolean defaultInitialized;
037:
038: private final CompoundConfigItemListener listener;
039:
040: private boolean haveCurrentValue;
041: private Object currentValue;
042:
043: public XPathBasedConfigItem(ConfigContext context, String xpath) {
044: Assert.assertNotNull(context);
045: Assert.assertNotBlank(xpath);
046:
047: this .context = context;
048: this .xpath = xpath;
049:
050: this .defaultInitialized = false;
051:
052: this .listener = new CompoundConfigItemListener();
053:
054: this .haveCurrentValue = false;
055: this .currentValue = null;
056:
057: this .context.itemCreated(this );
058: }
059:
060: private synchronized void initializeDefaultIfNecessary() {
061: if (!this .defaultInitialized) {
062: try {
063: if (this .context.hasDefaultFor(xpath)) {
064: this .defaultValue = fetchDataFromXmlObject(this .context
065: .defaultFor(this .xpath));
066: } else {
067: this .defaultValue = null;
068: }
069: this .defaultInitialized = true;
070: } catch (XmlException xmle) {
071: throw Assert.failure("Couldn't use XPath '"
072: + this .xpath + "' to fetch a default value",
073: xmle);
074: }
075: }
076: }
077:
078: /**
079: * Generally, you <strong>SHOULD NOT</strong> use this constructor. Instead, you should let the schema specify the
080: * default, as we want that to be the canonical repository for default values. However, certain things — like
081: * attributes, arrays, and other complex structures — can't have defaults provided in a schema, so we must
082: * provide this method instead.
083: */
084: public XPathBasedConfigItem(ConfigContext context, String xpath,
085: Object defaultValue) {
086: Assert.assertNotNull(context);
087: Assert.assertNotBlank(xpath);
088:
089: this .context = context;
090: this .xpath = xpath;
091:
092: this .defaultValue = defaultValue;
093: this .defaultInitialized = true;
094:
095: this .listener = new CompoundConfigItemListener();
096:
097: this .haveCurrentValue = false;
098: this .currentValue = null;
099:
100: this .context.itemCreated(this );
101: }
102:
103: public synchronized Object getObject() {
104: if (!this .haveCurrentValue) {
105: synchronized (this .context.syncLockForBean()) {
106: this .currentValue = fetchDataFromTopLevelBean(this .context
107: .bean());
108: this .haveCurrentValue = true;
109: }
110: }
111:
112: return this .currentValue;
113: }
114:
115: private Object fetchDataFromTopLevelBean(XmlObject bean) {
116: Object out = null;
117:
118: initializeDefaultIfNecessary();
119:
120: if (bean != null) {
121: XmlObject[] targetList;
122:
123: // We synchronize on the bean for test code; test code might be changing it while we're selecting from it.
124: synchronized (bean) {
125: targetList = bean.selectPath(this .xpath);
126: }
127:
128: if (targetList == null
129: || targetList.length == 0
130: || (targetList.length == 1 && targetList[0] == null))
131: out = fetchDataFromXmlObject(null);
132: else if (targetList.length == 1)
133: out = fetchDataFromXmlObject(targetList[0]);
134: else
135: throw Assert
136: .failure("From "
137: + bean
138: + ", XPath '"
139: + this .xpath
140: + "' selected "
141: + targetList.length
142: + " nodes, not "
143: + "just 1. This should never happen; there is a bug in the software.");
144: }
145:
146: if (out == null)
147: out = this .defaultValue;
148:
149: return out;
150: }
151:
152: protected abstract Object fetchDataFromXmlObject(XmlObject xmlObject);
153:
154: protected final Object fetchDataFromXmlObjectByReflection(
155: XmlObject xmlObject, String methodName) {
156: if (xmlObject == null)
157: return null;
158:
159: Method method = getMethodWithNoParametersByName(xmlObject
160: .getClass(), methodName);
161:
162: if (method == null) {
163: // formatting
164: throw Assert.failure("There is no method named '"
165: + methodName + "' on object " + xmlObject
166: + " (of class " + xmlObject.getClass().getName()
167: + ") with no parameters.");
168: }
169:
170: try {
171: return method.invoke(xmlObject, new Object[0]);
172: } catch (IllegalArgumentException iae) {
173: throw Assert.failure("Unable to invoke method " + method
174: + ".", iae);
175: } catch (IllegalAccessException iae) {
176: throw Assert.failure("Unable to invoke method " + method
177: + ".", iae);
178: } catch (InvocationTargetException ite) {
179: throw Assert.failure("Unable to invoke method " + method
180: + ".", ite);
181: }
182: }
183:
184: protected final Method getMethodWithNoParametersByName(
185: Class theClass, String methodName) {
186: Method[] allMethods = theClass.getMethods();
187: for (int i = 0; i < allMethods.length; ++i) {
188: if (allMethods[i].getName().equals(methodName)
189: && allMethods[i].getParameterTypes().length == 0) {
190: return allMethods[i];
191: }
192: }
193:
194: return null;
195: }
196:
197: public synchronized void addListener(
198: ConfigItemListener changeListener) {
199: Assert.assertNotNull(changeListener);
200: this .listener.addListener(changeListener);
201: }
202:
203: public synchronized void removeListener(
204: ConfigItemListener changeListener) {
205: Assert.assertNotNull(changeListener);
206: this .listener.removeListener(changeListener);
207: }
208:
209: public synchronized void configurationChanged(XmlObject oldConfig,
210: XmlObject newConfig) {
211: Object oldValue, newValue;
212:
213: synchronized (this .context.syncLockForBean()) {
214: if (this .haveCurrentValue)
215: oldValue = this .currentValue;
216: else
217: oldValue = fetchDataFromTopLevelBean(oldConfig);
218:
219: newValue = fetchDataFromTopLevelBean(newConfig);
220:
221: this .currentValue = newValue;
222: this .haveCurrentValue = true;
223: }
224:
225: if (((oldValue == null) != (newValue == null))
226: || ((oldValue != null) && (!oldValue.equals(newValue)))) {
227: this .listener.valueChanged(oldValue, newValue);
228: }
229: }
230:
231: public String toString() {
232: return "configuration item at XPath '" + this .xpath + "'";
233: }
234:
235: /**
236: * For <strong>TESTS ONLY</strong>.
237: */
238: public ConfigContext context() {
239: return this .context;
240: }
241:
242: /**
243: * For <strong>TESTS ONLY</strong>.
244: */
245: public String xpath() {
246: return this .xpath;
247: }
248:
249: /**
250: * For <strong>TESTS ONLY</strong>.
251: */
252: public Object defaultValue() {
253: initializeDefaultIfNecessary();
254: return this.defaultValue;
255: }
256:
257: }
|