package com.tyndalehouse.step.core.service.jsword.impl;
import com.tyndalehouse.step.core.data.DirectoryListingInstaller;
import com.tyndalehouse.step.core.exceptions.StepInternalException;
import com.tyndalehouse.step.core.exceptions.TranslatedException;
import com.tyndalehouse.step.core.models.BibleInstaller;
import com.tyndalehouse.step.core.service.helpers.VersionResolver;
import com.tyndalehouse.step.core.service.jsword.JSwordModuleService;
import com.tyndalehouse.step.core.service.jsword.JSwordVersificationService;
import com.tyndalehouse.step.core.utils.JSwordUtils;
import com.tyndalehouse.step.core.utils.ValidateUtils;
import org.crosswire.common.progress.JobManager;
import org.crosswire.common.progress.Progress;
import org.crosswire.common.progress.WorkEvent;
import org.crosswire.common.progress.WorkListener;
import org.crosswire.jsword.book.Book;
import org.crosswire.jsword.book.BookCategory;
import org.crosswire.jsword.book.BookFilter;
import org.crosswire.jsword.book.Books;
import org.crosswire.jsword.book.install.InstallException;
import org.crosswire.jsword.book.install.Installer;
import org.crosswire.jsword.index.IndexManager;
import org.crosswire.jsword.index.IndexManagerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import static com.tyndalehouse.step.core.exceptions.UserExceptionType.SERVICE_VALIDATION_ERROR;
import static com.tyndalehouse.step.core.utils.ValidateUtils.notBlank;
import static java.lang.String.format;
/**
* Service to manipulate modules
*
* @author chrisburrell
*/
@Singleton
public class JSwordModuleServiceImpl implements JSwordModuleService {
private static final int INDEX_WAITING = 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(JSwordModuleServiceImpl.class);
private static final String CURRENT_BIBLE_INDEX_JOB = "Creating index. Processing %s";
// BE CAREFUL about using these installers.
private final List<Installer> bookInstallers;
private final List<Installer> offlineInstallers;
private final JSwordVersificationService versificationService;
private final VersionResolver versionResolver;
private boolean offline = false;
/**
* @param installers a list of installers to use to download books
* @param offlineInstallers the set of installers to use offline, rather than online
*/
@Inject
public JSwordModuleServiceImpl(@Named("onlineInstallers") final List<Installer> installers,
@Named("offlineInstallers") final List<Installer> offlineInstallers,
final JSwordVersificationService versificationService,
final VersionResolver versionResolver) {
this.bookInstallers = installers;
this.offlineInstallers = offlineInstallers;
this.versificationService = versificationService;
this.versionResolver = versionResolver;
// add a handler to be notified of all job progresses
JobManager.addWorkListener(new WorkListener() {
@Override
public void workStateChanged(final WorkEvent ev) {
// Never fired - mailed jsword-devel list. so unfortunately need to use below
}
@Override
public void workProgressed(final WorkEvent ev) {
// ignore for now...
final Progress job = ev.getJob();
LOGGER.trace("Work [{}] at [{}] / [{}]", new Object[]{job.getJobName(), job.getTotalWork(),
job.getTotalWork()});
}
});
}
// CHECKSTYLE:OFF
@Override
public List<Installer> getInstallers() {
return this.offline ? this.offlineInstallers : this.bookInstallers;
}
// CHECKSTYLE:ON
@Override
public void setOffline(final boolean offline) {
this.offline = offline;
}
@Override
public boolean isInstalled(final String... modules) {
for (final String moduleInitials : modules) {
if (this.versificationService.getBookSilently(moduleInitials) == null) {
return false;
}
}
return true;
}
@Override
public boolean isIndexed(final String version) {
final IndexManager indexManager = IndexManagerFactory.getIndexManager();
return indexManager.isIndexed(this.versificationService.getBookFromVersion(version));
}
@Override
public void index(final String initials) {
final IndexManager indexManager = IndexManagerFactory.getIndexManager();
final Book book = this.versificationService.getBookFromVersion(initials);
if (!indexManager.isIndexed(book)) {
indexManager.scheduleIndexCreation(book);
}
}
@Override
public void reIndex(final String initials) {
final Book book = this.versificationService.getBookFromVersion(initials);
try {
IndexManagerFactory.getIndexManager().deleteIndex(book);
} catch (final Exception e) {
LOGGER.info("Error deleting index. Attempting to rebuild index all the same");
LOGGER.trace("Error deleting index. Attempting to rebuild index all the same", e);
}
IndexManagerFactory.getIndexManager().scheduleIndexCreation(book);
}
@Override
public void installBook(final int installerIndex, final String initials) {
if (installerIndex == -1) {
installBook(initials);
return;
}
final List<Installer> installers = getInstallers();
final List<Installer> reducedInstallers = new ArrayList<Installer>();
reducedInstallers.add(installers.get(installerIndex));
installFromInstallers(initials, reducedInstallers);
}
@Override
public void installBook(final String initials) {
installFromInstallers(initials, getInstallers());
}
private void installFromInstallers(final String initials, List<Installer> installers) {
LOGGER.debug("Installing module [{}]", initials);
notBlank(initials, "No version was found", SERVICE_VALIDATION_ERROR);
// check if already installed?
if (!isInstalled(initials)) {
LOGGER.debug("Book was not already installed, so kicking off installation process for [{}]",
initials);
for (final Installer i : installers) {
//long initials
String longInitials = this.versionResolver.getLongName(initials);
final Book bookToBeInstalled = i.getBook(longInitials);
if (bookToBeInstalled != null) {
// then we can kick off installation and return
try {
i.install(bookToBeInstalled);
return;
} catch (final InstallException e) {
// we log error here,
LOGGER.error(
"An error occurred error, and we unable to use this installer for module"
+ initials, e
);
// but go round the loop to see if more options are available
continue;
}
}
}
// if we get here, then we were unable to install the book
// since we couldn't find it.
LOGGER.error("Unable to install: [{}]", initials);
throw new TranslatedException("book_not_found", initials);
}
// if we get here then we had already installed the book - how come we're asking for this again?
LOGGER.warn("A request to install an already installed book was made for initials " + initials);
}
@Override
public double getProgressOnInstallation(final String version) {
notBlank(version, "The book name provided was blank", SERVICE_VALIDATION_ERROR);
if (isInstalled(version)) {
return 1;
}
// not yet installed (or at least wasn't on the lines above, so check job list
String longVersionName = this.versionResolver.getLongName(version);
final Iterator<Progress> iterator = JobManager.iterator();
while (iterator.hasNext()) {
final Progress p = iterator.next();
final String expectedJobName = format(Progress.INSTALL_BOOK, longVersionName);
if (expectedJobName.equals(p.getJobID())) {
if (p.isFinished()) {
return 1;
}
return (double) p.getWorkDone() / p.getTotalWork();
}
}
// the job may have completed by now, while we did the search, so do a final check
if (isInstalled(version)) {
return 1;
}
throw new StepInternalException(
"An unknown error has occurred: the job has disappeared of the job list, "
+ "but the module is not installed"
);
}
@Override
public double getProgressOnIndexing(final String bookName) {
notBlank(bookName, "The book name to be indexed was blank", SERVICE_VALIDATION_ERROR);
if (isIndexed(bookName)) {
return 1;
}
// not yet installed (or at least wasn't on the lines above, so check job list
String longVersionName = this.versionResolver.getLongName(bookName);
final Iterator<Progress> iterator = JobManager.iterator();
while (iterator.hasNext()) {
final Progress p = iterator.next();
final String expectedJobName = format(CURRENT_BIBLE_INDEX_JOB, longVersionName);
if (expectedJobName.equals(p.getJobName())) {
if (p.isFinished()) {
return 1;
}
return (double) p.getWork() / p.getTotalWork();
}
}
// the job may have completed by now, while we did the search, so do a final check
if (isIndexed(bookName)) {
return 1;
}
throw new StepInternalException(
"An unknown error has occurred: the job has disappeared of the job list, "
+ "but the module is not installed"
);
}
@Override
public void reloadInstallers() {
boolean errors = false;
LOGGER.trace("About to reload installers");
final List<Installer> installers = getInstallers();
for (final Installer i : installers) {
try {
LOGGER.trace("Reloading installer [{}]", i.getInstallerDefinition());
i.reloadBookList();
} catch (final InstallException e) {
errors = true;
LOGGER.error(e.getMessage(), e);
}
}
if (errors) {
throw new StepInternalException(
"Errors occurred while trying to retrieve the latest installer information");
}
}
@Override
public List<Book> getInstalledModules(final boolean allVersions, final String language,
final BookCategory... bibleCategory) {
if (!allVersions) {
ValidateUtils.notNull(language, "Locale was not passed by requester", SERVICE_VALIDATION_ERROR);
}
// TODO : TOTOTOTOTOTOTOTO
final String tempLang;
if ("eng".equals(language)) {
tempLang = "en";
} else {
tempLang = language;
}
if (bibleCategory == null || bibleCategory.length == 0) {
return new ArrayList<Book>();
}
// quickly transform the categories to a set for fast comparison
final Set<BookCategory> categories = new HashSet<BookCategory>();
for (int ii = 0; ii < bibleCategory.length; ii++) {
categories.add(bibleCategory[ii]);
}
// we set up a filter to retrieve just certain types of books
final BookFilter bf = new BookFilter() {
@Override
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public boolean test(final Book b) {
return categories.contains(b.getBookCategory())
&& (allVersions || isAcceptableVersions(b, tempLang));
}
};
return Books.installed().getBooks(bf);
}
/**
* @param locale the language we are interested in
* @param book the book we are testing
* @return true if we are to accept the book
*/
private boolean isAcceptableVersions(final Book book, final String locale) {
return JSwordUtils.isAncientBook(book) || locale.equals(book.getLanguage().getCode());
}
@Override
public List<Book> getInstalledModules(final BookCategory... bibleCategory) {
return getInstalledModules(true, null, bibleCategory);
}
@Override
public List<Book> getAllModules(int installerIndex, final BookCategory... bibleCategory) {
final List<Book> books = new ArrayList<Book>();
List<Installer> installers = getInstallers();
if (installerIndex != -1) {
//use a single installer
Installer installer = installers.get(installerIndex);
installers = new ArrayList<Installer>();
installers.add(installer);
}
for (final Installer installer : installers) {
try {
installer.reloadBookList();
final List<Book> installerBooks = installer.getBooks();
// iterate through books, doing a linear search for each category, because the list is only 1
// - 4 items bigs, likely 2
for (final Book b : installerBooks) {
for (final BookCategory cat : bibleCategory) {
if (cat.equals(b.getBookCategory())) {
books.add(b);
break;
}
}
}
} catch (final InstallException e) {
// log an error
LOGGER.error("Unable to update installer", e);
}
}
return books;
}
@Override
public void removeModule(final String initials) {
final Book book = this.versificationService.getBookFromVersion(initials);
if (book != null) {
Book deadBook = Books.installed().getBook(book.getInitials());
try {
IndexManagerFactory.getIndexManager().deleteIndex(deadBook);
} catch (Exception e) {
LOGGER.warn("Deleting search index failed: " + initials, e);
}
try {
deadBook.getDriver().delete(deadBook);
} catch (final Exception e) {
// book wasn't found probably
LOGGER.warn("Deleting book failed: " + initials, e);
}
}
}
@Override
public void waitForIndexes(final String... versions) {
for (final String s : versions) {
while (!this.isIndexed(s)) {
try {
Thread.sleep(INDEX_WAITING);
} catch (final InterruptedException e) {
LOGGER.warn("Interrupted exception", e);
}
}
}
}
@Override
public BibleInstaller addDirectoryInstaller(final String directoryPath) {
final DirectoryListingInstaller installer = new DirectoryListingInstaller(directoryPath, directoryPath);
this.bookInstallers.add(installer);
return new BibleInstaller(this.bookInstallers.size() - 1, installer.getInstallerName(), false);
}
}