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 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the 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-2013
18   *     The copyright to this program is held by it's authors.
19   *
20   */
21  package org.crosswire.jsword.book;
22  
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.util.ArrayList;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  
30  import org.crosswire.common.activate.Activator;
31  import org.crosswire.common.util.CollectionUtil;
32  import org.crosswire.common.util.PluginUtil;
33  import org.crosswire.common.util.Reporter;
34  import org.crosswire.jsword.JSOtherMsg;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  /**
39   * The Books class (along with Book) is the central point of contact between the
40   * rest of the world and this set of packages.
41   * 
42   * @see gnu.lgpl.License for license details.<br>
43   *      The copyright to this program is held by it's authors.
44   * @author Joe Walker [joe at eireneh dot com]
45   * @author DM Smith
46   */
47  public final class Books extends AbstractBookList {
48      /**
49       * Create a singleton instance of the class. This is private to ensure that
50       * only one can be created. This also makes the class final!
51       */
52      private Books() {
53          super();
54          books = new BookSet();
55          drivers = new HashSet<BookDriver>();
56      }
57  
58      /**
59       * Accessor for the singleton instance
60       * 
61       * @return The singleton instance
62       */
63      public static Books installed() {
64          return instance;
65      }
66  
67      /* (non-Javadoc)
68       * @see org.crosswire.jsword.book.BookList#getBooks()
69       */
70      public synchronized List<Book> getBooks() {
71          return new BookSet(books);
72      }
73  
74      /**
75       * Search for the book by name. Looks for exact matches first,
76       * then searches case insensitive. If that doesn't work, matches
77       * the initials of the book first case sensitive, then insensitive.
78       * In all cases the whole name or the whole initials has to match.
79       * 
80       * @param name The book to find
81       * @return the book or null
82       */
83      public synchronized Book getBook(String name) {
84          if (name == null) {
85              return null;
86          }
87  
88          // Check name first
89          // First check for exact matches
90          for (Book book : books) {
91              if (name.equals(book.getName())) {
92                  return book;
93              }
94          }
95  
96          // Next check for case-insensitive matches
97          for (Book book : books) {
98              if (name.equalsIgnoreCase(book.getName())) {
99                  return book;
100             }
101         }
102 
103         // Then check initials
104         // First check for exact matches
105         for (Book book : books) {
106             BookMetaData bmd = book.getBookMetaData();
107             if (name.equals(bmd.getInitials())) {
108                 return book;
109             }
110         }
111 
112         // Next check for case-insensitive matches
113         for (Book book : books) {
114             if (name.equalsIgnoreCase(book.getInitials())) {
115                 return book;
116             }
117         }
118         return null;
119     }
120 
121     /* (non-Javadoc)
122      * @see org.crosswire.jsword.book.BookList#getBooks(org.crosswire.jsword.book.BookFilter)
123      */
124     @Override
125     public synchronized List<Book> getBooks(BookFilter filter) {
126         List<Book> temp = CollectionUtil.createList(new BookFilterIterator(getBooks(), filter));
127         return new BookSet(temp);
128     }
129 
130     /**
131      * Get the maximum string length of a property
132      * 
133      * @param propertyKey
134      *            The desired property
135      * @return -1 if there is no match, otherwise the maximum length.
136      */
137     public int getMaxLength(String propertyKey) {
138         int max = -1;
139         for (Book book : getBooks()) {
140             Object property = book.getProperty(propertyKey);
141             if (property != null) {
142                 String value = property instanceof String ? (String) property : property.toString();
143                 max = Math.max(max, value.length());
144             }
145         }
146         return max;
147     }
148 
149     /**
150      * Get the maximum string length of a property on a subset of books.
151      * 
152      * @param propertyKey
153      *            The desired property
154      * @param filter
155      *            The filter
156      * @return -1 if there is no match, otherwise the maximum length.
157      */
158     public int getMaxLength(String propertyKey, BookFilter filter) {
159         int max = -1;
160         for (Book book : getBooks(filter)) {
161             Object property = book.getProperty(propertyKey);
162             if (property != null) {
163                 String value = property instanceof String ? (String) property : property.toString();
164                 max = Math.max(max, value.length());
165             }
166         }
167         return max;
168     }
169 
170     /**
171      * Add a Book to the current list of Books. This method should only be
172      * called by BibleDrivers, it is not a method for general consumption.
173      */
174     public synchronized void addBook(Book book) {
175         // log.debug("registering book: "+bmd.getName());
176 
177         books.add(book);
178         fireBooksChanged(instance, book, true);
179     }
180 
181     /**
182      * Remove a Book from the current list of Books. This method should only be
183      * called by BibleDrivers, it is not a method for general consumption.
184      */
185     public synchronized void removeBook(Book book) throws BookException {
186         // log.debug("unregistering book: {}", bmd.getName());
187 
188         Activator.deactivate(book);
189 
190         boolean removed = books.remove(book);
191         if (removed) {
192             fireBooksChanged(instance, book, false);
193         } else {
194             throw new BookException(JSOtherMsg.lookupText("Could not remove unregistered Book: {0}", book.getName()));
195         }
196     }
197 
198     /**
199      * Register the driver, adding its books to the list. Any books that this
200      * driver used, but not any more are removed. This can be called repeatedly
201      * to re-register the driver.
202      * 
203      * @param driver
204      *            The BookDriver to add
205      */
206     public synchronized void registerDriver(BookDriver driver) throws BookException {
207         log.debug("begin registering driver: {}", driver.getClass().getName());
208 
209         drivers.add(driver);
210 
211         // Go through all the books and add all the new ones.
212         // Remove those that are not known to the driver, but used to be.
213         Book[] bookArray = driver.getBooks();
214         Set<Book> current = CollectionUtil.createSet(new BookFilterIterator(getBooks(), BookFilters.getBooksByDriver(driver)));
215 
216         for (int j = 0; j < bookArray.length; j++) {
217             Book b = bookArray[j];
218             if (current.contains(b)) {
219                 // Since it was already in there, we don't add it.
220                 // By removing it from current we will be left with
221                 // what is not now known by the driver.
222                 current.remove(b);
223             } else {
224                 addBook(bookArray[j]);
225             }
226         }
227 
228         // Remove the books from the previous version of the driver
229         // that are not in this version.
230         for (Book book : current) {
231             removeBook(book);
232         }
233 
234         log.debug("end registering driver: {}", driver.getClass().getName());
235     }
236 
237     /**
238      * Remove from the list of drivers
239      * 
240      * @param driver
241      *            The BookDriver to remove
242      */
243     public synchronized void unregisterDriver(BookDriver driver) throws BookException {
244         log.debug("begin un-registering driver: {}", driver.getClass().getName());
245 
246         Book[] bookArray = driver.getBooks();
247         for (int j = 0; j < bookArray.length; j++) {
248             removeBook(bookArray[j]);
249         }
250 
251         if (!drivers.remove(driver)) {
252             throw new BookException(JSOtherMsg.lookupText("Could not remove unregistered Driver: {0}", driver.getClass().getName()));
253         }
254 
255         log.debug("end un-registering driver: {}", driver.getClass().getName());
256     }
257 
258     /**
259      * Since Books keeps a track of drivers itself, including creating them when
260      * registered it can be hard to get a hold of the current book driver. This
261      * method gives access to the registered instances.
262      */
263     public synchronized BookDriver[] getDriversByClass(Class<? extends BookDriver> type) {
264         List<BookDriver> matches = new ArrayList<BookDriver>();
265         for (BookDriver driver : drivers) {
266             if (driver.getClass() == type) {
267                 matches.add(driver);
268             }
269         }
270 
271         return matches.toArray(new BookDriver[matches.size()]);
272     }
273 
274     /**
275      * Get an array of all the known drivers
276      * 
277      * @return Found int or the default value
278      */
279     public synchronized BookDriver[] getDrivers() {
280         return drivers.toArray(new BookDriver[drivers.size()]);
281     }
282 
283     /**
284      * Get an array of all the known drivers
285      * 
286      * @return Found int or the default value
287      */
288     public synchronized BookDriver[] getWritableDrivers() {
289         int i = 0;
290         for (BookDriver driver : drivers) {
291             if (driver.isWritable()) {
292                 i++;
293             }
294         }
295 
296         BookDriver[] reply = new BookDriver[i];
297 
298         i = 0;
299         for (BookDriver driver : drivers) {
300             if (driver.isWritable()) {
301                 reply[i++] = driver;
302             }
303         }
304 
305         return reply;
306     }
307 
308     /**
309      * Registers all the drivers known to the program.
310      */
311     private void autoRegister() {
312         // This will classload them all and they will register themselves.
313         Class<? extends BookDriver>[] types = PluginUtil.getImplementors(BookDriver.class);
314 
315         log.debug("begin auto-registering {} drivers:", Integer.toString(types.length));
316 
317         for (int i = 0; i < types.length; i++) {
318             // job.setProgress(Msg.JOB_DRIVER.toString() +
319             // ClassUtils.getShortClassName(types[i]));
320 
321             try {
322                 Method driverInstance = types[i].getMethod("instance", new Class[0]);
323                 BookDriver driver = (BookDriver) driverInstance.invoke(null, new Object[0]); // types[i].newInstance();
324                 registerDriver(driver);
325             } catch (NoSuchMethodException e) {
326                 Reporter.informUser(Books.class, e);
327             } catch (IllegalArgumentException e) {
328                 Reporter.informUser(Books.class, e);
329             } catch (IllegalAccessException e) {
330                 Reporter.informUser(Books.class, e);
331             } catch (InvocationTargetException e) {
332                 Reporter.informUser(Books.class, e);
333             } catch (BookException e) {
334                 Reporter.informUser(Books.class, e);
335             }
336         }
337     }
338 
339     /**
340      * The list of Books
341      */
342     private BookSet books;
343 
344     /**
345      * An array of BookDrivers
346      */
347     private Set<BookDriver> drivers;
348 
349     /**
350      * The log stream
351      */
352     private static final Logger log = LoggerFactory.getLogger(Books.class);
353 
354     /**
355      * The singleton instance.
356      * This needs to be declared after all other statics it uses.
357      */
358     private static final Books instance = new Books();
359     // And it cannot register books until it is fully constructed
360     // When this was the last call in the constructor it resulted
361     // in "instance" being null in something it called.
362     static {
363         instance.autoRegister();
364     }
365 }
366