Java tutorial
/** * Distribution License: * JSword is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License, version 2.1 or later * as published by the Free Software Foundation. This program is distributed * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The License is available on the internet at: * http://www.gnu.org/copyleft/lgpl.html * or by writing to: * Free Software Foundation, Inc. * 59 Temple Place - Suite 330 * Boston, MA 02111-1307, USA * * Copyright: 2005-2013 * The copyright to this program is held by it's authors. * */ package org.crosswire.jsword.book.install.sword; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.crosswire.common.progress.JobManager; import org.crosswire.common.progress.Progress; import org.crosswire.common.util.CWProject; import org.crosswire.common.util.CollectionUtil; import org.crosswire.common.util.IOUtil; import org.crosswire.common.util.NetUtil; import org.crosswire.common.util.Reporter; import org.crosswire.jsword.JSMsg; import org.crosswire.jsword.JSOtherMsg; import org.crosswire.jsword.book.AbstractBookList; import org.crosswire.jsword.book.Book; import org.crosswire.jsword.book.BookDriver; import org.crosswire.jsword.book.BookException; import org.crosswire.jsword.book.BookFilter; import org.crosswire.jsword.book.BookFilterIterator; import org.crosswire.jsword.book.BookMetaData; import org.crosswire.jsword.book.BookSet; import org.crosswire.jsword.book.install.InstallException; import org.crosswire.jsword.book.install.Installer; import org.crosswire.jsword.book.sword.ConfigEntry; import org.crosswire.jsword.book.sword.NullBackend; import org.crosswire.jsword.book.sword.SwordBook; import org.crosswire.jsword.book.sword.SwordBookDriver; import org.crosswire.jsword.book.sword.SwordBookMetaData; import org.crosswire.jsword.book.sword.SwordBookPath; import org.crosswire.jsword.book.sword.SwordConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The AbstractSwordInstaller provides for the common implementation of derived classes. * * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's authors. * @author Joe Walker [joe at eireneh dot com] * @author DM Smith */ public abstract class AbstractSwordInstaller extends AbstractBookList implements Installer, Comparable<AbstractSwordInstaller> { /** * Build a default AbstractSwordInstaller */ public AbstractSwordInstaller() { super(); } /** * Utility to download a file from a remote site * * @param job * The way of noting progress * @param dir * The directory from which to download the file * @param file * The file to download * @throws InstallException */ protected abstract void download(Progress job, String dir, String file, URI dest) throws InstallException; /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#getInstallerDefinition() */ public String getInstallerDefinition() { StringBuilder buf = new StringBuilder(host); buf.append(','); buf.append(packageDirectory); buf.append(','); buf.append(catalogDirectory); buf.append(','); buf.append(indexDirectory); buf.append(','); if (proxyHost != null) { buf.append(proxyHost); } buf.append(','); if (proxyPort != null) { buf.append(proxyPort); } return buf.toString(); } /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#isNewer(org.crosswire.jsword.book.Book) */ public boolean isNewer(final Book book) { SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData(); File conf = sbmd.getConfigFile(); // The conf may not exist in our download dir. // In this case we say that it should not be downloaded again. if (conf == null || !conf.exists()) { return false; } URI configURI = NetUtil.getURI(conf); URI remote = toRemoteURI(book); return NetUtil.isNewer(remote, configURI, proxyHost, proxyPort); } /* (non-Javadoc) * @see org.crosswire.jsword.book.BookList#getBooks() */ public List<Book> getBooks() { try { if (!loaded) { loadCachedIndex(); } // We need to create a List from the Set returned by // entries.values() so the underlying list is not modified. return new ArrayList<Book>(entries.values()); } catch (InstallException ex) { log.error("Failed to reload cached index file", ex); return new ArrayList<Book>(); } } /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#getBook(java.lang.String) */ public Book getBook(final String name) { List<Book> books = null; synchronized (this) { books = getBooks(); } // Check name first // First check for exact matches for (Book book : books) { if (name.equals(book.getName())) { return book; } } // Next check for case-insensitive matches for (Book book : books) { if (name.equalsIgnoreCase(book.getName())) { return book; } } // Then check initials // First check for exact matches for (Book book : books) { BookMetaData bmd = book.getBookMetaData(); if (name.equals(bmd.getInitials())) { return book; } } // Next check for case-insensitive matches for (Book book : books) { if (name.equalsIgnoreCase(book.getInitials())) { return book; } } return null; } /* (non-Javadoc) * @see org.crosswire.jsword.book.basic.AbstractBookList#getBooks(org.crosswire.jsword.book.BookFilter) */ @Override public List<Book> getBooks(BookFilter filter) { List<Book> books = null; synchronized (this) { books = getBooks(); } List<Book> temp = CollectionUtil.createList(new BookFilterIterator(books, filter)); return new BookSet(temp); } /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#install(org.crosswire.jsword.book.Book) */ public void install(final Book book) { // // Is the book already installed? Then nothing to do. // if (Books.installed().getBook(book.getName()) != null) // { // return; // } // final SwordBookMetaData sbmd = (SwordBookMetaData) book.getBookMetaData(); // TRANSLATOR: Progress label indicating the installation of a book. {0} is a placeholder for the name of the book. String jobName = JSMsg.gettext("Installing book: {0}", sbmd.getName()); Progress job = JobManager.createJob(String.format(Progress.INSTALL_BOOK, book.getInitials()), jobName, Thread.currentThread()); URI temp = null; try { // Don't bother setting a size, we'll do it later. job.beginJob(jobName); Thread.yield(); // TRANSLATOR: Progress label indicating the Initialization of installing of a book. job.setSectionName(JSMsg.gettext("Initializing")); temp = NetUtil.getTemporaryURI("swd", ZIP_SUFFIX); download(job, packageDirectory, sbmd.getInitials() + ZIP_SUFFIX, temp); // Once the unzipping is started, we need to continue job.setCancelable(false); if (!job.isFinished()) { File dldir = SwordBookPath.getSwordDownloadDir(); IOUtil.unpackZip(NetUtil.getAsFile(temp), dldir); // TRANSLATOR: Progress label for installing the conf file for a book. job.setSectionName(JSMsg.gettext("Copying config file")); sbmd.setLibrary(NetUtil.getURI(dldir)); SwordBookDriver.registerNewBook(sbmd); } } catch (IOException e) { Reporter.informUser(this, e); job.cancel(); } catch (InstallException e) { Reporter.informUser(this, e); job.cancel(); } catch (BookException e) { Reporter.informUser(this, e); job.cancel(); } finally { job.done(); // tidy up after ourselves // This is a best effort. If for some reason it does not delete now // it will automatically be deleted when the JVM exits normally. if (temp != null) { try { NetUtil.delete(temp); } catch (IOException e) { log.warn("Error deleting temp download file.", e); } } } } /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#reloadBookList() */ public void reloadBookList() throws InstallException { // TRANSLATOR: Progress label for downloading one or more files. String jobName = JSMsg.gettext("Downloading files"); Progress job = JobManager.createJob(Progress.RELOAD_BOOK_LIST, jobName, Thread.currentThread()); job.beginJob(jobName); try { URI scratchfile = getCachedIndexFile(); download(job, catalogDirectory, FILE_LIST_GZ, scratchfile); loaded = false; } catch (InstallException ex) { job.cancel(); throw ex; } finally { job.done(); } } /* (non-Javadoc) * @see org.crosswire.jsword.book.install.Installer#downloadSearchIndex(org.crosswire.jsword.book.Book, java.net.URI) */ public void downloadSearchIndex(Book book, URI localDest) throws InstallException { // TRANSLATOR: Progress label for downloading one or more files. String jobName = JSMsg.gettext("Downloading files"); Progress job = JobManager.createJob(String.format(Progress.DOWNLOAD_SEARCH_INDEX, book.getInitials()), jobName, Thread.currentThread()); job.beginJob(jobName); try { download(job, packageDirectory + '/' + SEARCH_DIR, book.getInitials() + ZIP_SUFFIX, localDest); } catch (InstallException ex) { job.cancel(); throw ex; } finally { job.done(); } } /** * Load the cached index file into memory */ protected void loadCachedIndex() throws InstallException { // We need a sword book driver so the installer can use the driver // name to use in deciding where to put the index. BookDriver fake = SwordBookDriver.instance(); entries.clear(); URI cache = getCachedIndexFile(); if (!NetUtil.isFile(cache)) { reloadBookList(); } InputStream in = null; GzipCompressorInputStream gin = null; TarArchiveInputStream tin = null; try { ConfigEntry.resetStatistics(); in = NetUtil.getInputStream(cache); gin = new GzipCompressorInputStream(in); tin = new TarArchiveInputStream(gin); while (true) { ArchiveEntry entry = tin.getNextEntry(); if (entry == null) { break; } String internal = entry.getName(); if (!entry.isDirectory()) { try { int size = (int) entry.getSize(); // Every now and then an empty entry sneaks in if (size == 0) { log.error("Empty entry: {}", internal); continue; } byte[] buffer = new byte[size]; if (tin.read(buffer) != size) { // This should not happen, but if it does then skip // it. log.error("Did not read all that was expected {}", internal); continue; } if (internal.endsWith(SwordConstants.EXTENSION_CONF)) { internal = internal.substring(0, internal.length() - 5); } else { log.error("Not a SWORD config file: {}", internal); continue; } if (internal.startsWith(SwordConstants.DIR_CONF + '/')) { internal = internal.substring(7); } SwordBookMetaData sbmd = new SwordBookMetaData(buffer, internal); sbmd.setDriver(fake); Book book = new SwordBook(sbmd, new NullBackend()); entries.put(book.getInitials() + book.getName(), book); } catch (IOException ex) { log.error("Failed to load config for entry: {}", internal, ex); } } } loaded = true; ConfigEntry.dumpStatistics(); } catch (IOException ex) { throw new InstallException(JSOtherMsg.lookupText("Error loading from cache"), ex); } finally { IOUtil.close(tin); IOUtil.close(gin); IOUtil.close(in); } } /** remove the cached book list to clear memory */ public void close() { entries.clear(); loaded = false; } /** * @return the catologDirectory */ public String getCatalogDirectory() { return catalogDirectory; } /** * @param catologDirectory * the catologDirectory to set */ public void setCatalogDirectory(String catologDirectory) { this.catalogDirectory = catologDirectory; } /** * @return Returns the directory. */ public String getPackageDirectory() { return packageDirectory; } /** * @param newDirectory * The directory to set. */ public void setPackageDirectory(String newDirectory) { if (packageDirectory == null || !packageDirectory.equals(newDirectory)) { packageDirectory = newDirectory; loaded = false; } } /** * @return the indexDirectory */ public String getIndexDirectory() { return indexDirectory; } /** * @param indexDirectory * the indexDirectory to set */ public void setIndexDirectory(String indexDirectory) { this.indexDirectory = indexDirectory; } /** * @return Returns the host. */ public String getHost() { return host; } /** * @param newHost * The host to set. */ public void setHost(String newHost) { if (host == null || !host.equals(newHost)) { host = newHost; loaded = false; } } /** * @return Returns the proxyHost. */ public String getProxyHost() { return proxyHost; } /** * @param newProxyHost * The proxyHost to set. */ public void setProxyHost(String newProxyHost) { String pHost = null; if (newProxyHost != null && newProxyHost.length() > 0) { pHost = newProxyHost; } if (proxyHost == null || !proxyHost.equals(pHost)) { proxyHost = pHost; loaded = false; } } /** * @return Returns the proxyPort. */ public Integer getProxyPort() { return proxyPort; } /** * @param newProxyPort * The proxyPort to set. */ public void setProxyPort(Integer newProxyPort) { if (proxyPort == null || !proxyPort.equals(newProxyPort)) { proxyPort = newProxyPort; loaded = false; } } /** * The URL for the cached index file for this installer */ protected URI getCachedIndexFile() throws InstallException { try { URI scratchdir = CWProject.instance() .getWriteableProjectSubdir(getTempFileExtension(host, catalogDirectory), true); return NetUtil.lengthenURI(scratchdir, FILE_LIST_GZ); } catch (IOException ex) { throw new InstallException(JSOtherMsg.lookupText("URL manipulation failed"), ex); } } /** * What are we using as a temp filename? */ private static String getTempFileExtension(String host, String catalogDir) { return DOWNLOAD_PREFIX + host + catalogDir.replace('/', '_'); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object object) { if (!(object instanceof AbstractSwordInstaller)) { return false; } AbstractSwordInstaller that = (AbstractSwordInstaller) object; if (!equals(this.host, that.host)) { return false; } if (!equals(this.packageDirectory, that.packageDirectory)) { return false; } return true; } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(AbstractSwordInstaller myClass) { int ret = host.compareTo(myClass.host); if (ret != 0) { ret = packageDirectory.compareTo(myClass.packageDirectory); } return ret; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return host.hashCode() + packageDirectory.hashCode(); } /** * Quick utility to check to see if 2 (potentially null) strings are equal */ protected boolean equals(String string1, String string2) { if (string1 == null) { return string2 == null; } return string1.equals(string2); } /** * A map of the books in this download area */ protected Map<String, Book> entries = new HashMap<String, Book>(); /** * The remote hostname. */ protected String host; /** * The remote proxy hostname. */ protected String proxyHost; /** * The remote proxy port. */ protected Integer proxyPort; /** * The directory containing zipped books on the <code>host</code>. */ protected String packageDirectory = ""; /** * The directory containing the catalog of all books on the * <code>host</code>. */ protected String catalogDirectory = ""; /** * The directory containing the catalog of all books on the * <code>host</code>. */ protected String indexDirectory = ""; /** * Do we need to reload the index file */ protected boolean loaded; /** * The sword index file */ protected static final String FILE_LIST_GZ = "mods.d.tar.gz"; /** * The suffix of zip books on this server */ protected static final String ZIP_SUFFIX = ".zip"; /** * The relative path of the dir holding the search index files */ protected static final String SEARCH_DIR = "search/jsword/L1"; /** * When we cache a download index */ protected static final String DOWNLOAD_PREFIX = "download-"; /** * The log stream */ protected static final Logger log = LoggerFactory.getLogger(AbstractSwordInstaller.class); }