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;
024:
025: import java.io.BufferedReader;
026: import java.io.IOException;
027: import java.io.InputStream;
028: import java.io.InputStreamReader;
029: import java.net.MalformedURLException;
030: import java.net.URL;
031: import java.text.NumberFormat;
032: import java.util.Locale;
033: import java.util.MissingResourceException;
034: import java.util.ResourceBundle;
035: import java.util.Vector;
036:
037: import net.sf.jmoney.model2.Currency;
038: import net.sf.jmoney.model2.CurrencyInfo;
039: import net.sf.jmoney.model2.CurrentSessionChangeListener;
040: import net.sf.jmoney.model2.DatastoreManager;
041: import net.sf.jmoney.model2.ISessionChangeFirer;
042: import net.sf.jmoney.model2.ISessionFactory;
043: import net.sf.jmoney.model2.Propagator;
044: import net.sf.jmoney.model2.PropertySet;
045: import net.sf.jmoney.model2.Session;
046: import net.sf.jmoney.model2.SessionChangeFirerListener;
047: import net.sf.jmoney.model2.SessionChangeListener;
048: import net.sf.jmoney.views.TreeNode;
049:
050: import org.eclipse.core.runtime.CoreException;
051: import org.eclipse.core.runtime.IConfigurationElement;
052: import org.eclipse.core.runtime.IExtensionRegistry;
053: import org.eclipse.core.runtime.IStatus;
054: import org.eclipse.core.runtime.Platform;
055: import org.eclipse.core.runtime.Status;
056: import org.eclipse.jface.resource.ImageDescriptor;
057: import org.eclipse.swt.events.DisposeEvent;
058: import org.eclipse.swt.events.DisposeListener;
059: import org.eclipse.swt.graphics.Image;
060: import org.eclipse.swt.widgets.Control;
061: import org.eclipse.ui.IMemento;
062: import org.eclipse.ui.IWorkbenchWindow;
063: import org.eclipse.ui.plugin.AbstractUIPlugin;
064: import org.osgi.framework.BundleContext;
065:
066: /**
067: * The main plugin class to be used in the desktop.
068: */
069: public class JMoneyPlugin extends AbstractUIPlugin {
070:
071: public static final String PLUGIN_ID = "net.sf.jmoney";
072:
073: public static final boolean DEBUG = "true"
074: .equalsIgnoreCase(Platform
075: .getDebugOption("net.sf.jmoney/debug"));
076:
077: //The shared instance.
078: private static JMoneyPlugin plugin;
079: //Resource bundle.
080: private ResourceBundle resourceBundle;
081:
082: private DatastoreManager sessionManager = null;
083:
084: private Vector<CurrentSessionChangeListener> sessionChangeListeners = new Vector<CurrentSessionChangeListener>();
085:
086: // Create a listener that listens for changes to the new session.
087: private SessionChangeFirerListener sessionChangeFirerListener = new SessionChangeFirerListener() {
088: public void sessionChanged(ISessionChangeFirer firer) {
089: if (!sessionChangeListeners.isEmpty()) {
090: // Take a copy of the listener list. By doing this we
091: // allow listeners to safely add or remove listeners.
092: SessionChangeListener listenerArray[] = new SessionChangeListener[sessionChangeListeners
093: .size()];
094: sessionChangeListeners.copyInto(listenerArray);
095: for (SessionChangeListener listener : listenerArray) {
096: firer.fire(listener);
097: }
098: }
099:
100: }
101: };
102:
103: /**
104: * The constructor.
105: */
106: public JMoneyPlugin() {
107: super ();
108: plugin = this ;
109: try {
110: resourceBundle = ResourceBundle
111: .getBundle("net.sf.jmoney.resources.Language");
112: } catch (MissingResourceException x) {
113: resourceBundle = null;
114: }
115: }
116:
117: /**
118: * This method is called upon plug-in activation
119: */
120: @Override
121: public void start(BundleContext context) throws Exception {
122: super .start(context);
123:
124: PropertySet.init();
125: Propagator.init();
126: TreeNode.init();
127: }
128:
129: /**
130: * This method is called when the plug-in is stopped
131: */
132: @Override
133: public void stop(BundleContext context) throws Exception {
134: super .stop(context);
135: }
136:
137: /**
138: * Returns the shared instance.
139: */
140: public static JMoneyPlugin getDefault() {
141: return plugin;
142: }
143:
144: /**
145: * Returns the string from the plugin's resource bundle,
146: * or 'key' if not found.
147: */
148: public static String getResourceString(String key) {
149: ResourceBundle bundle = JMoneyPlugin.getDefault()
150: .getResourceBundle();
151: try {
152: return (bundle != null) ? bundle.getString(key) : key;
153: } catch (MissingResourceException e) {
154: return key;
155: }
156: }
157:
158: public static Image createImage(String name) {
159: // String iconPath = "icons/";
160: String iconPath = "";
161: try {
162: URL installURL = getDefault().getBundle().getEntry("/");
163: URL url = new URL(installURL, iconPath + name);
164: return ImageDescriptor.createFromURL(url).createImage();
165: } catch (MalformedURLException e) {
166: // should not happen
167: return ImageDescriptor.getMissingImageDescriptor()
168: .createImage();
169: }
170: }
171:
172: public static ImageDescriptor createImageDescriptor(String name) {
173: // Make above call this, or remove above
174: // String iconPath = "icons/";
175: String iconPath = "";
176: try {
177: URL installURL = getDefault().getBundle().getEntry("/");
178: URL url = new URL(installURL, iconPath + name);
179: return ImageDescriptor.createFromURL(url);
180: } catch (MalformedURLException e) {
181: // should not happen
182: return ImageDescriptor.getMissingImageDescriptor();
183: }
184: }
185:
186: /**
187: * Log status to log the of this plug-in.
188: */
189: public static void log(IStatus status) {
190: getDefault().getLog().log(status);
191: }
192:
193: /**
194: * Log exception to the log of this plug-in.
195: *
196: * @param e Exception to log
197: */
198: public static void log(Throwable e) {
199: log(new Status(IStatus.ERROR, JMoneyPlugin.PLUGIN_ID,
200: IStatus.ERROR, "Internal errror", e));
201: }
202:
203: /**
204: * Returns the plugin's resource bundle,
205: */
206: public ResourceBundle getResourceBundle() {
207: return resourceBundle;
208: }
209:
210: public DatastoreManager getSessionManager() {
211: return sessionManager;
212: }
213:
214: /**
215: * Saves the old session.
216: * Returns false if canceled by user or the save fails.
217: */
218: public boolean saveOldSession(IWorkbenchWindow window) {
219: if (sessionManager == null) {
220: return true;
221: } else {
222: return sessionManager.canClose(window);
223: }
224: }
225:
226: // Helper method
227: // TODO: see if we really need this method.
228: public Session getSession() {
229: return sessionManager == null ? null : sessionManager
230: .getSession();
231: }
232:
233: /**
234: * Sets the Session object. The session object contains the accounting
235: * data so this method will replace the accounting data in the framework
236: * with a new set of accounting data. This method is normally called
237: * only by plug-ins that implement a datastore when accounting data
238: * is loaded.
239: *
240: * To avoid doing too much work and user input before setting the new
241: * session, only to find that the
242: * user does not want to close the previous session, plug-in actions
243: * that expect to set a new session should call canClose on the previous
244: * session before preparing the new session. It is the caller's
245: * responsibility to ensure that
246: * both canClose() and close() are called on the previous session.
247: * This method will not close any previously set session.
248: */
249: public void setSessionManager(DatastoreManager newSessionManager) {
250: // It is up to the caller to ensure that the previous session
251: // has been closed.
252:
253: if (sessionManager == newSessionManager)
254: return;
255: DatastoreManager oldSessionManager = sessionManager;
256: sessionManager = newSessionManager;
257:
258: /*
259: * JMoney depends on having at least one currency, which must also be
260: * set as the default currency. If there is no default currency then
261: * this must be a new datastore and we must set a default currency.
262: */
263: if (newSessionManager != null) {
264: if (getSession().getDefaultCurrency() == null) {
265: initSystemCurrency(getSession());
266: }
267: }
268:
269: // It is possible, tho I can't think why, that a listener who
270: // we tell of a change in the current session will modify either
271: // the old or the new session.
272: // The correct way of handling this is:
273: // - if a change is made to the old session then only those
274: // listeners that have not been told of the change of session
275: // should be told.
276: // - if a change is made to the new session then only those
277: // listeners that have already been told of the change of session
278: // (including the listener that made the change) should be told
279: // of the change.
280: // This code handles this correctly.
281:
282: // We do not support the scenario where a listener replaces the
283: // session itself while being notified of a change in the session.
284: // Any attempt to do this will cause an exception to be thrown.
285: // TODO: Throw this exception.
286:
287: // If a listener adds a further listener then the correct
288: // way of handling this is for the new listener to start
289: // recieving change notifications immediately. This includes
290: // changes made to the session by the listener that had added
291: // the new listener and also changes made by other listeners that
292: // had not, at the time the new listener had been created,
293: // been notified of the change in the current session.
294:
295: // TODO: Implement the above or decide on a design and what
296: // restrictions we impose.
297:
298: if (!sessionChangeListeners.isEmpty()) {
299: // Take a copy of the listener list. By doing this we
300: // allow listeners to safely add or remove listeners.
301: CurrentSessionChangeListener listenerArray[] = new CurrentSessionChangeListener[sessionChangeListeners
302: .size()];
303: sessionChangeListeners.copyInto(listenerArray);
304: for (int i = 0; i < listenerArray.length; i++) {
305: listenerArray[i].sessionReplaced(
306: oldSessionManager == null ? null
307: : oldSessionManager.getSession(),
308: newSessionManager == null ? null
309: : newSessionManager.getSession());
310: }
311: }
312:
313: // Stop listening to the old session and start listening to the
314: // new session for changes within the session.
315: if (oldSessionManager != null) {
316: oldSessionManager
317: .removeSessionChangeFirerListener(sessionChangeFirerListener);
318: }
319: if (newSessionManager != null) {
320: newSessionManager
321: .addSessionChangeFirerListener(sessionChangeFirerListener);
322: }
323: }
324:
325: /**
326: * Get the corresponding ISO currency for "code". If "session" already
327: * contains such a currency this currency is returned. Otherwise, we
328: * check our list of ISO 4217 currencies and we create a new currency
329: * instance for "session".
330: *
331: * @param session Session object which will contain the currency
332: * @param code ISO currency code
333: * @return Currency for "code"
334: */
335: public static Currency getIsoCurrency(Session session, String code) {
336: // Check if the currency already exists for this session.
337: Currency result = session.getCurrencyForCode(code);
338: if (result != null)
339: return result;
340:
341: // Find the currency in our list of ISO 4217 currencies
342: ResourceBundle res = ResourceBundle
343: .getBundle("net.sf.jmoney.resources.Currency");
344: byte decimals = 2;
345: try {
346: InputStream in = JMoneyPlugin.class
347: .getResourceAsStream("Currencies.txt");
348: BufferedReader buffer = new BufferedReader(
349: new InputStreamReader(in));
350: for (String line = buffer.readLine(); line != null; line = buffer
351: .readLine()) {
352: if (line.substring(0, 3).equals(code)) {
353: // The Currencies.txt file does not contain the number of decimals
354: // for every currency. If no number is in the file then a StringIndexOutOfBoundsException
355: // will be thrown and we assume two decimal places.
356: try {
357: decimals = Byte.parseByte(line.substring(4, 5));
358: } catch (StringIndexOutOfBoundsException e) {
359: decimals = 2;
360: }
361: }
362: }
363: } catch (IOException ioex) {
364: log(ioex);
365: } catch (NumberFormatException nfex) {
366: log(nfex);
367: }
368:
369: result = session.createCommodity(CurrencyInfo.getPropertySet());
370: result.setCode(code);
371: result.setName(res.getString(code));
372: result.setDecimals(decimals);
373:
374: return result;
375: }
376:
377: /**
378: * Whenever a new session is created, JMoney will set a single initial
379: * currency. The currency is taken from our list of ISO 4217
380: * currencies and chosen using information from the default locale.
381: * This currency is also set as the default currency.
382: * <P>
383: * By doing this, we minimize the number of steps that a new JMoney
384: * user must take to get started. If a user only ever uses a single
385: * currency then the user may never have to worry about currencies
386: * and may never see a currency selection control.
387: *
388: * @param session
389: */
390: public static void initSystemCurrency(Session session) {
391: Locale defaultLocale = Locale.getDefault();
392: NumberFormat format = NumberFormat
393: .getCurrencyInstance(defaultLocale);
394: String code = format.getCurrency().getCurrencyCode();
395: Currency currency = getIsoCurrency(session, code);
396: if (currency == null) {
397: // JMoney depends on a default currency
398: currency = getIsoCurrency(session, "USD");
399: }
400:
401: /*
402: * Note that although we are modifying the datastore,
403: * we do not make this an undoable operation. The user
404: * did not set this currency and the user should not be
405: * able to undo it.
406: */
407: session.setDefaultCurrency(currency);
408: }
409:
410: /**
411: * Adds a change listener.
412: * <P>
413: * The listener is active only for as long as the given control exists. When the
414: * given control is disposed, the listener is removed and will receive no more
415: * notifications.
416: * <P>
417: * This method is generally used when a listener is used to update contents in a
418: * control. Typically multiple controls are updated by a listener and the parent
419: * composite control is passed to this method.
420: *
421: * @param listener
422: * @param control
423: */
424: public void addSessionChangeListener(
425: final CurrentSessionChangeListener listener, Control control) {
426: sessionChangeListeners.add(listener);
427:
428: // Remove the listener when the given control is disposed.
429: control.addDisposeListener(new DisposeListener() {
430: public void widgetDisposed(DisposeEvent e) {
431: sessionChangeListeners.remove(listener);
432: }
433: });
434: }
435:
436: // Preferences
437:
438: /**
439: * Get the format to be used for dates. This format is
440: * compatible with the VerySimpleDateFormat class.
441: * The format is read from the preference store.
442: */
443: public String getDateFormat() {
444: /*
445: * The following line cannot return a null value, even if the user did
446: * not set a value, because a default value is set. The default value is
447: * set by by JMoneyPreferenceInitializer (an extension to the
448: * org.eclipse.core.runtime.preferences extension point).
449: */
450: return getPreferenceStore().getString("dateFormat");
451: }
452:
453: /**
454: * Given a memento containing the data needed to open a session,
455: * return the session. If the session is already open
456: * then return the session, otherwise the session is opened
457: * by this method and returned.
458: *
459: * @param memento
460: * @return
461: */
462: public static Session openSession(IMemento memento) {
463: if (memento != null) {
464: // This is a kludge. Only one session can be open at a time,
465: // therefore all views that need a session will save the same
466: // data in the session memento. Therefore, if a session is open,
467: // just return that. We know it is the right session.
468: if (getDefault().getSession() != null) {
469: return getDefault().getSession();
470: }
471:
472: String factoryId = memento
473: .getString("currentSessionFactoryId");
474: if (factoryId != null && factoryId.length() != 0) {
475: // Search for the factory.
476: IExtensionRegistry registry = Platform
477: .getExtensionRegistry();
478: for (IConfigurationElement element : registry
479: .getConfigurationElementsFor("org.eclipse.ui.elementFactories")) {
480: if (element.getName().equals("factory")) {
481: if (element.getAttribute("id")
482: .equals(factoryId)) {
483: try {
484: ISessionFactory listener = (ISessionFactory) element
485: .createExecutableExtension("class");
486:
487: // Create and initialize the session object from
488: // the data stored in the memento.
489: listener.openSession(memento
490: .getChild("currentSession"));
491: return getDefault().getSession();
492: } catch (CoreException e) {
493: // Could not create the factory given by the 'class' attribute
494: // Log the error and start JMoney with no open session.
495: e.printStackTrace();
496: }
497: break;
498: }
499: }
500: }
501: }
502: }
503:
504: return null;
505: }
506:
507: /**
508: * Helper method to compare two objects. Either or both
509: * the objects may be null. If both objects are null,
510: * they are considered equal.
511: *
512: * @param object
513: * @param object2
514: * @return
515: */
516: public static boolean areEqual(Object object1, Object object2) {
517: return (object1 == null) ? (object2 == null) : object1
518: .equals(object2);
519: }
520: }
|