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: AbstractSwordInstaller.java 2099 2011-03-07 17:13:00Z dmsmith $
21   */
22  package org.crosswire.jsword.book.install.sword;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.net.URI;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.zip.GZIPInputStream;
33  
34  import org.crosswire.common.progress.JobManager;
35  import org.crosswire.common.progress.Progress;
36  import org.crosswire.common.util.CWProject;
37  import org.crosswire.common.util.CollectionUtil;
38  import org.crosswire.common.util.IOUtil;
39  import org.crosswire.common.util.Logger;
40  import org.crosswire.common.util.NetUtil;
41  import org.crosswire.common.util.Reporter;
42  import org.crosswire.jsword.JSMsg;
43  import org.crosswire.jsword.JSOtherMsg;
44  import org.crosswire.jsword.book.Book;
45  import org.crosswire.jsword.book.BookDriver;
46  import org.crosswire.jsword.book.BookException;
47  import org.crosswire.jsword.book.BookFilter;
48  import org.crosswire.jsword.book.BookFilterIterator;
49  import org.crosswire.jsword.book.BookMetaData;
50  import org.crosswire.jsword.book.BookSet;
51  import org.crosswire.jsword.book.basic.AbstractBookList;
52  import org.crosswire.jsword.book.install.InstallException;
53  import org.crosswire.jsword.book.install.Installer;
54  import org.crosswire.jsword.book.sword.ConfigEntry;
55  import org.crosswire.jsword.book.sword.SwordBook;
56  import org.crosswire.jsword.book.sword.SwordBookDriver;
57  import org.crosswire.jsword.book.sword.SwordBookMetaData;
58  import org.crosswire.jsword.book.sword.SwordBookPath;
59  import org.crosswire.jsword.book.sword.SwordConstants;
60  
61  import com.ice.tar.TarEntry;
62  import com.ice.tar.TarInputStream;
63  
64  /**
65   * .
66   * 
67   * @see gnu.lgpl.License for license details.<br>
68   *      The copyright to this program is held by it's authors.
69   * @author Joe Walker [joe at eireneh dot com]
70   * @author DM Smith [dmsmith555 at yahoo dot com]
71   */
72  public abstract class AbstractSwordInstaller extends AbstractBookList implements Installer, Comparable<AbstractSwordInstaller> {
73      /**
74       * Utility to download a file from a remote site
75       * 
76       * @param job
77       *            The way of noting progress
78       * @param dir
79       *            The directory from which to download the file
80       * @param file
81       *            The file to download
82       * @throws InstallException
83       */
84      protected abstract void download(Progress job, String dir, String file, URI dest) throws InstallException;
85  
86      /*
87       * (non-Javadoc)
88       * 
89       * @see org.crosswire.jsword.book.install.Installer#getInstallerDefinition()
90       */
91      public String getInstallerDefinition() {
92          StringBuilder buf = new StringBuilder(host);
93          buf.append(',');
94          buf.append(packageDirectory);
95          buf.append(',');
96          buf.append(catalogDirectory);
97          buf.append(',');
98          buf.append(indexDirectory);
99          buf.append(',');
100         if (proxyHost != null) {
101             buf.append(proxyHost);
102         }
103         buf.append(',');
104         if (proxyPort != null) {
105             buf.append(proxyPort);
106         }
107         return buf.toString();
108     }
109 
110     /*
111      * (non-Javadoc)
112      * 
113      * @see
114      * org.crosswire.jsword.book.install.Installer#isNewer(org.crosswire.jsword
115      * .book.BookMetaData)
116      */
117     public boolean isNewer(Book book) {
118         File dldir = SwordBookPath.getSwordDownloadDir();
119 
120         SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData();
121         File conf = new File(dldir, sbmd.getConfPath());
122 
123         // The conf may not exist in our download dir.
124         // In this case we say that it should not be downloaded again.
125         if (!conf.exists()) {
126             return false;
127         }
128 
129         URI configURI = NetUtil.getURI(conf);
130 
131         URI remote = toRemoteURI(book);
132         return NetUtil.isNewer(remote, configURI, proxyHost, proxyPort);
133     }
134 
135     /*
136      * (non-Javadoc)
137      * 
138      * @see org.crosswire.jsword.book.BookList#getBooks()
139      */
140     public List<Book> getBooks() {
141         try {
142             if (!loaded) {
143                 loadCachedIndex();
144             }
145 
146             // We need to create a List from the Set returned by
147             // entries.values() so the underlying list is not modified.
148             return new ArrayList<Book>(entries.values());
149         } catch (InstallException ex) {
150             log.error("Failed to reload cached index file", ex);
151             return new ArrayList<Book>();
152         }
153     }
154 
155     /*
156      * (non-Javadoc)
157      * 
158      * @see org.crosswire.jsword.book.BookList#getBook(java.lang.String)
159      */
160     public synchronized Book getBook(String name) {
161         // Check name first
162         // First check for exact matches
163         for (Book book : getBooks()) {
164             if (name.equals(book.getName())) {
165                 return book;
166             }
167         }
168 
169         // Next check for case-insensitive matches
170         for (Book book : getBooks()) {
171             if (name.equalsIgnoreCase(book.getName())) {
172                 return book;
173             }
174         }
175 
176         // Then check initials
177         // First check for exact matches
178         for (Book book : getBooks()) {
179             BookMetaData bmd = book.getBookMetaData();
180             if (name.equals(bmd.getInitials())) {
181                 return book;
182             }
183         }
184 
185         // Next check for case-insensitive matches
186         for (Book book : getBooks()) {
187             if (name.equalsIgnoreCase(book.getInitials())) {
188                 return book;
189             }
190         }
191         return null;
192     }
193 
194     /*
195      * (non-Javadoc)
196      * 
197      * @see
198      * org.crosswire.jsword.book.BookList#getBooks(org.crosswire.jsword.book
199      * .BookFilter)
200      */
201     @Override
202     public synchronized List<Book> getBooks(BookFilter filter) {
203         List<Book> temp = CollectionUtil.createList(new BookFilterIterator(getBooks(), filter));
204         return new BookSet(temp);
205     }
206 
207     /*
208      * (non-Javadoc)
209      * 
210      * @see
211      * org.crosswire.jsword.book.install.Installer#install(org.crosswire.jsword
212      * .book.Book)
213      */
214     public void install(Book book) {
215         // // Is the book already installed? Then nothing to do.
216         // if (Books.installed().getBook(book.getName()) != null)
217         // {
218         // return;
219         // }
220         //
221         final SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData();
222 
223         // So now we know what we want to install - all we need to do
224         // is installer.install(name) however we are doing it in the
225         // background so we create a job for it.
226         final Thread worker = new Thread("DisplayPreLoader")
227         {
228             /*
229              * (non-Javadoc)
230              * 
231              * @see java.lang.Runnable#run()
232              */
233             @Override
234             public void run() {
235                 // TRANSLATOR: Progress label indicating the installation of a book. {0} is a placeholder for the name of the book.
236                 String jobName = JSMsg.gettext("Installing book: {0}", sbmd.getName());
237                 Progress job = JobManager.createJob(jobName, this);
238 
239                 // Don't bother setting a size, we'll do it later.
240                 job.beginJob(jobName);
241 
242                 yield();
243 
244                 URI temp = null;
245                 try {
246                     // TRANSLATOR: Progress label indicating the Initialization of installing of a book.
247                     job.setSectionName(JSMsg.gettext("Initializing"));
248 
249                     temp = NetUtil.getTemporaryURI("swd", ZIP_SUFFIX);
250 
251                     download(job, packageDirectory, sbmd.getInitials() + ZIP_SUFFIX, temp);
252 
253                     // Once the unzipping is started, we need to continue
254                     job.setCancelable(false);
255                     if (!job.isFinished()) {
256                         File dldir = SwordBookPath.getSwordDownloadDir();
257                         IOUtil.unpackZip(NetUtil.getAsFile(temp), dldir);
258                         // TRANSLATOR: Progress label for installing the conf file for a book.
259                         job.setSectionName(JSMsg.gettext("Copying config file"));
260                         sbmd.setLibrary(NetUtil.getURI(dldir));
261                         SwordBookDriver.registerNewBook(sbmd);
262                     }
263 
264                 } catch (IOException e) {
265                     Reporter.informUser(this, e);
266                     job.cancel();
267                 } catch (InstallException e) {
268                     Reporter.informUser(this, e);
269                     job.cancel();
270                 } catch (BookException e) {
271                     Reporter.informUser(this, e);
272                     job.cancel();
273                 } finally {
274                     job.done();
275                     // tidy up after ourselves
276                     // This is a best effort. If for some reason it does not delete now
277                     // it will automatically be deleted when the JVM exits normally.
278                     if (temp != null) {
279                         try {
280                             NetUtil.delete(temp);
281                         } catch (IOException e) {
282                             log.warn("Error deleting temp download file:" + e.getMessage());
283                         }
284                     }
285                 }
286             }
287         };
288 
289         // this actually starts the thread off
290         worker.setPriority(Thread.MIN_PRIORITY);
291         worker.start();
292     }
293 
294     /*
295      * (non-Javadoc)
296      * 
297      * @see org.crosswire.jsword.book.install.Installer#reloadIndex()
298      */
299     public void reloadBookList() throws InstallException {
300         // TRANSLATOR: Progress label for downloading one or more files.
301         String jobName = JSMsg.gettext("Downloading files");
302         Progress job = JobManager.createJob(jobName, Thread.currentThread());
303         job.beginJob(jobName);
304 
305         try {
306             URI scratchfile = getCachedIndexFile();
307             download(job, catalogDirectory, FILE_LIST_GZ, scratchfile);
308             loaded = false;
309         } catch (InstallException ex) {
310             job.cancel();
311             throw ex;
312         } finally {
313             job.done();
314         }
315     }
316 
317     /*
318      * (non-Javadoc)
319      * 
320      * @see
321      * org.crosswire.jsword.book.install.Installer#downloadSearchIndex(org.crosswire
322      * .jsword.book.BookMetaData, java.net.URI)
323      */
324     public void downloadSearchIndex(Book book, URI localDest) throws InstallException {
325         // TRANSLATOR: Progress label for downloading one or more files.
326         String jobName = JSMsg.gettext("Downloading files");
327         Progress job = JobManager.createJob(jobName, Thread.currentThread());
328         job.beginJob(jobName);
329 
330         try {
331             download(job, packageDirectory + '/' + SEARCH_DIR, book.getInitials() + ZIP_SUFFIX, localDest);
332         } catch (InstallException ex) {
333             job.cancel();
334             throw ex;
335         } finally {
336             job.done();
337         }
338     }
339 
340     /**
341      * Load the cached index file into memory
342      */
343     private void loadCachedIndex() throws InstallException {
344         // We need a sword book driver so the installer can use the driver
345         // name to use in deciding where to put the index.
346         BookDriver fake = SwordBookDriver.instance();
347 
348         entries.clear();
349 
350         URI cache = getCachedIndexFile();
351         if (!NetUtil.isFile(cache)) {
352             reloadBookList();
353         }
354 
355         InputStream in = null;
356         GZIPInputStream gin = null;
357         TarInputStream tin = null;
358         try {
359             ConfigEntry.resetStatistics();
360 
361             in = NetUtil.getInputStream(cache);
362             gin = new GZIPInputStream(in);
363             tin = new TarInputStream(gin);
364             while (true) {
365                 TarEntry entry = tin.getNextEntry();
366                 if (entry == null) {
367                     break;
368                 }
369 
370                 String internal = entry.getName();
371                 if (!entry.isDirectory()) {
372                     try {
373                         int size = (int) entry.getSize();
374 
375                         // Every now and then an empty entry sneaks in
376                         if (size == 0) {
377                             log.error("Empty entry: " + internal);
378                             continue;
379                         }
380 
381                         byte[] buffer = new byte[size];
382                         if (tin.read(buffer) != size) {
383                             // This should not happen, but if it does then skip
384                             // it.
385                             log.error("Did not read all that was expected " + internal);
386                             continue;
387                         }
388 
389                         if (internal.endsWith(SwordConstants.EXTENSION_CONF)) {
390                             internal = internal.substring(0, internal.length() - 5);
391                         } else {
392                             log.error("Not a SWORD config file: " + internal);
393                             continue;
394                         }
395 
396                         if (internal.startsWith(SwordConstants.DIR_CONF + '/')) {
397                             internal = internal.substring(7);
398                         }
399 
400                         SwordBookMetaData sbmd = new SwordBookMetaData(buffer, internal);
401                         sbmd.setDriver(fake);
402                         Book book = new SwordBook(sbmd, null);
403                         entries.put(book.getName(), book);
404                     } catch (IOException ex) {
405                         log.error("Failed to load config for entry: " + internal, ex);
406                     }
407                 }
408             }
409 
410             loaded = true;
411 
412             ConfigEntry.dumpStatistics();
413         } catch (IOException ex) {
414             throw new InstallException(JSOtherMsg.lookupText("Error loading from cache"), ex);
415         } finally {
416             IOUtil.close(tin);
417             IOUtil.close(gin);
418             IOUtil.close(in);
419         }
420     }
421 
422     /**
423      * @return the catologDirectory
424      */
425     public String getCatalogDirectory() {
426         return catalogDirectory;
427     }
428 
429     /**
430      * @param catologDirectory
431      *            the catologDirectory to set
432      */
433     public void setCatalogDirectory(String catologDirectory) {
434         this.catalogDirectory = catologDirectory;
435     }
436 
437     /**
438      * @return Returns the directory.
439      */
440     public String getPackageDirectory() {
441         return packageDirectory;
442     }
443 
444     /**
445      * @param newDirectory
446      *            The directory to set.
447      */
448     public void setPackageDirectory(String newDirectory) {
449         if (packageDirectory == null || !packageDirectory.equals(newDirectory)) {
450             packageDirectory = newDirectory;
451             loaded = false;
452         }
453     }
454 
455     /**
456      * @return the indexDirectory
457      */
458     public String getIndexDirectory() {
459         return indexDirectory;
460     }
461 
462     /**
463      * @param indexDirectory
464      *            the indexDirectory to set
465      */
466     public void setIndexDirectory(String indexDirectory) {
467         this.indexDirectory = indexDirectory;
468     }
469 
470     /**
471      * @return Returns the host.
472      */
473     public String getHost() {
474         return host;
475     }
476 
477     /**
478      * @param newHost
479      *            The host to set.
480      */
481     public void setHost(String newHost) {
482         if (host == null || !host.equals(newHost)) {
483             host = newHost;
484             loaded = false;
485         }
486     }
487 
488     /**
489      * @return Returns the proxyHost.
490      */
491     public String getProxyHost() {
492         return proxyHost;
493     }
494 
495     /**
496      * @param newProxyHost
497      *            The proxyHost to set.
498      */
499     public void setProxyHost(String newProxyHost) {
500         String pHost = null;
501         if (newProxyHost != null && newProxyHost.length() > 0) {
502             pHost = newProxyHost;
503         }
504         if (proxyHost == null || !proxyHost.equals(pHost)) {
505             proxyHost = pHost;
506             loaded = false;
507         }
508     }
509 
510     /**
511      * @return Returns the proxyPort.
512      */
513     public Integer getProxyPort() {
514         return proxyPort;
515     }
516 
517     /**
518      * @param newProxyPort
519      *            The proxyPort to set.
520      */
521     public void setProxyPort(Integer newProxyPort) {
522         if (proxyPort == null || !proxyPort.equals(newProxyPort)) {
523             proxyPort = newProxyPort;
524             loaded = false;
525         }
526     }
527 
528     /**
529      * The URL for the cached index file for this installer
530      */
531     protected URI getCachedIndexFile() throws InstallException {
532         try {
533             URI scratchdir = CWProject.instance().getWriteableProjectSubdir(getTempFileExtension(host, catalogDirectory), true);
534             return NetUtil.lengthenURI(scratchdir, FILE_LIST_GZ);
535         } catch (IOException ex) {
536             throw new InstallException(JSOtherMsg.lookupText("URL manipulation failed"), ex);
537         }
538     }
539 
540     /**
541      * What are we using as a temp filename?
542      */
543     private static String getTempFileExtension(String host, String catalogDir) {
544         return DOWNLOAD_PREFIX + host + catalogDir.replace('/', '_');
545     }
546 
547     /*
548      * (non-Javadoc)
549      * 
550      * @see java.lang.Object#equals(java.lang.Object)
551      */
552     @Override
553     public boolean equals(Object object) {
554         if (!(object instanceof AbstractSwordInstaller)) {
555             return false;
556         }
557         AbstractSwordInstaller that = (AbstractSwordInstaller) object;
558 
559         if (!equals(this.host, that.host)) {
560             return false;
561         }
562 
563         if (!equals(this.packageDirectory, that.packageDirectory)) {
564             return false;
565         }
566 
567         return true;
568     }
569 
570     /*
571      * (non-Javadoc)
572      * 
573      * @see java.lang.Comparable#compareTo(java.lang.Object)
574      */
575     public int compareTo(AbstractSwordInstaller myClass) {
576 
577         int ret = host.compareTo(myClass.host);
578         if (ret != 0) {
579             ret = packageDirectory.compareTo(myClass.packageDirectory);
580         }
581         return ret;
582     }
583 
584     /*
585      * (non-Javadoc)
586      * 
587      * @see java.lang.Object#hashCode()
588      */
589     @Override
590     public int hashCode() {
591         return host.hashCode() + packageDirectory.hashCode();
592     }
593 
594     /**
595      * Quick utility to check to see if 2 (potentially null) strings are equal
596      */
597     protected boolean equals(String string1, String string2) {
598         if (string1 == null) {
599             return string2 == null;
600         }
601         return string1.equals(string2);
602     }
603 
604     /**
605      * A map of the books in this download area
606      */
607     protected Map<String, Book> entries = new HashMap<String, Book>();
608 
609     /**
610      * The remote hostname.
611      */
612     protected String host;
613 
614     /**
615      * The remote proxy hostname.
616      */
617     protected String proxyHost;
618 
619     /**
620      * The remote proxy port.
621      */
622     protected Integer proxyPort;
623 
624     /**
625      * The directory containing zipped books on the <code>host</code>.
626      */
627     protected String packageDirectory = "";
628 
629     /**
630      * The directory containing the catalog of all books on the
631      * <code>host</code>.
632      */
633     protected String catalogDirectory = "";
634 
635     /**
636      * The directory containing the catalog of all books on the
637      * <code>host</code>.
638      */
639     protected String indexDirectory = "";
640 
641     /**
642      * Do we need to reload the index file
643      */
644     protected boolean loaded;
645 
646     /**
647      * The sword index file
648      */
649     protected static final String FILE_LIST_GZ = "mods.d.tar.gz";
650 
651     /**
652      * The suffix of zip books on this server
653      */
654     protected static final String ZIP_SUFFIX = ".zip";
655 
656     /**
657      * The log stream
658      */
659     protected static final Logger log = Logger.getLogger(AbstractSwordInstaller.class);
660 
661     /**
662      * The relative path of the dir holding the search index files
663      */
664     protected static final String SEARCH_DIR = "search/jsword/L1";
665 
666     /**
667      * When we cache a download index
668      */
669     protected static final String DOWNLOAD_PREFIX = "download-";
670 
671 }
672