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   * © CrossWire Bible Society, 2005 - 2016
18   *
19   */
20  package org.crosswire.jsword.book;
21  
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.TreeSet;
31  
32  import org.crosswire.common.activate.Activator;
33  import org.crosswire.common.util.CollectionUtil;
34  import org.crosswire.common.util.PluginUtil;
35  import org.crosswire.common.util.Reporter;
36  import org.crosswire.jsword.JSOtherMsg;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * The Books class (along with Book) is the central point of contact between the
42   * rest of the world and this set of packages.
43   * 
44   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
45   * @author Joe Walker
46   * @author DM Smith
47   */
48  public final class Books extends AbstractBookList {
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          super();
55          initials = new HashMap<String, Book>();
56          names = new HashMap<String, Book>();
57          drivers = new HashSet<BookDriver>();
58          books = new TreeSet();
59      }
60  
61      /**
62       * Accessor for the singleton instance
63       * 
64       * @return The singleton instance
65       */
66      public static Books installed() {
67          return instance;
68      }
69  
70      /* (non-Javadoc)
71       * @see org.crosswire.jsword.book.BookList#getBooks()
72       */
73      public synchronized List<Book> getBooks() {
74          return CollectionUtil.createList(books);
75      }
76  
77      /* (non-Javadoc)
78       * @see org.crosswire.jsword.book.BookList#getBooks(org.crosswire.jsword.book.BookFilter)
79       */
80      @Override
81      public synchronized List<Book> getBooks(BookFilter filter) {
82          return CollectionUtil.createList(new BookFilterIterator(books, filter));
83      }
84  
85      /**
86       * Search for the book by initials and name.
87       * Looks for exact matches first, then searches case insensitive. 
88       * In all cases the whole initials or the whole name has to match.
89       * 
90       * @param name The initials or name of the book to find
91       * @return the book or null
92       */
93      public synchronized Book getBook(String name) {
94          if (name == null) {
95              return null;
96          }
97  
98          Book book = initials.get(name);
99          if (book != null) {
100             return book;
101         }
102 
103         book = names.get(name);
104         if (book != null) {
105             return book;
106         }
107 
108         // Check for case-insensitive initial and name matches
109         for (Book b : books) {
110             if (name.equalsIgnoreCase(b.getInitials()) || name.equalsIgnoreCase(b.getName())) {
111                 return b;
112             }
113         }
114 
115         return null;
116     }
117 
118     /**
119      * Add a Book to the current list of Books. This method should only be
120      * called by BibleDrivers, it is not a method for general consumption.
121      * 
122      * @param book the book to add to this book list
123      */
124     public synchronized void addBook(Book book) {
125         if (book != null && books.add(book)) {
126             initials.put(book.getInitials(), book);
127             names.put(book.getName(), book);
128             fireBooksChanged(instance, book, true);
129         }
130     }
131 
132     /**
133      * Remove a Book from the current list of Books. This method should only be
134      * called by BibleDrivers, it is not a method for general consumption.
135      * 
136      * @param book the book to be removed from this book list
137      * @throws BookException when an error occurs when performing this method
138      */
139     public synchronized void removeBook(Book book) throws BookException {
140         // log.debug("unregistering book: {}", bmd.getName());
141 
142         Activator.deactivate(book);
143 
144         boolean removed = books.remove(book);
145         if (removed) {
146             initials.remove(book.getInitials());
147             names.remove(book.getName());
148             fireBooksChanged(instance, book, false);
149         } else {
150             throw new BookException(JSOtherMsg.lookupText("Could not remove unregistered Book: {0}", book.getName()));
151         }
152     }
153 
154     /**
155      * Register the driver, adding its books to the list. Any books that this
156      * driver used, but not any more are removed. This can be called repeatedly
157      * to re-register the driver.
158      * 
159      * @param driver
160      *            The BookDriver to add
161      * @throws BookException when an error occurs when performing this method
162      */
163     public synchronized void registerDriver(BookDriver driver) throws BookException {
164         log.debug("begin registering driver: {}", driver.getClass().getName());
165 
166         drivers.add(driver);
167 
168         // Go through all the books and add all the new ones.
169         // Remove those that are not known to the driver, but used to be.
170         Book[] bookArray = driver.getBooks();
171         Set<Book> current = CollectionUtil.createSet(new BookFilterIterator(books, BookFilters.getBooksByDriver(driver)));
172 
173         for (int j = 0; j < bookArray.length; j++) {
174             Book b = bookArray[j];
175             if (current.contains(b)) {
176                 // Since it was already in there, we don't add it.
177                 // By removing it from current we will be left with
178                 // what is not now known by the driver.
179                 current.remove(b);
180             } else {
181                 addBook(bookArray[j]);
182             }
183         }
184 
185         // Remove the books from the previous version of the driver
186         // that are not in this version.
187         for (Book book : current) {
188             removeBook(book);
189         }
190 
191         log.debug("end registering driver: {}", driver.getClass().getName());
192     }
193 
194     /**
195      * Since Books keeps a track of drivers itself, including creating them when
196      * registered it can be hard to get a hold of the current book driver. This
197      * method gives access to the registered instances.
198      * 
199      * @param type the type of BookDriver
200      * @return matching BookDrivers
201      */
202     public synchronized BookDriver[] getDriversByClass(Class<? extends BookDriver> type) {
203         List<BookDriver> matches = new ArrayList<BookDriver>();
204         for (BookDriver driver : drivers) {
205             if (driver.getClass() == type) {
206                 matches.add(driver);
207             }
208         }
209 
210         return matches.toArray(new BookDriver[matches.size()]);
211     }
212 
213     /**
214      * Get an array of all the known drivers
215      * 
216      * @return Found int or the default value
217      */
218     public synchronized BookDriver[] getDrivers() {
219         return drivers.toArray(new BookDriver[drivers.size()]);
220     }
221 
222     /**
223      * Registers all the drivers known to the program.
224      */
225     private void autoRegister() {
226         // This will classload them all and they will register themselves.
227         Class<? extends BookDriver>[] types = PluginUtil.getImplementors(BookDriver.class);
228 
229         log.debug("begin auto-registering {} drivers:", Integer.toString(types.length));
230 
231         for (int i = 0; i < types.length; i++) {
232             // job.setProgress(Msg.JOB_DRIVER.toString() +
233             // ClassUtils.getShortClassName(types[i]));
234 
235             try {
236                 Method driverInstance = types[i].getMethod("instance", new Class[0]);
237                 BookDriver driver = (BookDriver) driverInstance.invoke(null, new Object[0]); // types[i].newInstance();
238                 registerDriver(driver);
239             } catch (NoSuchMethodException e) {
240                 Reporter.informUser(Books.class, e);
241             } catch (IllegalArgumentException e) {
242                 Reporter.informUser(Books.class, e);
243             } catch (IllegalAccessException e) {
244                 Reporter.informUser(Books.class, e);
245             } catch (InvocationTargetException e) {
246                 Reporter.informUser(Books.class, e);
247             } catch (BookException e) {
248                 Reporter.informUser(Books.class, e);
249             }
250         }
251     }
252 
253     /**
254      * The collection of Books
255      */
256     private Set<Book> books;
257 
258     /**
259      * The map of book initials
260      */
261     private Map<String, Book> initials;
262 
263     /**
264      * The map of book names
265      */
266     private Map<String, Book> names;
267 
268     /**
269      * An array of BookDrivers
270      */
271     private Set<BookDriver> drivers;
272 
273     /**
274      * The log stream
275      */
276     private static final Logger log = LoggerFactory.getLogger(Books.class);
277 
278     /**
279      * The singleton instance.
280      * This needs to be declared after all other statics it uses.
281      */
282     private static final Books instance = new Books();
283     // And it cannot register books until it is fully constructed
284     // When this was the last call in the constructor it resulted
285     // in "instance" being null in something it called.
286     static {
287         log.trace("Auto-registering start @ {}", Long.toString(System.currentTimeMillis()));
288         instance.autoRegister();
289         log.trace("Auto-registering stop @ {}", Long.toString(System.currentTimeMillis()));
290     }
291 }
292