Java tutorial
/* * * JMoney - A Personal Finance Manager * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net> * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * */ package net.sf.jmoney.serializeddatastore.formats; import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.text.DateFormat; import java.text.MessageFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import net.sf.jmoney.JMoneyPlugin; import net.sf.jmoney.model2.Account; import net.sf.jmoney.model2.AccountInfo; import net.sf.jmoney.model2.BankAccount; import net.sf.jmoney.model2.BankAccountInfo; import net.sf.jmoney.model2.CapitalAccount; import net.sf.jmoney.model2.Currency; import net.sf.jmoney.model2.CurrencyInfo; import net.sf.jmoney.model2.Entry; import net.sf.jmoney.model2.ExtendableObject; import net.sf.jmoney.model2.ExtendablePropertySet; import net.sf.jmoney.model2.ExtensionPropertySet; import net.sf.jmoney.model2.IListManager; import net.sf.jmoney.model2.IObjectKey; import net.sf.jmoney.model2.IValues; import net.sf.jmoney.model2.IncomeExpenseAccount; import net.sf.jmoney.model2.IncomeExpenseAccountInfo; import net.sf.jmoney.model2.ListKey; import net.sf.jmoney.model2.ListPropertyAccessor; import net.sf.jmoney.model2.PropertyAccessor; import net.sf.jmoney.model2.PropertyNotFoundException; import net.sf.jmoney.model2.PropertySet; import net.sf.jmoney.model2.PropertySetNotFoundException; import net.sf.jmoney.model2.ReferencePropertyAccessor; import net.sf.jmoney.model2.ScalarPropertyAccessor; import net.sf.jmoney.model2.Session; import net.sf.jmoney.model2.SessionInfo; import net.sf.jmoney.model2.Transaction; import net.sf.jmoney.serializeddatastore.IFileDatastore; import net.sf.jmoney.serializeddatastore.Messages; import net.sf.jmoney.serializeddatastore.SessionManager; import net.sf.jmoney.serializeddatastore.SimpleListManager; import net.sf.jmoney.serializeddatastore.SimpleObjectKey; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.ui.IWorkbenchWindow; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import org.xml.sax.helpers.DefaultHandler; /** * Implementation of the IFileDatastore extension listener for the JMoney JMX * format. * * @author Nigel Westbury */ public class JMoneyXmlFormat implements IFileDatastore { public static String ID_FILE_FORMAT = "net.sf.jmoney.serializeddatastore.jmxFormat"; //$NON-NLS-1$ /** * Date format used for dates in this file format: yyyy.MM.dd */ static SimpleDateFormat dateFormat = (SimpleDateFormat) DateFormat.getDateInstance(); static { dateFormat.applyPattern("yyyy.MM.dd"); //$NON-NLS-1$ } /** * Interface to generate id and idref values for objects. */ interface IdGenerator { /** * This method should be called only once for a given object. If it is * called more than once for the same object, this method MAY return a * different id. */ String generateId(ExtendableObject object); } class GenericIdGenerator implements IdGenerator { private String prefix; private int nextId = 1; public GenericIdGenerator(String prefix) { this.prefix = prefix; } public String generateId(ExtendableObject object) { return prefix + new Integer(nextId++).toString(); } } class CurrencyIdGenerator implements IdGenerator { public String generateId(ExtendableObject object) { return ((Currency) object).getCode(); } } /** * Maps property sets to IdGenerator implementations that generate the ids * for objects in that property set. * * Only property sets that may be referenced will be in this map. This * ensures that ids are generated only for object classes that may be * referenced, thus avoiding unnecessary ids from being written out. */ Map<ExtendablePropertySet, IdGenerator> idGenerators = new HashMap<ExtendablePropertySet, IdGenerator>(); /** * Read session from file. The session is set as the open session in the * given session manager. * <P> * The opened session is set as the current open JMoney session. If no * session can be opened then an appropriate message is displayed to the * user and the previous session, if any, is left open. * <P> * If this method returns false then any previous session will be left open. * The caller will not display any error message. This method must display * an appropriate error message if the file cannot be read. * * @return true if the file was successfully read and the session was set in * the given session manager, false if the user cancelled the * operation or if a failure occurred */ public boolean readSession(final File sessionFile, final SessionManager sessionManager, final IWorkbenchWindow window) { try { if (sessionFile.length() < 500000) { // If the file is smaller than 500K then it is // not worthwhile using a progress monitor. // The monitor would flash up so quickly that the // user could not read it. readSessionQuietly(sessionFile, sessionManager, null); } else { IRunnableWithProgress readSessionRunnable = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException { // Set the number of work units in the monitor where // one work unit is reading 100 Kbytes. int workUnits = (int) (sessionFile.length() / 100000); monitor.beginTask(MessageFormat.format(Messages.JMoneyXmlFormat_OpeningFile, sessionFile), workUnits); try { readSessionQuietly(sessionFile, sessionManager, monitor); } catch (Exception ex) { throw new InvocationTargetException(ex); } finally { monitor.done(); } } }; ProgressMonitorDialog progressDialog = new ProgressMonitorDialog(window.getShell()); try { progressDialog.run(true, false, readSessionRunnable); } catch (InvocationTargetException e) { throw e.getCause(); } } } catch (InterruptedException e) { /* * If the user interrupted the read then no error message is * displayed. Currently this cannot happen because the cancel button * is not enabled in the progress dialog, but if the cancel button * is enabled then we do nothing here, leaving the previous session, * if any, open. */ return false; } catch (Throwable ex) { JMoneyPlugin.log(ex); String message = MessageFormat.format(Messages.JMoneyXmlFormat_ReadErrorMessage, sessionFile.getPath()); String title = Messages.JMoneyXmlFormat_ReadErrorTitle; MessageDialog.openError(window.getShell(), title, message); return false; } return true; } /** * This class extends FileInputStream and overrides the various read * methods, counting the total number of bytes read and updating the * progress monitor. * <P> * This stream is used as input to BufferedInputStream, either directly or * through GZIPInputStream. Of all the read methods, only read(byte b[], int * off, int len) is used by BufferedInputStream, and read() is used * occassionally by GZIPInputStream. However, for completeness, all the read * methods have been overridden to update the byte count. Other methods that * may affect the progress, such as skip(n), do not appear to be called by * the above consumers of the stream. */ private class FileInputStreamWithMonitor extends FileInputStream { private IProgressMonitor monitor; private long totalBytes = 0; private int previousTotalWork = 0; /** * @param monitor * The monitor to be updated. This parameter must be * non-null. The monitor must have been initialized for an * expected amount of total work units where one work unit is * reading 100 KBytes of the input stream. */ FileInputStreamWithMonitor(File sessionFile, IProgressMonitor monitor) throws FileNotFoundException { super(sessionFile); this.monitor = monitor; } /* * This method reads a single byte at a time. GZIPInputStream uses this * method occassionally, so we increment the count of bytes read just to * stop errors creeping in. However, we don't bother to update the * monitor. */ @Override public int read() throws IOException { totalBytes++; return super.read(); } @Override public int read(byte b[]) throws IOException { int bytesRead = super.read(b); updateProgress(bytesRead); return bytesRead; } @Override public int read(byte b[], int off, int len) throws IOException { int bytesRead = super.read(b, off, len); updateProgress(bytesRead); return bytesRead; } /** * Update the progress monitor. The number of bytes read from the input * stream is passed to this method and used to measure the progress. * * @param bytesRead * the number of bytes read from the input stream. */ private void updateProgress(int bytesRead) { if (bytesRead > 0) { totalBytes += bytesRead; int newTotalWork = (int) (totalBytes / 100000); if (newTotalWork > previousTotalWork) { monitor.worked(newTotalWork - previousTotalWork); previousTotalWork = newTotalWork; } } } } /** * Read a session from file, creating a session manager and a session. * * @param monitor * Monitor into which this method will call the beginTask method * and update the progress. This parameter may be null in which * this method will read the session without feedback on the * progress. */ public void readSessionQuietly(File sessionFile, SessionManager sessionManager, IProgressMonitor monitor) throws FileNotFoundException, IOException, CoreException { InputStream fin; if (monitor == null) { fin = new FileInputStream(sessionFile); } else { fin = new FileInputStreamWithMonitor(sessionFile, monitor); } // If the extension is 'xml' then no compression is used. // If the extension is 'jmx' then compression is used. GZIPInputStream gin = null; BufferedInputStream bin; if (sessionFile.getName().endsWith(".xml")) { //$NON-NLS-1$ bin = new BufferedInputStream(fin); } else { gin = new GZIPInputStream(fin); bin = new BufferedInputStream(gin); } // First attempt to read the XML as though it is in the // current format. SAXParserFactory factory = SAXParserFactory.newInstance(); try { try { idToObjectMap = new HashMap<String, SimpleObjectKey>(); currentSAXEventProcessor = null; factory.setValidating(false); factory.setNamespaceAware(true); SAXParser saxParser = factory.newSAXParser(); HandlerForObject handler = new HandlerForObject(sessionManager); saxParser.parse(bin, handler); Session newSession = handler.getSession(); sessionManager.setSession(newSession); } catch (ParserConfigurationException e) { throw new RuntimeException("Serious XML parser configuration error"); //$NON-NLS-1$ } catch (SAXException e) { // Workaround: OldFormatJMoneyFileException seems to be thrown // in // two different ways: Either embedded in a SAXException or // directly as an OldFormatJMoneyFileException. if (e.getException() instanceof OldFormatJMoneyFileException) { throw (OldFormatJMoneyFileException) e.getException(); } else { throw new RuntimeException("Fatal SAX parser error"); //$NON-NLS-1$ } } } catch (OldFormatJMoneyFileException se) { // This exception will be throw if the file is old format (0.4.5 or // prior). // Try reading as an old format file. // First close and re-open the file. bin.close(); if (gin != null) gin.close(); fin.close(); if (monitor == null) { fin = new FileInputStream(sessionFile); } else { fin = new FileInputStreamWithMonitor(sessionFile, monitor); } // If the extension is 'xml' then no compression is used. // If the extension is 'jmx' then compression is used. if (sessionFile.getName().endsWith(".xml")) { //$NON-NLS-1$ bin = new BufferedInputStream(fin); } else { gin = new GZIPInputStream(fin); bin = new BufferedInputStream(gin); } // The XMLDecoder must use the same classpath that was used to load // this class. // The classpath set in this thread is the system class path, and if // that // is used then XMLDecoder will not be able to find the classes // specified // in the XML. We must therefore temporarily replace the classpath. // ClassLoader originalClassLoader = // Thread.currentThread().getContextClassLoader(); // Thread.currentThread().setContextClassLoader(SerializedDatastorePlugin.getDefault().getDescriptor().getPluginClassLoader()); XMLDecoder dec = new XMLDecoder(bin); Object newSession = dec.readObject(); dec.close(); // Thread.currentThread().setContextClassLoader(originalClassLoader); if (!(newSession instanceof net.sf.jmoney.model.Session)) { throw new CoreException(new Status(Status.ERROR, "net.sf.jmoney.serializeddatastore", Status.OK, //$NON-NLS-1$ Messages.JMoneyXmlFormat_DeserializedMessage, null)); } SimpleObjectKey key = new SimpleObjectKey(sessionManager); Session newSessionNewFormat = new Session(key, null); key.setObject(newSessionNewFormat); sessionManager.setSession(newSessionNewFormat); convertModelOneFormat((net.sf.jmoney.model.Session) newSession, newSessionNewFormat); } catch (IOException ioe) { throw new RuntimeException("IO internal exception error"); //$NON-NLS-1$ } finally { bin.close(); if (gin != null) gin.close(); fin.close(); } } private class HandlerForObject extends DefaultHandler { protected SessionManager sessionManager; /** * The top level session object. */ private Session session; HandlerForObject(SessionManager sessionManager) { this.sessionManager = sessionManager; } Session getSession() { return session; } /** * Receive notification of the start of an element. * * <p> * See if there is a setter for this element name. If there is then set * the setter. Otherwise set the setter to null to indicate that any * character data should be ignored. * </p> * * @param name * The element type name. * @param attributes * The specified or defaulted attributes. * @exception org.xml.sax.SAXException * Any SAX exception, possibly wrapping another * exception. * @see org.xml.sax.ContentHandler#startElement */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (currentSAXEventProcessor == null) { if (!localName.equals("session")) { //$NON-NLS-1$ if (localName.equals("java")) { //$NON-NLS-1$ throw new OldFormatJMoneyFileException(); } else { throw new SAXException("Unexpected top level element '" + localName //$NON-NLS-1$ + "' found. The top level element must be either 'session' (new format file) or 'java' (old format file)."); //$NON-NLS-1$ } } // The session object is not likely to have an id, but pass // it anyway just for completeness. String id = attributes.getValue("id"); //$NON-NLS-1$ currentSAXEventProcessor = new ObjectProcessor(sessionManager, null, null, SessionInfo.getPropertySet(), id); } else { currentSAXEventProcessor.startElement(uri, localName, attributes); } } /** * Receive notification of the end of an element. * * <p> * Set the property accessor back to null. * </p> * * @param name * The element type name. * @param attributes * The specified or defaulted attributes. * @exception org.xml.sax.SAXException * Any SAX exception, possibly wrapping another * exception. * @see org.xml.sax.ContentHandler#endElement */ @Override public void endElement(String uri, String localName, String qName) { SAXEventProcessor parent = currentSAXEventProcessor.endElement(); if (parent == null) { // We are back at the top level. // Save this object because it is the session object. session = (Session) currentSAXEventProcessor.getValue(); } currentSAXEventProcessor = parent; } /** * Receive notification of character data inside an element. * * <p> * If a setter method is set then the character data is passed to the * setter. Otherwise the character data is dropped. * </p> * * @param ch * The characters. * @param start * The start position in the character array. * @param length * The number of characters to use from the character array. * @exception org.xml.sax.SAXException * Any SAX exception, possibly wrapping another * exception. * @see org.xml.sax.ContentHandler#characters */ @Override public void characters(char ch[], int start, int length) throws SAXException { if (currentSAXEventProcessor == null) { throw new SAXException("data outside top element is illegal"); //$NON-NLS-1$ } currentSAXEventProcessor.characters(ch, start, length); } } abstract private class SAXEventProcessor { protected SAXEventProcessor parent; protected SessionManager sessionManager; /** * Creates a new SAXEventProcessor object. * * @param parent * The event processor that was in effect. This newly created * event processor will take over and will process the * contents of an element. When the end tag for the element * is found then this original event processor must be * restored as the active event processor. */ SAXEventProcessor(SessionManager sessionManager, SAXEventProcessor parent) { this.sessionManager = sessionManager; this.parent = parent; } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * @param atts * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ public abstract void startElement(String uri, String localName, Attributes atts) throws SAXException; /** * DOCUMENT ME! * * @param ch * DOCUMENT ME! * @param start * DOCUMENT ME! * @param length * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ public void characters(char ch[], int start, int length) { /* ignore data by default */ } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * * @return DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ public SAXEventProcessor endElement() { return parent; } /** * This method is called each time the next processor down the stack * processed an 'end element' and returned control to this processor. * <P> * The processor below will pass the object that it had read in. This * object might be a scalar property value or an object that itself has * properties. */ public void elementCompleted(Object value) { // The default processing assumes that no inner // element processors will ever pass values back. // If inner elements may pass up values then this // method must be overridden. throw new RuntimeException("Value passed back from inner element but no value expected."); //$NON-NLS-1$ } /** * @return */ // TODO: This method can be called instead of using elementCompleted. // elementCompleted can then be removed. public abstract Object getValue(); } /** * An event processor that takes over processing while we are inside an * object. The processor looks for elements representing the properties of * the object. */ private class ObjectProcessor extends SAXEventProcessor { private ExtendablePropertySet<?> propertySet; /** * If we have processed the start of an element representing a property * but have not yet processed the end of the element then this field is * the property accessor. Otherwise this field is null. This property * may be a scalar property (containing an extendable object) or a list * property. */ private PropertyAccessor propertyAccessor = null; /** * Key to the object being parsed by this ObjectProcessor. * * Saved key of objects that might be referenced from other objects. The key is created * and added to the 'id to key' map before the object itself is created. We save the key * here so when the object is later created we can set the object into it. */ SimpleObjectKey objectKey; /** * Key to the list that contains the object being parsed. This is saved * because we will need it when the object is constructed. */ ListKey listKey; /** * The list of parameters to be passed to the constructor of this * object. */ Map<PropertyAccessor, Object> propertyValueMap = new HashMap<PropertyAccessor, Object>(); Set<ExtensionPropertySet<?>> nonDefaultExtensions = new HashSet<ExtensionPropertySet<?>>(); Object value; /** * * @param parent * The event processor that was in effect. This newly created * event processor will take over and will process the * contents of an element. When the end tag for the element * is found then this original event processor must be * restored as the active event processor. */ @SuppressWarnings("unchecked") ObjectProcessor(SessionManager sessionManager, ObjectProcessor parent, ListKey listKey, ExtendablePropertySet<?> propertySet, String id) { super(sessionManager, parent); this.listKey = listKey; this.propertySet = propertySet; /* * Create the object key now and put it in the id map, unless the object key has already * been created because it was referenced by a previous idref. * * Either way, the object will be set into the key later. */ objectKey = idToObjectMap.get(id); if (objectKey == null) { objectKey = new SimpleObjectKey(sessionManager); idToObjectMap.put(id, objectKey); } for (ListPropertyAccessor propertyAccessor : propertySet.getListProperties3()) { propertyValueMap.put(propertyAccessor, new SimpleListManager(sessionManager, new ListKey(objectKey, propertyAccessor))); } } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * @param atts * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ @Override public void startElement(String uri, String localName, Attributes atts) { // We set propertyAccessor to be the property accessor // for the property whose value is contained in this // element. This property may be a scalar or a list // property. // If the property is not found then we currently drop // the value. // TODO: Keep values even for unknown properties in // case plug-ins are installed later that can use the data. // All elements are expected to be in a namespace beginning // "http://jmoney.sf.net". If the element is a property in an // extension property set then the id of the extension property // set will be appended. String namespace; if (uri.length() == 20) { namespace = null; } else { namespace = uri.substring(21); } try { // Find the property accessor for this property. // The property may be in the property set for the // object or in the property set for any base objects, // or may be in extensions of this or any base object. // If no namespace is specified then we search only // this and the base property sets. if (namespace == null) { // Search this property set and base property sets, // but exclude extensions. propertyAccessor = propertySet .getPropertyAccessorGivenLocalNameAndExcludingExtensions(localName); } else { ExtensionPropertySet<?> propertySet = PropertySet.getExtensionPropertySet(namespace); propertyAccessor = propertySet.getProperty(localName); } } catch (PropertySetNotFoundException e) { // The property no longer exists. // TODO: Log this. When changing the properties, // one is supposed to provide upgrader properties // for all obsoleted properties. // We drop the value. // Ignore content currentSAXEventProcessor = new IgnoreElementProcessor(sessionManager, this, null); return; } catch (PropertyNotFoundException e) { // The property no longer exists. // TODO: Log this. When changing the properties, // one is supposed to provide upgrader properties // for all obsoleted properties. // We drop the value. // Ignore content currentSAXEventProcessor = new IgnoreElementProcessor(sessionManager, this, null); return; } if (propertyAccessor.isScalar()) { Class propertyClass = ((ScalarPropertyAccessor<?>) propertyAccessor).getClassOfValueObject(); // See if the 'idref' attribute is specified. String idref = atts.getValue("idref"); //$NON-NLS-1$ if (idref != null) { SimpleObjectKey value = idToObjectMap.get(idref); if (value == null) { value = new SimpleObjectKey(sessionManager); idToObjectMap.put(idref, value); } /* * Process this element. * * Although we already have all the data we need from the * start element, we still need a processor to process it. * Ideally we should create another processor which gives * errors if there is any additional data. * * We pass the value to the processor so that it can pass * the value back to us! (That is the design - it is up to * the inner processor to supply the value. It just so * happens in the case of an idref that we know the value * before we even create the inner processor). */ currentSAXEventProcessor = new IgnoreElementProcessor(sessionManager, this, value); } else { Assert.isTrue(!ExtendableObject.class.isAssignableFrom(propertyClass)); // Property class is primitive or primitive class currentSAXEventProcessor = new PropertyProcessor(sessionManager, this, propertyClass); } } else { ListPropertyAccessor<?> listProperty = (ListPropertyAccessor<?>) propertyAccessor; ExtendablePropertySet<?> typedPropertySet = listProperty.getElementPropertySet(); Class propertyClass = typedPropertySet.getImplementationClass(); ExtendablePropertySet<?> actualPropertySet; if (typedPropertySet.isDerivable()) { String propertySetId = atts.getValue("propertySet"); //$NON-NLS-1$ if (propertySetId == null) { throw new RuntimeException("No 'propertySet' attribute specified when required."); //$NON-NLS-1$ } try { actualPropertySet = PropertySet.getExtendablePropertySet(propertySetId); } catch (PropertySetNotFoundException e) { // TODO: The plug-in which defined the property set may // have been uninstalled. // Therefore it is incorrect to throw a runtime // exception. We must handle // this situation by ignoring this element. The // specifics of this process // are not simple and need some thinking. We probably // need a view in which unknown // data is shown together with the contributing // plug-in's symbolic name // and the user has the option of purging. throw new RuntimeException("Invalid 'propertySet' attribute specified."); //$NON-NLS-1$ } } else { actualPropertySet = typedPropertySet; } SimpleListManager list = (SimpleListManager) propertyValueMap.get(propertyAccessor); String id = atts.getValue("id"); //$NON-NLS-1$ currentSAXEventProcessor = new ObjectProcessor(sessionManager, this, list.getListKey(), actualPropertySet, id); } } /** * DOCUMENT ME! * * @param ch * DOCUMENT ME! * @param start * DOCUMENT ME! * @param length * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ @Override public void characters(char ch[], int start, int length) { for (int i = start; i < start + length; i++) { if (ch[i] != ' ' && ch[i] != '\n' && ch[i] != '\t') { throw new RuntimeException("unexpected character data found."); //$NON-NLS-1$ } } } @SuppressWarnings("unchecked") @Override public SAXEventProcessor endElement() { IValues values = new IValues() { public <V> V getScalarValue(ScalarPropertyAccessor<V> propertyAccessor) { if (propertyValueMap.containsKey(propertyAccessor)) { return propertyAccessor.getClassOfValueObject() .cast(propertyValueMap.get(propertyAccessor)); } else { return propertyAccessor.getDefaultValue(); } } public IObjectKey getReferencedObjectKey(ReferencePropertyAccessor<?> propertyAccessor) { if (propertyValueMap.containsKey(propertyAccessor)) { return (IObjectKey) propertyValueMap.get(propertyAccessor); } else { return null; } } public <E extends ExtendableObject> IListManager<E> getListManager(IObjectKey listOwnerKey, ListPropertyAccessor<E> listAccessor) { return (IListManager<E>) propertyValueMap.get(listAccessor); } public Collection<ExtensionPropertySet<?>> getNonDefaultExtensions() { return nonDefaultExtensions; } }; // We can now create the object. ExtendableObject extendableObject = propertySet.constructImplementationObject(objectKey, listKey, values); objectKey.setObject(extendableObject); // TODO: Move this out of format specific code if (extendableObject instanceof Account) { Account account = (Account) extendableObject; sessionManager.addAccountList(account); } if (extendableObject instanceof Entry) { Entry entry = (Entry) extendableObject; if (entry.getAccount() != null) { sessionManager.addEntryToList(entry.getAccount(), entry); } } // Pass the value back up to the outer element processor. if (parent != null) { parent.elementCompleted(extendableObject); } // Save the value so that getValue can return it. // TODO: Change this method so it returns the value, // and replace the getValue method with a getParent method. // That would be a little cleaner. value = extendableObject; return parent; } /** * The inner element processor has returned a value to us. We now set * the value into the appropriate property or add it to the appropriate * list. */ @SuppressWarnings("unchecked") @Override public void elementCompleted(Object value) { /* * Now we have the value of this property. If it is null, something * is wrong, because null values are not written out. An empty * element may exist, but only if the element content is of a type * that can construct a valid non-null value from an empty string, * so no null values should be here. */ if (value == null) { throw new RuntimeException("null value"); //$NON-NLS-1$ } if (propertyAccessor == null) { throw new RuntimeException("internal error"); //$NON-NLS-1$ } // Set the value in our object. If the property // is a list property then the object is added to // the list. if (propertyAccessor.isScalar()) { propertyValueMap.put(propertyAccessor, value); } else { // Must be an element in an array. SimpleListManager list = (SimpleListManager) propertyValueMap.get(propertyAccessor); list.add(value); } /* * Update set of all extensions for which a property value has been * set. */ if (propertyAccessor.getPropertySet() instanceof ExtensionPropertySet) { nonDefaultExtensions.add((ExtensionPropertySet) propertyAccessor.getPropertySet()); } } /* * (non-Javadoc) * * @seenet.sf.jmoney.serializeddatastore.SerializedDatastorePlugin. * SAXEventProcessor#getValue() */ @Override public Object getValue() { return value; } } /** * An event processor that takes over processing while we are inside a * scalar property. The processor looks for the character content of the * element (which gives the value of the property). */ private class PropertyProcessor extends SAXEventProcessor { /** * Class of the property we are expecting. This may be a primative or * Date. */ Class propertyClass; /** * Value of the property to be returned to the outer processor. */ Object value = null; String s = ""; //$NON-NLS-1$ /** * * @param parent * The event processor that was in effect. This newly created * event processor will take over and will process the * contents of an element. When the end tag for the element * is found then this original event processor must be * restored as the active event processor. */ PropertyProcessor(SessionManager sessionManager, SAXEventProcessor parent, Class<?> propertyClass) { super(sessionManager, parent); this.propertyClass = propertyClass; } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * @param atts * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ @Override public void startElement(String uri, String localName, Attributes atts) throws SAXException { throw new SAXException("element not expected inside scalar property"); //$NON-NLS-1$ } /** * DOCUMENT ME! * * @param ch * DOCUMENT ME! * @param start * DOCUMENT ME! * @param length * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ @Override public void characters(char ch[], int start, int length) { s += new String(ch, start, length); } @SuppressWarnings("unchecked") @Override public SAXEventProcessor endElement() { // TODO: change this. Find a constructor from string. if (propertyClass.equals(Integer.class)) { value = new Integer(s); } else if (propertyClass.equals(Long.class)) { value = new Long(s); } else if (propertyClass.equals(String.class)) { value = s; } else if (propertyClass.equals(Character.class)) { value = new Character(s.charAt(0)); } else if (propertyClass.equals(Boolean.class)) { value = new Boolean(s); } else if (propertyClass.equals(Date.class)) { try { value = dateFormat.parse(s); } catch (ParseException e) { // If the date does not parse then the file is not // valid, so throw an exception to cause a file read // failure. throw new RuntimeException("file contains invalid date"); //$NON-NLS-1$ } } else { // The property value is an class that is in none of the above // categories. We therefore use the string constructor to // construct // the object. try { value = propertyClass.getConstructor(new Class[] { String.class }) .newInstance(new Object[] { s }); } catch (Exception e) { // The classes used in the data model are checked when the // PropertySet and PropertyAccessor static fields are // initialized. Therefore other plug-ins should not be // able to cause an error here. // TODO: put the above mentioned check into the // initialization code. e.printStackTrace(); throw new RuntimeException("internal error"); //$NON-NLS-1$ } } // Pass the value back up to the outer element processor. parent.elementCompleted(value); return parent; } /* * (non-Javadoc) * * @seenet.sf.jmoney.serializeddatastore.SerializedDatastorePlugin. * SAXEventProcessor#getValue() */ @Override public Object getValue() { return value; } } /** * Process events that occur within any element for which we are not * interested in the contents. * <P> * This class does double duty. It processes both elements with an idref and * elements for which we know nothing about the object. In the former case, * a non-null value is passed to the constructor which is passed back as the * value of this element. In the latter case a null value is passed. */ private class IgnoreElementProcessor extends SAXEventProcessor { private Object value; /** * Creates a new IgnoreElementProcessor object. * * @param parent * DOCUMENT ME! * @param elementName * DOCUMENT ME! */ IgnoreElementProcessor(SessionManager sessionManager, SAXEventProcessor parent, Object value) { super(sessionManager, parent); this.value = value; } /** * Process elements that occur within an element for which we are * ignoring content. * * @param name * The name of the element found inside the element. * @param atts * A map object that contains the names and values of all the * attributes for the element. * * @throws SAXException * DOCUMENT ME! */ @Override public void startElement(String uri, String localName, Attributes atts) { // If we are ignoring an element, also ignore elements inside it. if (value != null) { throw new RuntimeException("Cannot have content inside an element with an idref"); //$NON-NLS-1$ } currentSAXEventProcessor = new IgnoreElementProcessor(sessionManager, this, null); } /** * DOCUMENT ME! * * @param name * DOCUMENT ME! * * @return DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ @Override public SAXEventProcessor endElement() { if (value != null) { parent.elementCompleted(value); } return parent; } /* * (non-Javadoc) * * @seenet.sf.jmoney.serializeddatastore.SerializedDatastorePlugin. * SAXEventProcessor#getValue() */ @Override public Object getValue() { return value; } } // Used for writing /** * PropertySet to String (namespace prefix) */ Map<PropertySet, String> namespaceMap; int accountId; Map<ExtendableObject, String> objectToIdMap; // Used for reading Map<String, SimpleObjectKey> idToObjectMap; /** * Current event processor. A stack of event processors is maintained as the * XML is parsed. Each event processor has a reference to the previous (next * outer) event processor. */ public SAXEventProcessor currentSAXEventProcessor; /** * Write session to file. */ public void writeSession(final SessionManager sessionManager, final File sessionFile, IWorkbenchWindow window) { // If there is any modified data in the controls in any of the // views, then commit these to the database now. // TODO: How do we do this? Should framework call first // commitRemainingUserChanges(); try { if (/* session.getTransactionCount() < 1000 */false) { // If the session has less than 1000 transactions then it is // not worthwhile using a progress monitor. // The monitor would flash up so quickly that the // user could not read it. writeSessionQuietly(sessionManager, sessionFile, null); } else { IRunnableWithProgress writeSessionRunnable = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException { // Set the number of work units in the monitor where // one work unit is writing 500 transactions // int workUnits = // (int)(session.getTransactionCount()/500); int workUnits = IProgressMonitor.UNKNOWN; monitor.beginTask(MessageFormat.format(Messages.JMoneyXmlFormat_SavingFile, sessionFile), workUnits); try { writeSessionQuietly(sessionManager, sessionFile, monitor); } catch (Exception ex) { throw new InvocationTargetException(ex); } finally { monitor.done(); } } }; ProgressMonitorDialog progressDialog = new ProgressMonitorDialog(window.getShell()); try { progressDialog.run(true, false, writeSessionRunnable); } catch (InvocationTargetException e) { throw e.getCause(); } } } catch (InterruptedException e) { // If the user inturrupted the write then we do nothing. // Currently this cannot happen because the cancel button is not // enabled in the progress dialog, but if the cancel button is // enabled // then a message should perhaps be displayed here indicating that // the // file is unusable. } catch (Throwable ex) { JMoneyPlugin.log(ex); fileWriteError(sessionFile, window); } } /** * Write session to file. * * @param monitor * Monitor into which this method will call the beginTask method * and update the progress. This parameter may be null in which * this method will write the session without feedback on the * progress. */ // TODO: update the monitor, perhaps by counting the transactions. public void writeSessionQuietly(SessionManager sessionManager, File sessionFile, IProgressMonitor monitor) throws IOException, SAXException, TransformerConfigurationException { /* * Initialize our id generator map */ IdGenerator genericGenerator = new GenericIdGenerator("id"); for (ExtendablePropertySet<?> propertySet : PropertySet.getAllExtendablePropertySets()) { for (ScalarPropertyAccessor propertyAccessor : propertySet.getScalarProperties2()) { if (ExtendableObject.class.isAssignableFrom(propertyAccessor.getClassOfValueType())) { // KLUDGE: Don't use generic for any objects derived from the account type if (!Account.class.isAssignableFrom(propertyAccessor.getClassOfValueType())) { idGenerators.put(PropertySet.getPropertySet(propertyAccessor.getClassOfValueType()), genericGenerator); } } } } // Add a couple of special case ones idGenerators.put(AccountInfo.getPropertySet(), new GenericIdGenerator("account")); idGenerators.put(CurrencyInfo.getPropertySet(), new CurrencyIdGenerator()); FileOutputStream fout = new FileOutputStream(sessionFile); // If the extension is 'xml' then no compression is used. // If the extension is 'jmx' then compression is used. BufferedOutputStream bout; if (sessionFile.getName().endsWith(".xml")) { //$NON-NLS-1$ bout = new BufferedOutputStream(fout); } else { GZIPOutputStream gout = new GZIPOutputStream(fout); bout = new BufferedOutputStream(gout); } namespaceMap = new HashMap<PropertySet, String>(); accountId = 1; objectToIdMap = new HashMap<ExtendableObject, String>(); StreamResult streamResult = new StreamResult(bout); SAXTransformerFactory tf = (SAXTransformerFactory) SAXTransformerFactory.newInstance(); // SAX2.0 ContentHandler. TransformerHandler hd = tf.newTransformerHandler(); Transformer serializer = hd.getTransformer(); serializer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1"); //$NON-NLS-1$ serializer.setOutputProperty(OutputKeys.INDENT, "no"); //$NON-NLS-1$ hd.setResult(streamResult); hd.startDocument(); writeObject(hd, sessionManager.getSession(), "session", Session.class); //$NON-NLS-1$ hd.endDocument(); bout.close(); fout.close(); } /** * * @param hd * @param object * @param elementName * @param propertyType * The typed class of the property. The property may be an object * of a class that is derived from this typed class. If the * property is a scalar property then the property type is * determined by inspecting the getter and setter methods. If the * property is a list property then the type is determined by * inspecting the adder and remover methods. * @throws SAXException */ void writeObject(TransformerHandler hd, ExtendableObject object, String elementName, Class propertyType) throws SAXException { // Find the property set information for this object. ExtendablePropertySet<?> propertySet = PropertySet.getPropertySet(object.getClass()); AttributesImpl atts = new AttributesImpl(); // Generate and declare the namespace prefixes. // All extension property sets have namespace prefixes. // Properties in base and derived property sets must be // unique within each object, so are all put in the // default namespace. atts.clear(); if (propertyType == Session.class) { atts.addAttribute("", "", "xmlns", "CDATA", "http://jmoney.sf.net"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ int suffix = 1; for (ExtensionPropertySet<?> extensionPropertySet : PropertySet.getAllExtensionPropertySets()) { // Put into our map. String namespacePrefix = "ns" + new Integer(suffix++).toString(); //$NON-NLS-1$ namespaceMap.put(extensionPropertySet, namespacePrefix); atts.addAttribute("", "", "xmlns:" + namespacePrefix, "CDATA", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$ "http://jmoney.sf.net/" + extensionPropertySet.getId()); //$NON-NLS-1$ } } String id = getId(propertySet, object); if (id != null) { atts.addAttribute("", "", "id", "CDATA", id); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } if (propertySet.getImplementationClass() != propertyType) { atts.addAttribute("", "", "propertySet", "CDATA", propertySet.getId()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } hd.startElement("", "", elementName, atts); //$NON-NLS-1$ //$NON-NLS-2$ /* * Write all properties for this object, including properties from base * objects and properties from extensions. * * For derived property sets, information must be in the XML that allows * the derived property set to be determined. This is done by outputting * the actual final property set id. The property set id is specified as * an attribute. * * When an object is not owned, an id is specified. These are specified * as 'id' and 'idref' attributes in the normal way. * * Write the list properties. This is done before the properties because * then, as it happens, we get no problems due to the single pass. * * TODO: we cannot rely on this mechanism to ensure all idref's are * written before they are used. */ for (ListPropertyAccessor<?> listAccessor : propertySet.getListProperties3()) { PropertySet<?> propertySet2 = listAccessor.getPropertySet(); if (!propertySet2.isExtension() || object.getExtension((ExtensionPropertySet<?>) propertySet2, false) != null) { for (ExtendableObject listElement : object.getListPropertyValue(listAccessor)) { writeObject(hd, listElement, listAccessor.getLocalName(), listAccessor.getElementPropertySet().getImplementationClass()); } } } for (ScalarPropertyAccessor propertyAccessor : propertySet.getScalarProperties3()) { PropertySet<?> propertySet2 = propertyAccessor.getPropertySet(); if (!propertySet2.isExtension() || object.getExtension((ExtensionPropertySet<?>) propertySet2, false) != null) { String name = propertyAccessor.getLocalName(); Object value = object.getPropertyValue(propertyAccessor); /* * If no element for a property exists in the file then the * property value is treated as null. Therefore, if the property * value is null, we do not write out an element. * * Strings are a special case because JMoney treats null strings * and empty strings the same. If a string is empty, we treat * the string as null and do not write out the value. */ if (value instanceof String && ((String) value).length() == 0) { value = null; } if (value != null) { atts.clear(); if (value instanceof ExtendableObject) { ExtendablePropertySet ps = PropertySet .getPropertySet(propertyAccessor.getClassOfValueType()); String idref = getId(ps, (ExtendableObject) value); atts.addAttribute("", "", "idref", "CDATA", idref); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } String qName; if (propertySet2.isExtension()) { String namespacePrefix = namespaceMap.get(propertySet2); qName = namespacePrefix + ":" + name; //$NON-NLS-1$ } else { qName = name; } hd.startElement("", "", qName, atts); //$NON-NLS-1$ //$NON-NLS-2$ if (!(value instanceof ExtendableObject)) { String text; if (value instanceof Date) { Date date = (Date) value; text = dateFormat.format(date); } else { text = value.toString(); } hd.characters(text.toCharArray(), 0, text.length()); } hd.endElement("", "", qName); //$NON-NLS-1$ //$NON-NLS-2$ } } } hd.endElement("", "", elementName); //$NON-NLS-1$ //$NON-NLS-2$ } private String getId(ExtendablePropertySet<?> propertySet, ExtendableObject object) { String id = objectToIdMap.get(object); if (id == null) { ExtendablePropertySet basePropertySet = propertySet; while (basePropertySet != null && !idGenerators.containsKey(basePropertySet)) { basePropertySet = basePropertySet.getBasePropertySet(); } if (basePropertySet != null) { IdGenerator generator = idGenerators.get(basePropertySet); id = generator.generateId(object); objectToIdMap.put(object, id); } } return id; } /** * This method is used when writing a session. */ public void fileWriteError(File file, IWorkbenchWindow window) { String message = MessageFormat.format(Messages.JMoneyXmlFormat_WriteErrorMessage, file.getPath()); String title = Messages.JMoneyXmlFormat_WriteErrorTitle; MessageDialog.openError(window.getShell(), title, message); } /** * Converts an old format session (net.sf.jmoney.model.Session) to the * latest format session (net.sf.jmoney.model2.Session). The current model * is implemented in the net.sf.jmoney.model2 package. The * net.sf.jmoney.model package implements an older model that is now * obsolete. This method allows persistent serializations of the old model * to be converted to the new model to ensure backwards compatibility. */ private void convertModelOneFormat(net.sf.jmoney.model.Session oldFormatSession, Session newSession) { Map<Object, Account> accountMap = new Hashtable<Object, Account>(); // Add the currencies JMoneyPlugin.initSystemCurrency(newSession); // Add the income and expense accounts net.sf.jmoney.model.CategoryNode root = oldFormatSession.getCategories().getRootNode(); for (Enumeration e = root.children(); e.hasMoreElements();) { net.sf.jmoney.model.CategoryNode node = (net.sf.jmoney.model.CategoryNode) e.nextElement(); Object obj = node.getUserObject(); if (obj instanceof net.sf.jmoney.model.SimpleCategory) { net.sf.jmoney.model.SimpleCategory oldCategory = (net.sf.jmoney.model.SimpleCategory) obj; IncomeExpenseAccount newCategory = newSession .createAccount(IncomeExpenseAccountInfo.getPropertySet()); copyCategoryProperties(oldCategory, newCategory, accountMap); } } // Add the capital accounts Vector oldAccounts = oldFormatSession.getAccounts(); for (Iterator iter = oldAccounts.iterator(); iter.hasNext();) { net.sf.jmoney.model.Account oldAccount = (net.sf.jmoney.model.Account) iter.next(); BankAccount newAccount = newSession.createAccount(BankAccountInfo.getPropertySet()); newAccount.setName(oldAccount.getName()); newAccount.setAbbreviation(oldAccount.getAbbrevation()); newAccount.setAccountNumber(oldAccount.getAccountNumber()); newAccount.setBank(oldAccount.getBank()); newAccount.setComment(oldAccount.getComment()); newAccount.setCurrency(JMoneyPlugin.getIsoCurrency(newSession, oldAccount.getCurrencyCode())); newAccount.setMinBalance(oldAccount.getMinBalance()); newAccount.setStartBalance(oldAccount.getStartBalance()); accountMap.put(oldAccount, newAccount); } // Add the transactions and entries // We must be very careful here. Consider a split entry that // contain a double entry within it. The other account in the // double entry does not see the split entry. There is simply no // way of getting to the split entry from the other account. // If we create a new format transaction for the old format // double entry then we are in trouble because we would need to // find and amend the transaction later when we find the split // entry. // When we find a split entry, we create the entire transaction // at that time. We know that the other half of any double entries // in the split entry cannot also be in a split entry, because // this could not have happened under the old model. // When we find a double entry (that is not part of a split entry) // we do not create the transaction because we do not know if the // other half of the entry is in a split entry. We add the // double entry to the set of double entries previously found. // However, if the other half of this entry is in the set then // we know neither half of the double entry is in a split entry, // so we create the transaction at that time. // Here is the set of double entries that have been found but // not yet processed. Set<net.sf.jmoney.model.DoubleEntry> doubleEntriesPreviouslyFound = new HashSet<net.sf.jmoney.model.DoubleEntry>(); // See if the plug-in for the reconciliation state is present. // If it is then we can copy the reconciliation state into // the extension for this plug-in. // This is an example of a plug-in that does not depend on another // plug-in but will use it if it is there. ScalarPropertyAccessor<?> statusProperty; try { ExtensionPropertySet<?> reconciliationProperties = PropertySet .getExtensionPropertySet("net.sf.jmoney.reconciliation.entryProperties"); //$NON-NLS-1$ statusProperty = (ScalarPropertyAccessor<?>) reconciliationProperties.getProperty("status"); //$NON-NLS-1$ } catch (PropertySetNotFoundException e) { // If the property set is not found then this means // the reconciliation plug-in is not installed. // We simply drop the reconciliation field in such // circumstances. // TODO It would be better if we saved the data // in case the user installs the plug-in later. // To do this, we must create a general purpose // property class that is able to store any property // given to it. // Alternatively, we could not do the above but // recommend creating an extension property and then // create a propagator to get the value into the // reconciliation plug-in. This would be better // if this process could be made more efficient. statusProperty = null; } catch (PropertyNotFoundException e) { // If the property is not found then this means // the reconciliation plug-in has been updated to // a later version and the 'status' property is not // longer supported in the later version. // The reconciliation plug-in should provide a 'status' // property with a setter only so that upgrades are // possible. However, plug-ins do not have to support // unlimited past versions (or should they)? // We simply drop the reconciliation field in such // circumstances. statusProperty = null; } for (Iterator iter = oldAccounts.iterator(); iter.hasNext();) { net.sf.jmoney.model.Account oldAccount = (net.sf.jmoney.model.Account) iter.next(); CapitalAccount newAccount = (CapitalAccount) accountMap.get(oldAccount); // As all accounts in the old format are bank accounts, we can get // the currency of the account simply by casting to BankAccount. Currency currencyForCategories = ((BankAccount) newAccount).getCurrency(); for (Iterator entryIter = oldAccount.getEntries().iterator(); entryIter.hasNext();) { net.sf.jmoney.model.Entry oldEntry = (net.sf.jmoney.model.Entry) entryIter.next(); if (oldEntry instanceof net.sf.jmoney.model.DoubleEntry) { net.sf.jmoney.model.DoubleEntry de = (net.sf.jmoney.model.DoubleEntry) oldEntry; // Only add this transaction if we have already come across // the // other half of this entry and so we know the other half is // not // part of a split entry. if (doubleEntriesPreviouslyFound.contains(de.getOther())) { Transaction trans = newSession.createTransaction(); trans.setDate(de.getDate()); Entry entry1 = trans.createEntry(); Entry entry2 = trans.createEntry(); entry1.setAmount(de.getAmount()); entry2.setAmount(-de.getAmount()); entry1.setAccount(accountMap.get(de.getOther().getCategory())); entry2.setAccount(accountMap.get(de.getCategory())); copyEntryProperties(de, entry1, statusProperty); copyEntryProperties(de.getOther(), entry2, statusProperty); } else { doubleEntriesPreviouslyFound.add(de); } } else if (oldEntry instanceof net.sf.jmoney.model.SplittedEntry) { net.sf.jmoney.model.SplittedEntry se = (net.sf.jmoney.model.SplittedEntry) oldEntry; Transaction trans = newSession.createTransaction(); trans.setDate(oldEntry.getDate()); // Add the entry for the account that was holding the split // entry. Entry subEntry = trans.createEntry(); subEntry.setAmount(oldEntry.getAmount()); subEntry.setAccount(newAccount); copyEntryProperties(oldEntry, subEntry, statusProperty); // Add an entry for each old entry in the split. for (Iterator subEntryIter = se.getEntries().iterator(); subEntryIter.hasNext();) { net.sf.jmoney.model.Entry oldSubEntry = (net.sf.jmoney.model.Entry) subEntryIter.next(); subEntry = trans.createEntry(); subEntry.setAmount(oldSubEntry.getAmount()); subEntry.setAccount(accountMap.get(oldSubEntry.getCategory())); copyEntryProperties(oldSubEntry, subEntry, statusProperty); // Under the old model, all categories are // multi-currency categories // and the currency of the category matches the currency // of the account // entry. We must set the currency. if (subEntry.getAccount() instanceof IncomeExpenseAccount) { subEntry.setIncomeExpenseCurrency(currencyForCategories); } } } else { Transaction trans = newSession.createTransaction(); trans.setDate(oldEntry.getDate()); Entry entry1 = trans.createEntry(); Entry entry2 = trans.createEntry(); entry1.setAmount(oldEntry.getAmount()); entry2.setAmount(-oldEntry.getAmount()); entry1.setAccount(newAccount); if (oldEntry.getCategory() != null) { entry2.setAccount(accountMap.get(oldEntry.getCategory())); } // Put the check, memo, valuta, and status into the account // entry only. // Assume the creation and description apply to both account // and // category. copyEntryProperties(oldEntry, entry1, statusProperty); entry2.setCreation(oldEntry.getCreation()); entry2.setMemo(oldEntry.getDescription()); // Under the old model, all categories are multi-currency // categories // and the currency of the category matches the currency of // the account // entry. We must set the currency. entry2.setIncomeExpenseCurrency(currencyForCategories); } } } } /** * Copies category properties across from old to new. Sub-categories are * also copied across. * * @param accountMap * this and all sub-categories are added to this map, mapping old * categories to the new categories */ private void copyCategoryProperties(net.sf.jmoney.model.SimpleCategory oldCategory, IncomeExpenseAccount newCategory, Map<Object, Account> accountMap) { accountMap.put(oldCategory, newCategory); newCategory.setName(oldCategory.getCategoryName()); for (Enumeration e2 = oldCategory.getCategoryNode().children(); e2.hasMoreElements();) { net.sf.jmoney.model.CategoryNode subNode = (net.sf.jmoney.model.CategoryNode) e2.nextElement(); Object obj2 = subNode.getUserObject(); if (obj2 instanceof net.sf.jmoney.model.SimpleCategory) { net.sf.jmoney.model.SimpleCategory oldSubCategory = (net.sf.jmoney.model.SimpleCategory) obj2; IncomeExpenseAccount newSubCategory = newCategory.createSubAccount(); copyCategoryProperties(oldSubCategory, newSubCategory, accountMap); accountMap.put(oldSubCategory, newSubCategory); } } } @SuppressWarnings("unchecked") private void copyEntryProperties(net.sf.jmoney.model.Entry oldEntry, Entry entry, ScalarPropertyAccessor<?> statusProperty) { entry.setCheck(oldEntry.getCheck()); entry.setCreation(oldEntry.getCreation()); if (oldEntry.getCategory() instanceof net.sf.jmoney.model.Account) { entry.setMemo(oldEntry.getMemo()); } else { entry.setMemo(oldEntry.getDescription()); } entry.setValuta(oldEntry.getValuta()); if (statusProperty != null && oldEntry.getStatus() != 0) { entry.setPropertyValue((ScalarPropertyAccessor<Integer>) statusProperty, oldEntry.getStatus()); } } }