1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: Books.java 2099 2011-03-07 17:13:00Z dmsmith $
21   */
22  package org.crosswire.jsword.book;
23  
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.util.ArrayList;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.crosswire.common.activate.Activator;
32  import org.crosswire.common.util.CollectionUtil;
33  import org.crosswire.common.util.EventListenerList;
34  import org.crosswire.common.util.Logger;
35  import org.crosswire.common.util.PluginUtil;
36  import org.crosswire.common.util.Reporter;
37  import org.crosswire.jsword.JSOtherMsg;
38  
39  /**
40   * The Books class (along with Book) is the central point of contact between the
41   * rest of the world and this set of packages.
42   * 
43   * @see gnu.lgpl.License for license details.<br>
44   *      The copyright to this program is held by it's authors.
45   * @author Joe Walker [joe at eireneh dot com]
46   * @author DM Smith [dmsmith555 at yahoo dot com]
47   */
48  public final class Books implements BookList {
49      /**
50       * Create a singleton instance of the class. This is private to ensure that
51       * only one can be created. This also makes the class final!
52       */
53      private Books() {
54          books = new BookSet();
55          drivers = new HashSet<BookDriver>();
56          listeners = new EventListenerList();
57          threaded = false;
58  
59          initialize(threaded);
60      }
61  
62      /**
63       * Accessor for the singleton instance
64       * 
65       * @return The singleton instance
66       */
67      public static Books installed() {
68          return instance;
69      }
70  
71      /*
72       * (non-Javadoc)
73       * 
74       * @see org.crosswire.jsword.book.BookList#getBooks()
75       */
76      public synchronized List<Book> getBooks() {
77          return new BookSet(books);
78      }
79  
80      /*
81       * (non-Javadoc)
82       * 
83       * @see org.crosswire.jsword.book.BookList#getBook(java.lang.String)
84       */
85      public synchronized Book getBook(String name) {
86          if (name == null) {
87              return null;
88          }
89  
90          // Check name first
91          // First check for exact matches
92          for (Book book : books) {
93              if (name.equals(book.getName())) {
94                  return book;
95              }
96          }
97  
98          // Next check for case-insensitive matches
99          for (Book book : books) {
100             if (name.equalsIgnoreCase(book.getName())) {
101                 return book;
102             }
103         }
104 
105         // Then check initials
106         // First check for exact matches
107         for (Book book : books) {
108             BookMetaData bmd = book.getBookMetaData();
109             if (name.equals(bmd.getInitials())) {
110                 return book;
111             }
112         }
113 
114         // Next check for case-insensitive matches
115         for (Book book : books) {
116             if (name.equalsIgnoreCase(book.getInitials())) {
117                 return book;
118             }
119         }
120         return null;
121     }
122 
123     /*
124      * (non-Javadoc)
125      * 
126      * @see
127      * org.crosswire.jsword.book.BookList#getBooks(org.crosswire.jsword.book
128      * .BookFilter)
129      */
130     public synchronized List<Book> getBooks(BookFilter filter) {
131         List<Book> temp = CollectionUtil.createList(new BookFilterIterator(getBooks(), filter));
132         return new BookSet(temp);
133     }
134 
135     /**
136      * Get the maximum string length of a property
137      * 
138      * @param propertyKey
139      *            The desired property
140      * @return -1 if there is no match, otherwise the maximum length.
141      */
142     public int getMaxLength(String propertyKey) {
143         int max = -1;
144         for (Book book : getBooks()) {
145             Object property = book.getProperty(propertyKey);
146             if (property != null) {
147                 String value = property instanceof String ? (String) property : property.toString();
148                 max = Math.max(max, value.length());
149             }
150         }
151         return max;
152     }
153 
154     /**
155      * Get the maximum string length of a property on a subset of books.
156      * 
157      * @param propertyKey
158      *            The desired property
159      * @param filter
160      *            The filter
161      * @return -1 if there is no match, otherwise the maximum length.
162      */
163     public int getMaxLength(String propertyKey, BookFilter filter) {
164         int max = -1;
165         for (Book book : getBooks(filter)) {
166             Object property = book.getProperty(propertyKey);
167             if (property != null) {
168                 String value = property instanceof String ? (String) property : property.toString();
169                 max = Math.max(max, value.length());
170             }
171         }
172         return max;
173     }
174 
175     /*
176      * (non-Javadoc)
177      * 
178      * @see
179      * org.crosswire.jsword.book.BookList#addBooksListener(org.crosswire.jsword
180      * .book.BooksListener)
181      */
182     public synchronized void addBooksListener(BooksListener li) {
183         listeners.add(BooksListener.class, li);
184     }
185 
186     /*
187      * (non-Javadoc)
188      * 
189      * @see
190      * org.crosswire.jsword.book.BookList#removeBooksListener(org.crosswire.
191      * jsword.book.BooksListener)
192      */
193     public synchronized void removeBooksListener(BooksListener li) {
194         listeners.remove(BooksListener.class, li);
195     }
196 
197     /**
198      * Kick of an event sequence
199      * 
200      * @param source
201      *            The event source
202      * @param book
203      *            The changed Book
204      * @param added
205      *            Is it added?
206      */
207     protected synchronized void fireBooksChanged(Object source, Book book, boolean added) {
208         // Guaranteed to return a non-null array
209         Object[] contents = listeners.getListenerList();
210 
211         // Process the listeners last to first, notifying
212         // those that are interested in this event
213         BooksEvent ev = null;
214         for (int i = contents.length - 2; i >= 0; i -= 2) {
215             if (contents[i] == BooksListener.class) {
216                 if (ev == null) {
217                     ev = new BooksEvent(source, book, added);
218                 }
219 
220                 if (added) {
221                     ((BooksListener) contents[i + 1]).bookAdded(ev);
222                 } else {
223                     ((BooksListener) contents[i + 1]).bookRemoved(ev);
224                 }
225             }
226         }
227     }
228 
229     /**
230      * Add a Book to the current list of Books. This method should only be
231      * called by BibleDrivers, it is not a method for general consumption.
232      */
233     public synchronized void addBook(Book book) {
234         // log.debug("registering book: "+bmd.getName());
235 
236         books.add(book);
237         fireBooksChanged(instance, book, true);
238     }
239 
240     /**
241      * Remove a Book from the current list of Books. This method should only be
242      * called by BibleDrivers, it is not a method for general consumption.
243      */
244     public synchronized void removeBook(Book book) throws BookException {
245         // log.debug("unregistering book: "+bmd.getName());
246 
247         Activator.deactivate(book);
248 
249         boolean removed = books.remove(book);
250         if (removed) {
251             fireBooksChanged(instance, book, true);
252         } else {
253             throw new BookException(JSOtherMsg.lookupText("Could not remove unregistered Book: {0}", book.getName()));
254         }
255     }
256 
257     /**
258      * Register the driver, adding its books to the list. Any books that this
259      * driver used, but not any more are removed. This can be called repeatedly
260      * to re-register the driver.
261      * 
262      * @param driver
263      *            The BookDriver to add
264      */
265     public synchronized void registerDriver(BookDriver driver) throws BookException {
266         log.debug("begin registering driver: " + driver.getClass().getName());
267 
268         drivers.add(driver);
269 
270         // Go through all the books and add all the new ones.
271         // Remove those that are not known to the driver, but used to be.
272         Book[] bookArray = driver.getBooks();
273         Set<Book> current = CollectionUtil.createSet(new BookFilterIterator(getBooks(), BookFilters.getBooksByDriver(driver)));
274 
275         for (int j = 0; j < bookArray.length; j++) {
276             Book b = bookArray[j];
277             if (current.contains(b)) {
278                 // Since it was already in there, we don't add it.
279                 // By removing it from current we will be left with
280                 // what is not now known by the driver.
281                 current.remove(b);
282             } else {
283                 addBook(bookArray[j]);
284             }
285         }
286 
287         // Remove the books from the previous version of the driver
288         // that are not in this version.
289         for (Book book : current) {
290             removeBook(book);
291         }
292 
293         log.debug("end registering driver: " + driver.getClass().getName());
294     }
295 
296     /**
297      * Remove from the list of drivers
298      * 
299      * @param driver
300      *            The BookDriver to remove
301      */
302     public synchronized void unregisterDriver(BookDriver driver) throws BookException {
303         log.debug("begin un-registering driver: " + driver.getClass().getName());
304 
305         Book[] bookArray = driver.getBooks();
306         for (int j = 0; j < bookArray.length; j++) {
307             removeBook(bookArray[j]);
308         }
309 
310         if (!drivers.remove(driver)) {
311             throw new BookException(JSOtherMsg.lookupText("Could not remove unregistered Driver: {0}", driver.getClass().getName()));
312         }
313 
314         log.debug("end un-registering driver: " + driver.getClass().getName());
315     }
316 
317     /**
318      * Since Books keeps a track of drivers itself, including creating them when
319      * registered it can be hard to get a hold of the current book driver. This
320      * method gives access to the registered instances.
321      */
322     public synchronized BookDriver[] getDriversByClass(Class<? extends BookDriver> type) {
323         List<BookDriver> matches = new ArrayList<BookDriver>();
324         for (BookDriver driver : drivers) {
325             if (driver.getClass() == type) {
326                 matches.add(driver);
327             }
328         }
329 
330         return matches.toArray(new BookDriver[matches.size()]);
331     }
332 
333     /**
334      * Get an array of all the known drivers
335      * 
336      * @return Found int or the default value
337      */
338     public synchronized BookDriver[] getDrivers() {
339         return drivers.toArray(new BookDriver[drivers.size()]);
340     }
341 
342     /**
343      * Get an array of all the known drivers
344      * 
345      * @return Found int or the default value
346      */
347     public synchronized BookDriver[] getWritableDrivers() {
348         int i = 0;
349         for (BookDriver driver : drivers) {
350             if (driver.isWritable()) {
351                 i++;
352             }
353         }
354 
355         BookDriver[] reply = new BookDriver[i];
356 
357         i = 0;
358         for (BookDriver driver : drivers) {
359             if (driver.isWritable()) {
360                 reply[i++] = driver;
361             }
362         }
363 
364         return reply;
365     }
366 
367     /**
368      * Registers all the drivers known to the program. Either in a thread or in
369      * the main thread
370      */
371     private void initialize(boolean doThreading) {
372         if (doThreading) {
373             Runnable runner = new Runnable() {
374                 public void run() {
375                     autoRegister();
376                 }
377             };
378 
379             Thread init = new Thread(runner, "book-driver-registration");
380             init.setPriority(Thread.MIN_PRIORITY);
381             init.start();
382         } else {
383             autoRegister();
384         }
385     }
386 
387     /**
388      * Registers all the drivers known to the program.
389      */
390     protected void autoRegister() {
391         // This will classload them all and they will register themselves.
392         Class<? extends BookDriver>[] types = PluginUtil.getImplementors(BookDriver.class);
393 
394         log.debug("begin auto-registering " + types.length + " drivers:");
395 
396         for (int i = 0; i < types.length; i++) {
397             // job.setProgress(Msg.JOB_DRIVER.toString() +
398             // ClassUtils.getShortClassName(types[i]));
399 
400             try {
401                 Method driverInstance = types[i].getMethod("instance", new Class[0]);
402                 BookDriver driver = (BookDriver) driverInstance.invoke(null, new Object[0]); // types[i].newInstance();
403                 registerDriver(driver);
404             } catch (NoSuchMethodException e) {
405                 Reporter.informUser(Books.class, e);
406             } catch (IllegalArgumentException e) {
407                 Reporter.informUser(Books.class, e);
408             } catch (IllegalAccessException e) {
409                 Reporter.informUser(Books.class, e);
410             } catch (InvocationTargetException e) {
411                 Reporter.informUser(Books.class, e);
412             } catch (BookException e) {
413                 Reporter.informUser(Books.class, e);
414             }
415         }
416     }
417 
418     /**
419      * The list of Books
420      */
421     private BookSet books;
422 
423     /**
424      * An array of BookDrivers
425      */
426     private Set<BookDriver> drivers;
427 
428     /**
429      * The list of listeners
430      */
431     private EventListenerList listeners;
432 
433     /**
434      * Do we try to get clever in registering books?. Not until we can get it to
435      * work! At this time there is no way to set this or influence it So it just
436      * acts as a means of commenting out code.
437      */
438     private boolean threaded;
439 
440     /**
441      * The log stream
442      */
443     private static final Logger log = Logger.getLogger(Books.class);
444 
445     /**
446      * The singleton instance. This needs to be declared after all other statics
447      * it uses.
448      */
449     private static final Books instance = new Books();
450 }
451