001: /*
002: *
003: * JMoney - A Personal Finance Manager
004: * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net>
005: *
006: *
007: * This program is free software; you can redistribute it and/or modify
008: * it under the terms of the GNU General Public License as published by
009: * the Free Software Foundation; either version 2 of the License, or
010: * (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
020: *
021: */
022:
023: package net.sf.jmoney.model2;
024:
025: import java.lang.reflect.Field;
026: import java.util.Collection;
027: import java.util.HashMap;
028: import java.util.Map;
029:
030: /**
031: * This is the base class for all objects that may have extension
032: * property sets added by plug-ins. The framework supports the
033: * following objects that may be extended:
034: * <UL>
035: * <LI>Session</LI>
036: * <LI>Commodity</LI>
037: * <LI>Account</LI>
038: * <LI>Transaction</LI>
039: * <LI>Entry</LI>
040: * </UL>
041: * <P>
042: * Plug-ins are also able to create new classes of extendable
043: * objects by deriving classes from this class.
044: * <P>
045: * This class contains abstract methods for which an implementation
046: * must be provided.
047: *
048: * @author Nigel Westbury
049: */
050: public abstract class ExtendableObject {
051:
052: /**
053: * The key from which this object can be fetched from
054: * the datastore and a reference to this object obtained.
055: */
056: IObjectKey objectKey;
057:
058: /**
059: * Extendable objects may have extensions containing additional data needed
060: * by the plug-ins. Plug-ins add properties to an object class by creating a
061: * property set and then adding that property set to the object class. This
062: * map will map property sets to the appropriate extension object.
063: */
064: protected Map<ExtensionPropertySet<?>, ExtensionObject> extensions = new HashMap<ExtensionPropertySet<?>, ExtensionObject>();
065:
066: /**
067: * The key which contains this object's parent and also the list property
068: * which contains this object.
069: */
070: protected ListKey parentKey;
071:
072: protected abstract String getExtendablePropertySetId();
073:
074: /**
075: * Constructs a new object with property values obtained from
076: * the given IValues interface.
077: *
078: * Derived classes will set their own properties from this interface,
079: * but this method is responsible for ensuring the appropriate extensions
080: * are created and passes on the IValues interface to the extension constructors.
081: */
082: protected ExtendableObject(IObjectKey objectKey, ListKey parentKey,
083: IValues extensionValues) {
084: this .objectKey = objectKey;
085: this .parentKey = parentKey;
086:
087: for (ExtensionPropertySet<?> propertySet : extensionValues
088: .getNonDefaultExtensions()) {
089: ExtensionObject extensionObject = propertySet
090: .constructImplementationObject(this ,
091: extensionValues);
092: extensions.put(propertySet, extensionObject);
093: }
094: }
095:
096: /**
097: * Constructs a new object with default property values.
098: */
099: protected ExtendableObject(IObjectKey objectKey, ListKey parentKey) {
100: this .objectKey = objectKey;
101: this .parentKey = parentKey;
102: }
103:
104: /**
105: * @return The key that fetches this object.
106: */
107: public IObjectKey getObjectKey() {
108: return objectKey;
109: }
110:
111: /**
112: * @return
113: */
114: // TODO: do we need this as well as the method below?
115: public IObjectKey getParentKey() {
116: return parentKey == null ? null : parentKey.getParentKey();
117: }
118:
119: public ListKey getParentListKey() {
120: return parentKey;
121: }
122:
123: /**
124: * @return The session containing this object
125: */
126: public Session getSession() {
127: // The key must contain the session and so there is no reason
128: // for the extendable objects to also contain a session field.
129: // Get the session from the key.
130: return objectKey.getSession();
131: }
132:
133: /**
134: * @return The data manager containing this object
135: */
136: public DataManager getDataManager() {
137: // The key must contain the data manager and so there is no reason
138: // for the extendable objects to also contain a data manager field.
139: // Get the data manager from the key.
140: return objectKey.getDataManager();
141: }
142:
143: /**
144: * Two or more instantiated objects may represent the same object
145: * in the datastore. Such objects should be considered
146: * the same. Therefore this method overrides the default
147: * implementation that is based on Java identity.
148: * <P>
149: * This method also considers two objects to be the same if the
150: * other object is an extension object to an object that is
151: * the same object.
152: * <P>
153: * @return true if the two objects represent the same object
154: * in the datastore, false otherwise.
155: */
156: // If we had an interface with the getObjectKey() method that
157: // both ExtendableObject and ExtensionObject implemented, then
158: // this method would be simpler.
159: @Override
160: public boolean equals(Object object) {
161: // Two objects represent the same object if and only if
162: // the keys from which they were created are the same.
163: // Therefore we compare the key objects to see if they
164: // both contain the same data.
165: if (object instanceof ExtendableObject) {
166: ExtendableObject extendableObject = (ExtendableObject) object;
167: return getObjectKey().equals(
168: extendableObject.getObjectKey());
169: } else if (object instanceof ExtensionObject) {
170: ExtensionObject extensionObject = (ExtensionObject) object;
171: return getObjectKey()
172: .equals(extensionObject.getObjectKey());
173: } else {
174: return false;
175: }
176: }
177:
178: /**
179: * Required to support hash maps.
180: *
181: * If the datastore plug-in keeps the entire datastore in
182: * memory then the default hashCode implementation in the
183: * object key will work fine. However, if the datastore is
184: * backed by a database then multiple instances of the same
185: * object key may exist in memory. In such a case, a hashCode
186: * implementation must be provided for the object keys that
187: * return the same hash code for each instance of the object key.
188: */
189: @Override
190: public int hashCode() {
191: return getObjectKey().hashCode();
192: }
193:
194: // Should allow default package access and protected access
195: // but not public access. Unfortunately this cannot be done
196: // so for time being allow public access.
197: public <V> void processPropertyChange(
198: final ScalarPropertyAccessor<V> propertyAccessor,
199: final V oldValue, final V newValue) {
200: /*
201: * If the value is an extendable object then we check that both this object and this object
202: * and the value are from the same data manager. Mixing objects from different data
203: * managers is not allowed.
204: */
205: if (newValue instanceof ExtendableObject
206: && ((ExtendableObject) newValue).getDataManager() != getDataManager()) {
207: throw new RuntimeException(
208: "The object being set as the value of a property and the parent object are being managed by different data managers. Objects cannot contain references to objects from other data managers.");
209: }
210:
211: if (oldValue == newValue
212: || (oldValue != null && oldValue.equals(newValue)))
213: return;
214:
215: // Update the database.
216: ExtendablePropertySet<?> actualPropertySet = PropertySet
217: .getPropertySet(this .getClass());
218:
219: // Build two arrays of old and new values.
220: // Ultimately we will have a layer between that does this
221: // for us, also combining multiple updates to the same row
222: // into a single update. Until then, we need this code here.
223:
224: // TODO: improve performance here.
225: // TODO: Do we really need this, or, now that transactional
226: // processing is supported, is it unnecessary to support the
227: // passing of multiple values???
228: int count = actualPropertySet.getScalarProperties3().size();
229: Object[] oldValues = new Object[count];
230: Object[] newValues = new Object[count];
231:
232: int i = 0;
233: for (ScalarPropertyAccessor<?> propertyAccessor2 : actualPropertySet
234: .getScalarProperties3()) {
235: if (propertyAccessor2 == propertyAccessor) {
236: oldValues[i] = oldValue;
237: newValues[i] = newValue;
238: } else {
239: Object value = getPropertyValue(propertyAccessor2);
240: oldValues[i] = value;
241: newValues[i] = value;
242: }
243: i++;
244: }
245: objectKey.updateProperties(actualPropertySet, oldValues,
246: newValues);
247:
248: // Notify the change manager.
249: getSession().getChangeManager().processPropertyUpdate(this ,
250: propertyAccessor, oldValue, newValue);
251:
252: // Fire an event for this change.
253: getDataManager().fireEvent(new ISessionChangeFirer() {
254: public void fire(SessionChangeListener listener) {
255: listener.objectChanged(ExtendableObject.this ,
256: propertyAccessor, oldValue, newValue);
257: }
258: });
259: }
260:
261: /**
262: * Get the extension that implements the properties needed by
263: * a given plug-in.
264: *
265: * @param alwaysReturnNonNull
266: * If true then the return value is guaranteed to be non-null. If false
267: * then the return value may be null, indicating that all properties in
268: * the extension have default values.
269: */
270: public <X extends ExtensionObject> X getExtension(
271: ExtensionPropertySet<X> propertySet,
272: boolean alwaysReturnNonNull) {
273: X extension = propertySet.classOfObject.cast(extensions
274: .get(propertySet));
275:
276: if (extension == null && alwaysReturnNonNull) {
277: extension = propertySet
278: .constructDefaultImplementationObject(this );
279: extensions.put(propertySet, extension);
280: }
281:
282: return extension;
283: }
284:
285: /**
286: * Returns the value of a given property.
287: * <P>
288: * The property may be any property in the passed object,
289: * including properties that are stored in extension objects.
290: * The property may be defined in the actual class or
291: * any super classes which the class extends. The property
292: * may also be a property in any extension class which extends
293: * the class of this object or which extends any super class
294: * of the class of this object.
295: * <P>
296: * If the property is in an extension and that extension does
297: * not exist in this object then the default value of the
298: * property is returned.
299: */
300: public <T> T getPropertyValue(
301: ScalarPropertyAccessor<T> propertyAccessor) {
302: PropertySet propertySet = propertyAccessor.getPropertySet();
303: Object objectWithProperties;
304: Class<?> implementationClass;
305: if (!propertySet.isExtension()) {
306: objectWithProperties = this ;
307: implementationClass = propertySet.getImplementationClass();
308: } else {
309: ExtensionObject extension = getExtension(
310: (ExtensionPropertySet<?>) propertySet, false);
311:
312: implementationClass = ((ExtensionPropertySet) propertySet)
313: .getExtendablePropertySet()
314: .getImplementationClass();
315:
316: /*
317: * If there is no extension then we return the default value.
318: */
319: if (extension == null) {
320: return propertyAccessor.getDefaultValue();
321: }
322:
323: objectWithProperties = extension;
324: }
325:
326: if (!implementationClass.isAssignableFrom(getClass())) {
327: // TODO: We should be able to validate this at compile time using generics.
328: // This would involve adding the implementation class of the containing
329: // property set as a type parameter to all property accessors.
330: throw new RuntimeException("Property "
331: + propertyAccessor.getName()
332: + " is implemented by "
333: + implementationClass.getName()
334: + " but is being called on an object of type "
335: + getClass().getName());
336: }
337:
338: return propertyAccessor.invokeGetMethod(objectWithProperties);
339: }
340:
341: /**
342: * Obtain a the collection of values of a list property.
343: *
344: * @param propertyAccessor The property accessor for the property
345: * whose values are to be obtained. The property
346: * must be a list property (and not a scalar property).
347: */
348: public <E2 extends ExtendableObject> ObjectCollection<E2> getListPropertyValue(
349: ListPropertyAccessor<E2> owningListProperty) {
350: return owningListProperty.getElements(this );
351: }
352:
353: public <V> void setPropertyValue(
354: ScalarPropertyAccessor<V> propertyAccessor, V value) {
355: Object objectWithProperties;
356: PropertySet propertySet = propertyAccessor.getPropertySet();
357: if (!propertySet.isExtension()) {
358: objectWithProperties = this ;
359: } else {
360: // Get the extension, creating one if necessary.
361: objectWithProperties = getExtension(
362: (ExtensionPropertySet<?>) propertySet, true);
363: }
364:
365: propertyAccessor.invokeSetMethod(objectWithProperties, value);
366: }
367:
368: /**
369: * Return a list of extension that exist for this object.
370: * This is the list of extensions that have actually been
371: * created for this object, not the list of valid extensions
372: * for this object type. If no property values have yet been set
373: * in an extension that the extension will not have been created
374: * and will thus not be returned by this method.
375: * <P>
376: * It is more efficient to use this method than to loop through
377: * all the possible extension property sets and see which ones exist
378: * in this object.
379: *
380: * @return an Iterator that returns elements of type
381: * <code>Map.Entry</code>. Each Map.Entry contains a
382: * key of type PropertySet and a value of
383: * ExtensionObject.
384: */
385: public Collection<ExtensionPropertySet<?>> getExtensions() {
386: return extensions.keySet();
387: }
388:
389: /**
390: * This method is called when loading data from a datastore.
391: * Therefore the method can assume that there is no prior extension
392: * in this object for the given property set id. The results are
393: * undetermined if the extension already exists.
394: */
395: /*
396: remove this...
397: protected void importExtensionString(String propertySetId, String extensionString) {
398: // This is a bit of a kludge. We need to put the object
399: // into editable mode. This ensures that a request for an
400: // extension will always return a non-null extension.
401: // This is necessary when setting properties here, and also
402: // necessary that the code that propagates property changes
403: // through the propagators get non-null extensions.
404: alwaysReturnNonNullExtensions = true;
405:
406: PropertySet propertySet = PropertySet.getPropertySetCreatingIfNecessary(propertySetId, getExtendablePropertySetId());
407:
408: if (!propertySet.isExtensionClassKnown()) {
409: // The plug-in that originally implemented this extension
410: // is not installed. We therefore do not know the class
411: // that contains the properties. We must not lose the
412: // data in case the plug-in is installed later.
413: // We therefore store the data in the map as a String.
414: // If the plug-in is ever installed then the string can be
415: // de-serialized to produce the correct extension object.
416: extensions.put(propertySet, extensionString);
417: } else {
418: // Because the 'alwaysReturnNonNullExtensions' flag is set,
419: // this method will always return non-null extension.
420: ExtensionObject extension = getExtension(propertySet);
421:
422: stringToExtension(extensionString, extension);
423: }
424:
425: alwaysReturnNonNullExtensions = false;
426: }
427:
428:
429: protected static String extensionToString(ExtendableObject extension) {
430: BeanInfo beanInfo;
431: try {
432: beanInfo = Introspector.getBeanInfo(extension.getClass());
433: } catch (IntrospectionException e) {
434: throw new MalformedPluginException("Property set extension caused introspection error");
435: }
436:
437: StringBuffer buffer = new StringBuffer();
438: PropertyDescriptor pd[] = beanInfo.getPropertyDescriptors();
439: for (int j = 0; j < pd.length; j++) {
440: String name = pd[j].getName();
441: // Must have read and write method to be serialized.
442: Method readMethod = pd[j].getReadMethod();
443: Method writeMethod = pd[j].getWriteMethod();
444: // TODO figure out a better way of finding our properties
445: // than the following.
446: if (readMethod != null
447: && writeMethod != null
448: && readMethod.getDeclaringClass() != ExtendableObject.class
449: && writeMethod.getDeclaringClass() != ExtendableObject.class
450: && readMethod.getDeclaringClass() != AccountExtension.class
451: && writeMethod.getDeclaringClass() != AccountExtension.class
452: && readMethod.getDeclaringClass() != EntryExtension.class
453: && writeMethod.getDeclaringClass() != EntryExtension.class) {
454: Object value;
455: try {
456: value = readMethod.invoke(extension, (Object [])null);
457: } catch (IllegalAccessException e) {
458: throw new MalformedPluginException("Property set extension caused introspection error");
459: // throw new MalformedPluginException("Method 'getEntryExtensionClass' in '" + pluginBean.getClass().getName() + "' must be public.");
460: } catch (InvocationTargetException e) {
461: // Plugin error
462: throw new RuntimeException("bad error");
463: }
464: buffer.append('<');
465: buffer.append(name);
466: buffer.append('>');
467: buffer.append(value);
468: buffer.append("</");
469: buffer.append(name);
470: buffer.append('>');
471: }
472: }
473: return buffer.toString();
474: }
475:
476: protected static void stringToExtension(String s, ExtensionObject extension) {
477: ByteArrayInputStream bin = new ByteArrayInputStream(s.getBytes());
478:
479:
480: SAXParserFactory factory = SAXParserFactory.newInstance();
481: try {
482: SAXParser saxParser = factory.newSAXParser();
483: HandlerForExtensions handler = new HandlerForExtensions(extension);
484: saxParser.parse(bin, handler);
485: }
486: catch (ParserConfigurationException e) {
487: throw new RuntimeException("Serious XML parser configuration error");
488: }
489: catch (SAXException se) {
490: throw new RuntimeException("SAX exception error");
491: }
492: catch (IOException ioe) {
493: throw new RuntimeException("IO internal exception error");
494: }
495:
496: try {
497: bin.close();
498: }
499: catch (IOException e) {
500: throw new RuntimeException("internal error");
501: }
502: }
503:
504: private static class HandlerForExtensions extends DefaultHandler {
505:
506: ExtensionObject extension;
507:
508: BeanInfo beanInfo;
509:
510: Method writeMethod = null;
511:
512: HandlerForExtensions(ExtensionObject extension) {
513: this.extension = extension;
514:
515: try {
516: beanInfo = Introspector.getBeanInfo(extension.getClass());
517: } catch (IntrospectionException e) {
518: throw new MalformedPluginException("Property set extension caused introspection error");
519: }
520: }
521:
522: /**
523: * Receive notification of the start of an element.
524: *
525: * <p>See if there is a setter for this element name. If there is
526: * then set the setter. Otherwise set the setter to null to indicate
527: * that any character data should be ignored.
528: * </p>
529: * @param name The element type name.
530: * @param attributes The specified or defaulted attributes.
531: * @exception org.xml.sax.SAXException Any SAX exception, possibly
532: * wrapping another exception.
533: * @see org.xml.sax.ContentHandler#startElement
534: * /
535: public void startElement(String uri, String localName,
536: String qName, Attributes attributes)
537: throws SAXException {
538: String propertyName = qName;
539:
540: PropertyDescriptor pd[] = beanInfo.getPropertyDescriptors();
541: for (int j = 0; j < pd.length; j++) {
542: String name = pd[j].getName();
543: if (name.equals(propertyName)) {
544: // Must have write method in the extension class.
545: Method writeMethod = pd[j].getWriteMethod();
546: // TODO: clean up
547: if (writeMethod != null
548: && writeMethod.getDeclaringClass() != ExtendableObject.class
549: && writeMethod.getDeclaringClass() != AccountExtension.class
550: && writeMethod.getDeclaringClass() != EntryExtension.class) {
551: this.writeMethod = writeMethod;
552: }
553: break;
554: }
555: }
556: }
557:
558:
559: /**
560: * Receive notification of the end of an element.
561: *
562: * <p>Set the setter back to null.
563: * </p>
564: * @param name The element type name.
565: * @param attributes The specified or defaulted attributes.
566: * @exception org.xml.sax.SAXException Any SAX exception, possibly
567: * wrapping another exception.
568: * @see org.xml.sax.ContentHandler#endElement
569: * /
570: public void endElement(String uri, String localName, String qName)
571: throws SAXException {
572: writeMethod = null;
573: }
574:
575:
576: /**
577: * Receive notification of character data inside an element.
578: *
579: * <p>If a setter method is set then the character data is passed
580: * to the setter. Otherwise the character data is dropped.
581: * </p>
582: * @param ch The characters.
583: * @param start The start position in the character array.
584: * @param length The number of characters to use from the
585: * character array.
586: * @exception org.xml.sax.SAXException Any SAX exception, possibly
587: * wrapping another exception.
588: * @see org.xml.sax.ContentHandler#characters
589: * /
590: public void characters(char ch[], int start, int length)
591: throws SAXException {
592: if (writeMethod != null) {
593: Class type = writeMethod.getParameterTypes()[0];
594: Object value = null;
595:
596: // TODO: change this. Find a constructor from string.
597: if (type.equals(int.class)) {
598: String s = new String(ch, start, length);
599: value = new Integer(s);
600: } else if (type.equals(String.class)) {
601: value = new String(ch, start, length);
602: } else if (type.equals(char.class)) {
603: value = new Character(ch[start]);
604: } else {
605: throw new RuntimeException("unsupported type");
606: }
607:
608: try {
609: writeMethod.invoke(extension, new Object[] { value });
610: } catch (IllegalAccessException e) {
611: throw new MalformedPluginException("Property set extension caused introspection error");
612: // throw new MalformedPluginException("Method 'getEntryExtensionClass' in '" + pluginBean.getClass().getName() + "' must be public.");
613: } catch (InvocationTargetException e) {
614: // Plugin error
615: throw new RuntimeException("bad error");
616: }
617: }
618: }
619: }
620: */
621: /**
622: * This method is used to enable other classes in the package to
623: * access protected fields in the extendable objects.
624: *
625: * @param theObjectKeyField
626: * @return
627: */
628: Object getProtectedFieldValue(Field theObjectKeyField) {
629: try {
630: return theObjectKeyField.get(this );
631: } catch (IllegalArgumentException e) {
632: throw new RuntimeException("internal error", e);
633: } catch (IllegalAccessException e) {
634: e.printStackTrace();
635: // TODO: check the protection earlier and raise MalformedPlugin
636: throw new RuntimeException(
637: "internal error - field protection problem");
638: }
639: }
640:
641: /**
642: * This method allows datastore implementations to re-parent an
643: * object (move it from one list to another).
644: * <P>
645: * This method is to be used by datastore implementations only.
646: * Other plug-ins should not be calling this method.
647: *
648: * @param listKey
649: */
650: void replaceParentListKey(ListKey listKey) {
651: this.parentKey = listKey;
652: }
653: }
|