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.format;
019:
020: import java.beans.BeanInfo;
021: import java.beans.IntrospectionException;
022: import java.beans.Introspector;
023: import java.beans.PropertyDescriptor;
024: import java.io.Serializable;
025: import java.lang.reflect.Array;
026: import java.lang.reflect.Field;
027: import java.lang.reflect.InvocationTargetException;
028: import java.lang.reflect.Method;
029: import java.util.Map;
030:
031: import org.jdom.Document;
032: import org.jdom.Element;
033:
034: import org.apache.commons.logging.Log;
035: import org.apache.commons.logging.LogFactory;
036:
037: import org.sape.carbon.core.config.Configuration;
038: import org.sape.carbon.core.config.InvalidConfigurationException;
039: import org.sape.carbon.core.exception.InvalidParameterException;
040: import org.sape.carbon.core.util.reflection.GenericProxy;
041: import org.sape.carbon.core.util.string.StringUtil;
042:
043: /**
044: * <p>This abstract class includes basic support for configuration objects
045: * in XML. This class implements the Dynamic Proxy
046: * <class>InvocationHandler</class> interface. By utilizing Dynamic Proxies this
047: * class can implement any subclass of
048: * {@link org.sape.carbon.core.config.Configuration} and automatically support
049: * reading and writing the data of that object to XML.
050: * </p>
051: *
052: * <P>This abstract class implements the basic functionality for managing a
053: * JDOM {@link org.jdom.Document}. It also provides abstract method declarations
054: * that allow a subclass to choose exactly the format for that XML and how it
055: * is treated by a Configuration object.
056: * </P>
057: *
058: * Copyright 2002 Sapient
059: * @since carbon 1.0
060: * @author Greg Hinkle, January 2002
061: * @version $Revision: 1.58 $($Author: dvoet $ / $Date: 2003/11/26 20:30:18 $)
062: */
063: public abstract class AbstractConfigurationProxy extends GenericProxy
064: implements Configuration, Serializable {
065:
066: /**
067: * Provides a handle to Apache-commons logger
068: */
069: private Log log = LogFactory.getLog(this .getClass());
070:
071: /** Prefix for config values that reference other configs. */
072: protected static final String REF_PREFIX = "ref://";
073:
074: /** Holds the length of reference prefix. */
075: protected static final int REF_PREFIX_LENGTH = REF_PREFIX.length();
076:
077: /**
078: * JDOM Document object that holds the xml version of this configuration
079: * object's data.
080: */
081: protected Document document;
082:
083: /**
084: * The root element of this configuration object. May represent the root
085: * node of the document or it may be a sub-element for included object
086: * types.
087: */
088: protected Element element;
089:
090: /**
091: * The Class of object that this configuration object is implementing.
092: */
093: protected Class documentType;
094:
095: /**
096: * The fully qualified configuration name for this configuration object
097: */
098: protected String name;
099:
100: /** The root XML tag for configuration documnts */
101: private static final String ROOT_TAG = "Configuration";
102:
103: /**
104: * True if this object may be altered, false if it should throw exceptions
105: * when attempts are made to alter it.
106: */
107: private boolean writable = true;
108:
109: /**
110: * Constructs an AbstractConfigurationProxy for the supplied document.
111: *
112: * @param document The JDOM document object representing the XML for this
113: * configuration object
114: * @param root The root element of this configuration object, not
115: * necessarily the root of the document
116: * @param documentType the Class of object represented by the DynamicProxy
117: * form of this object
118: */
119: protected AbstractConfigurationProxy(Document document,
120: Element root, Class documentType) {
121:
122: this .document = document;
123: this .element = root;
124: this .documentType = documentType;
125:
126: this .element.setAttribute("ConfigurationInterface",
127: this .documentType.getName());
128: }
129:
130: /**
131: * Constructs an AbstractConfigurationProxy for the supplied document.
132: *
133: * @param documentType the Class of object represented by the DynamicProxy
134: * form of this object
135: */
136: protected AbstractConfigurationProxy(Class documentType) {
137:
138: this .element = new Element(ROOT_TAG);
139:
140: this .documentType = documentType;
141:
142: this .element.setAttribute("ConfigurationInterface",
143: this .documentType.getName());
144:
145: this .document = new Document(this .element);
146: }
147:
148: /**
149: * Retrieves this configuration object's name
150: *
151: * @return the fully qualified path name for this configuration object in
152: * the configuration service; <code>null</code> if this configuration
153: * object has not yet been stored in the configuration service
154: */
155: public String getConfigurationName() {
156: return this .name;
157: }
158:
159: /**
160: * Sets the name of this configuration object
161: *
162: * @param name the fully qualified name of this configuration in the
163: * configuration service
164: */
165: public void setConfigurationName(String name) {
166: this .name = name;
167: }
168:
169: /**
170: * Retrieves the primary type of this configuration
171: * @return the type of this configuration object. This is the primary
172: * class type of the DynamicProxy that was built to support the storage
173: * and retrieval of configurations for that interface
174: */
175: public Class getDocumentType() {
176: return this .documentType;
177: }
178:
179: /**
180: * <P>This method provides the basic implementation of invocations
181: * from the standpoint of DynamicProxies. The base proxy class,
182: * <code>GenericProxy</code> provides a default implemenetation
183: * of an InvocationHandler and basic handling for the standad
184: * <code>Object</code> methods. Other methods are passed to this
185: * method for invocation.</P>
186: *
187: * <P>This <code>AbstractConfigurationProxy</code> will handle basic
188: * bean based calls into an object graph and utilize method name
189: * and JavaBean based naming standards to determine the proper data
190: * item to query.</P>
191: *
192: * <P>Sublcasses of <code>AbstractConfigurationProxy</code> will be able
193: * to provide specific implementations of these methods based on what
194: * the underlying datastore type is.</P>
195: *
196: * @param proxy the proxy object on which the method was called
197: * @param m the method which was called to be executed
198: * @param args the array of arguments to the specified method call
199: * @throws Throwable when an invoke fails with any exception -
200: * this is cast by standard dynamic proxy functionality into an
201: * appropriate exception for the type of method that was actually called.
202: * @return the object value returned from the real delegated method call
203: */
204: protected Object handleInvoke(Object proxy, Method m, Object[] args)
205: throws Throwable {
206: String methodName = m.getName();
207: Object returnObject = null;
208:
209: if (m.getDeclaringClass() == Configuration.class) {
210: // all methods defined by the Configuration interface are handled
211: // by this class
212: try {
213: returnObject = m.invoke(this , args);
214: } catch (InvocationTargetException ite) {
215: // unwrap exception and throw it
216: throw ite.getTargetException();
217: }
218:
219: } else if (methodName.startsWith("get")) {
220: String attrName = m.getName().substring(3);
221: Class returnType = m.getReturnType();
222:
223: if (returnType.isArray()) {
224: returnObject = getArray(attrName, m.getReturnType()
225: .getComponentType());
226:
227: } else if (returnType.equals(Map.class)) {
228:
229: returnObject = getMap(attrName,
230: getCollectionComponentType(attrName));
231: } else {
232:
233: if ((args == null) || (args.length == 0)) {
234: // Simple get retrieval
235: returnObject = lookupAttribute(attrName, m
236: .getReturnType());
237:
238: } else if (args.length == 1) {
239: // Could be an array or a map lookup
240: if (isMapAttribute(attrName)) {
241: returnObject = getMap(attrName,
242: getCollectionComponentType(attrName))
243: .get(args[0]);
244:
245: } else {
246: // Array retrieval
247: returnObject = getArrayValue(attrName, m
248: .getReturnType(), ((Integer) args[0])
249: .intValue());
250: }
251: }
252: }
253:
254: } else if (methodName.startsWith("is")) {
255: String attrName = m.getName().substring(2);
256: returnObject = lookupAttribute(attrName, m.getReturnType());
257:
258: } else if (methodName.startsWith("set")) {
259: // Only allow if document is writable
260: if (!this .isConfigurationWritable()) {
261: throw new java.lang.UnsupportedOperationException(this
262: .getClass().getName()
263: + "Configuration Document is read-only, write "
264: + "not supported.");
265: }
266:
267: String attrName = m.getName().substring(3);
268: if (args.length == 1) {
269: // Simple value set
270: alterAttribute(attrName, m.getParameterTypes()[0],
271: args[0]);
272:
273: } else if (args.length == 2) {
274:
275: if (isMapAttribute(attrName)) {
276: //Set the map value
277: setMapValue(attrName,
278: getCollectionComponentType(attrName),
279: args[0], args[1]);
280:
281: } else {
282: // Array index value set
283: setArrayValue(attrName, m.getParameterTypes()[1],
284: ((Integer) args[0]).intValue(), args[1]);
285: }
286: }
287:
288: } else if (methodName.startsWith("add")) {
289: // Only allow if document is writable
290: if (!this .isConfigurationWritable()) {
291: throw new java.lang.UnsupportedOperationException(this
292: .getClass().getName()
293: + "Configuration Document is read-only, write "
294: + "not supported.");
295: }
296:
297: String attrName = m.getName().substring(3);
298: addAttribute(attrName, m.getParameterTypes()[0], args[0]);
299:
300: } else {
301: throw new UnsupportedOperationException(
302: this .getClass().getName()
303: + ": Method named ["
304: + methodName
305: + "] in configuration "
306: + "interface ["
307: + m.getDeclaringClass()
308: + "] is not a supported "
309: + "method within a Configuration interface. Methods must "
310: + "conform to the JavaBeans specification.");
311: }
312:
313: return returnObject;
314: }
315:
316: /**
317: * Gets the value from an configuration array.
318: *
319: * @param attributeName the name of the attribute that holds the array
320: * @param type the type of class within the array
321: * @param index the index of the value to retreive
322: * @return the object at the given index in the array
323: * @throws InvalidParameterException indicates there was an error
324: * accessing the give index in the array such as an
325: * IndexOutOfBoundsException
326: */
327: protected Object getArrayValue(String attributeName, Class type,
328: int index) {
329:
330: Object array = getArray(attributeName, type);
331:
332: Object returnObject = null;
333: try {
334: returnObject = Array.get(array, index);
335: } catch (IndexOutOfBoundsException ioobe) {
336: throw new InvalidParameterException(this .getClass(),
337: "Indexed configuration out of bounds on document ["
338: + this .getConfigurationName()
339: + "] attribute [" + attributeName
340: + "] index [" + index + "]", ioobe);
341: }
342: return returnObject;
343: }
344:
345: /**
346: * Checks if the given attribute contains a map.
347: *
348: * @param attrName the attribute to test for being a map
349: * @return true if the given attribute is for a map
350: */
351: protected boolean isMapAttribute(String attrName) {
352: boolean isMapAttribute = false;
353:
354: try {
355: Method readMethod = this .documentType.getMethod("get"
356: + attrName, new Class[0]);
357:
358: isMapAttribute = readMethod.getReturnType().equals(
359: Map.class);
360: } catch (NoSuchMethodException e) {
361: // read method does not exist, so leave isMapAttribute as false
362: }
363:
364: return isMapAttribute;
365: }
366:
367: /**
368: * Gets the type of class contained within a map.
369: *
370: * @param attrName the name of the attribute to determine the
371: * class type of
372: * @return the type of class contained within the array or null
373: * if it is unable to determine the type.
374: */
375: protected Class getCollectionComponentType(String attrName) {
376: Class componentType = null;
377:
378: try {
379: Method readMethod = this .documentType.getMethod("get"
380: + attrName, new Class[] { String.class });
381:
382: componentType = readMethod.getReturnType();
383: } catch (NoSuchMethodException e) {
384: // read method does not exist, so leave componentType as null
385: }
386:
387: return componentType;
388: }
389:
390: /**
391: * Retrieves the type of a JavaBean attribute by introspecting for its
392: * retrieval method and checking its return type.
393: * @param attributeName the name of the attribute to get the type of
394: * @return the class type of the attribute
395: */
396: public Class getChildType(String attributeName) {
397: attributeName = StringUtil.capitalize(attributeName);
398: try {
399: Method method = this .documentType.getMethod("get"
400: + attributeName, new Class[0]);
401: return method.getReturnType();
402: } catch (NoSuchMethodException nsme) {
403: throw new InvalidParameterException(this .getClass(),
404: "Unknown attribute [" + attributeName
405: + "] on configuration ["
406: + this .getConfigurationName() + "]");
407: }
408: }
409:
410: /**
411: * Retrieves the default value for a requested configuration attribute
412: * from an interface static variable with the same name as the
413: * requested attribute.
414: *
415: * @param type the Class of the configuration interface that may
416: * contain a default value
417: * @param attributeName the name of the attribute being looked for
418: * @param returnType the return type of the method
419: * @return the default value of the specified attribute
420: */
421: protected Object lookupDefaultAttributeValue(Class type,
422: String attributeName, Class returnType) {
423:
424: try {
425:
426: Field field = type.getField(attributeName);
427: Object val = field.get(null);
428:
429: if (val == null) {
430: return null;
431: } else {
432: if (field.getType().equals(returnType)) {
433: // The attribute has a default and it is the right type
434: return val;
435: } else {
436: // Default value has the wrong type
437: StringBuffer buf = new StringBuffer();
438: buf
439: .append("The Default value in the configuration of [");
440: buf.append(type.getName());
441: buf.append("] was of type [");
442: buf.append(field.getType());
443: buf
444: .append("], but the configuration interface requires a ");
445: buf.append(" default of type [");
446: buf.append(returnType);
447: buf.append("].");
448:
449: throw new InvalidConfigurationException(this
450: .getClass(), this .getConfigurationName(),
451: buf.toString());
452:
453: }
454: }
455: } catch (NoSuchFieldException nsfe) {
456: // No field, no problem
457: // search super interfaces (depth first search)
458: Class[] super Interfaces = type.getInterfaces();
459: Object val = null;
460: for (int i = 0; ((i < super Interfaces.length) && (val == null)); i++) {
461:
462: val = lookupDefaultAttributeValue(super Interfaces[i],
463: attributeName, returnType);
464: }
465: return val;
466: } catch (IllegalAccessException iae) {
467: throw new InvalidConfigurationException(this .getClass(),
468: this .getConfigurationName(), attributeName,
469: "Could not access the default variable named ["
470: + attributeName
471: + "] in the configuration interface ["
472: + type.getName() + "].", iae);
473: } catch (IllegalArgumentException iae) {
474: throw new InvalidConfigurationException(this .getClass(),
475: this .getConfigurationName(), attributeName,
476: "Could not access the default variable named ["
477: + attributeName
478: + "] in the configuration interface ["
479: + type.getName() + "].", iae);
480: }
481:
482: }
483:
484: /**
485: * <P>Implementations of this method should provide the ability to alter an
486: * attribute within the configuration object setting its value to the
487: * provide <code>newValue</code>.
488: *
489: * @param attributeName the name of the attribute to be altered
490: * @param attributeType the type of the attribute
491: * @param newValue the new value to change that attribute to
492: */
493: public abstract void alterAttribute(String attributeName,
494: Class attributeType, Object newValue);
495:
496: /**
497: * <P>Implementations of this method should retrieve a value from the
498: * configuration object with the specified name and Type. The Type
499: * information is gleaned from the providing configuration interface
500: * and allows the system to determine the proper mechanism for
501: * instantiating this value. This may involve the usage of the
502: * micro-level formatting for configuration values.</P>
503: *
504: * @return the object representing the specified confuguration value
505: * @param attributeName the name of the attribute to retrieve
506: * @param returnType the class type of the object that should be returned
507: */
508: public abstract Object lookupAttribute(String attributeName,
509: Class returnType);
510:
511: /**
512: * <P>Retrieves an array of objects of the specified type that have
513: * the specified name. This might get a list of dependent objects or
514: * a list of string values.</P>
515: *
516: * @param attributeName the name of the array to retrieve
517: * @param componentType the type of the objects that should be retrieved
518: * within the array
519: * @return an array of objects that match the name and type specified
520: */
521: public abstract Object getArray(String attributeName,
522: Class componentType);
523:
524: /**
525: * <P>Retrieves a java.util.Map of objects of the specified type that have
526: * the specified name. This might get a list of dependent objects or
527: * a list of string values.</P>
528: *
529: * @param attributeName the name of the array to retrieve
530: * @param contentType the type of the objects that should be retrieved
531: * within the Map
532: * @return a Map of objects that match the name and type specified
533: */
534: public abstract Map getMap(String attributeName, Class contentType);
535:
536: /**
537: * <P>Sets the value of a specific index in an array of data in this
538: * configuration object. Implementing classes must set the specified index
539: * of the array named by <code>attributeName</code> to the specified value.
540: * </P>
541: *
542: * @param attributeName the name of the array to alter
543: * @param attributeType the type of the objects in the array
544: * @param index the indicie of the array to alter
545: * @param value the Object to set as the value of this indicie
546: */
547: public abstract void setArrayValue(String attributeName,
548: Class attributeType, int index, Object value);
549:
550: /**
551: * <P>Sets the value of a specific index in an array of data in this
552: * configuration object. Implementing classes must set the specified index
553: * of the array named by <code>attributeName</code> to the specified value.
554: * </P>
555: *
556: * @param attributeName the name of the array to alter
557: * @param attributeType the type of the objects in the map
558: * @param key the key within the map to set
559: * @param value the Object to set as the value of this indicie
560: */
561: public abstract void setMapValue(String attributeName,
562: Class attributeType, Object key, Object value);
563:
564: /**
565: * This method should add a child entity to this configuration entity
566: *
567: * @param entityName the name of the array on which to add a particular
568: * value
569: * @param type type of class of the child entity
570: * @param obj The object to be added as a configuration entity
571: * @return the element representing the added attribute
572: */
573: public abstract Element addAttribute(String entityName, Class type,
574: Object obj);
575:
576: /**
577: * <P>Returns the simple class name without the prepended package
578: * structure</P>
579: *
580: * @param theClass the class whose name is returned
581: * @return the unqualifiedclass name of the supplied class
582: */
583: public static String getSimpleClassName(Class theClass) {
584: String className = theClass.getName();
585: if (className.lastIndexOf('.') > className.lastIndexOf('$')) {
586: return className.substring(className.lastIndexOf('.') + 1);
587: } else {
588: return className.substring(className.lastIndexOf('$') + 1);
589: }
590: }
591:
592: /**
593: * @see Configuration#getConfigurationInterface()
594: */
595: public Class getConfigurationInterface() {
596: try {
597: String configurationInterfaceName = this .element
598: .getAttributeValue("ConfigurationInterface");
599:
600: if (configurationInterfaceName == null) {
601: throw new InvalidConfigurationException(
602: this .getClass(), getConfigurationName(),
603: "ConfigurationInterface", "No value specifed");
604: }
605:
606: return Class.forName(configurationInterfaceName);
607: } catch (ClassNotFoundException cnfe) {
608: throw new InvalidConfigurationException(this .getClass(),
609: getConfigurationName(), "ConfigurationInterface",
610: "Specifed class not found", cnfe);
611: }
612: }
613:
614: /**
615: * @see Configuration#getDataStructure()
616: */
617: public Document getDataStructure() {
618: return this .document;
619: }
620:
621: /**
622: * Retrieves the root element of the data being currently mapped by this
623: * configuration object. Child structures will have a different root then
624: * the root of the "Document" object.
625: *
626: * @return the root element being proxied by this object
627: */
628: public Element getRootElement() {
629: return this .element;
630: }
631:
632: /**
633: * @see Object#clone()
634: */
635: public abstract Object clone();
636:
637: /**
638: * Checks whether or not childElement is a child configuration or not
639: *
640: * @param childElement element to be tested.
641: * @return boolean true if childElement is a configuration
642: */
643: public boolean isChildConfiguration(Element childElement) {
644: return (childElement.getAttribute("ConfigurationInterface") != null || isReference(childElement));
645: }
646:
647: /**
648: * Checks whether or not element is a reference to another configuration.
649: *
650: * @param element element to check if it is a reference
651: * @return true is the text of element starts with REF_PREFIX
652: */
653: public boolean isReference(Element element) {
654: String textValue = element.getTextTrim();
655: return textValue.startsWith(REF_PREFIX);
656: }
657:
658: /**
659: * Gets the ConfigurationInterface specifed in an element. If the
660: * ConfigurationInterface is not defined by the element of the class
661: * specified is not found, defaultInterface is returned.
662: *
663: * @param element the element to retreive the configuration element from
664: * @param defaultInterface the default interface if there is no
665: * configuration interface defined by the element
666: * @return ConfigurationInterface from the element or default
667: */
668: protected Class getConfigurationInterface(Element element,
669: Class defaultInterface) {
670:
671: Class configurationInterface = defaultInterface;
672:
673: try {
674: String configTypeName = element
675: .getAttributeValue("ConfigurationInterface");
676:
677: if (configTypeName != null) {
678: configurationInterface = Class.forName(configTypeName);
679: }
680:
681: if (!Configuration.class
682: .isAssignableFrom(configurationInterface)) {
683: throw new InvalidConfigurationException(
684: this .getClass(),
685: this .getConfigurationName(),
686: this .element.getName(),
687: "The configured configuration interface does not extend "
688: + "[org.sape.carbon.core.config.Configuration] and "
689: + "therefore can not be instantiated. Please correct "
690: + "the class ["
691: + configurationInterface.getName()
692: + "]");
693: }
694:
695: } catch (ClassNotFoundException cnfe) {
696: throw new InvalidConfigurationException(
697: this .getClass(),
698: this .getConfigurationName(),
699: element.getName(),
700: "The child configuration has defined a configuration interface "
701: + "class which can not be found. Check the configuration document "
702: + "to ensure the class name is correct.",
703: cnfe);
704: } catch (ExceptionInInitializerError eiie) {
705: if (log.isTraceEnabled()) {
706: log.trace("Caught ExceptionInInitializerError [" + eiie
707: + "], returning default interface");
708: }
709: }
710:
711: return configurationInterface;
712: }
713:
714: /**
715: * @see org.sape.carbon.core.util.reflection.GenericProxy#proxyEquals(Object, Object)
716: */
717: protected Boolean proxyEquals(Object proxy, Object other) {
718: try {
719: Element proxyElement = ((Configuration) proxy)
720: .getRootElement();
721: Element otherElement = ((Configuration) other)
722: .getRootElement();
723:
724: if (proxyElement == otherElement) {
725: return Boolean.TRUE;
726: } else {
727: return Boolean.FALSE;
728: }
729: } catch (ClassCastException cce) {
730: return Boolean.FALSE;
731: }
732: }
733:
734: /**
735: * @see org.sape.carbon.core.util.reflection.GenericProxy#proxyHashCode(Object)
736: */
737: protected Integer proxyHashCode(Object proxy) {
738: Element proxyElement = ((Configuration) proxy).getRootElement();
739: return new Integer(System.identityHashCode(proxyElement));
740: }
741:
742: /**
743: * Is this configuration document writable or is it read-only. Shared cache
744: * instances of configuration objects should either be synchronized or
745: * marked not writable. When a configuration object is not writable, it
746: * should throw an exception if an attempt is made to modify it.
747: *
748: * @return true if this cofiguration object may be altered.
749: * @since carbon 1.1
750: */
751: public boolean isConfigurationWritable() {
752: return this .writable;
753: }
754:
755: /**
756: * Sets whether this configuration may be altered.
757: * @since carbon 1.1
758: */
759: public void setConfigurationReadOnly() {
760: this .writable = false;
761: }
762:
763: }
|