/*
* Copyright 2009 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.formats.importer.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import loci.formats.MissingLibraryException;
import ome.formats.importer.IObservable;
import ome.formats.importer.IObserver;
import ome.formats.importer.ImportCandidates;
import ome.formats.importer.ImportConfig;
import ome.formats.importer.ImportContainer;
import ome.formats.importer.ImportEvent;
import omero.client;
import omero.api.IQueryPrx;
import omero.api.RawFileStorePrx;
import omero.api.ServiceFactoryPrx;
import omero.model.OriginalFile;
import omero.sys.ParametersI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Top of the error handling hierarchy. Will add errors to a queue
* which can be sent with {@link #sendErrors()}. Subclasses will get
* a chance to handle all {@link ImportEvent} instances, but should
* try not to duplicate handling.
*
* @author Brian W. Loranger
* @author Josh Moore
*
* @since Beta4.1
*/
public abstract class ErrorHandler implements IObserver, IObservable {
/**
* @author Brian W. Loranger
* @author Josh Moore
*/
public abstract static class EXCEPTION_EVENT extends ImportEvent {
public final Exception exception;
/**
* @param exception - set exception
*/
public EXCEPTION_EVENT(Exception exception) {
this.exception = exception;
}
}
/**
* @author Brian W. Loranger
* @author Josh Moore
*/
public static class INTERNAL_EXCEPTION extends EXCEPTION_EVENT {
public final String filename;
public final String[] usedFiles;
public final String reader;
public INTERNAL_EXCEPTION(String filename, Exception exception, String[] usedFiles, String reader) {
super(exception);
this.filename = filename;
this.usedFiles = usedFiles;
this.reader = reader;
}
/* (non-Javadoc)
* @see ome.formats.importer.ImportEvent#toLog()
*/
@Override
public String toLog() {
return String.format("%s: %s\n%s", super.toLog(), filename,
getStackTrace(exception));
}
}
/**
* Unlike {@link FILE_EXCEPTION}, UNKNOWN_FORMAT does not have a reader
* since bio-formats is telling us that it does not know how to handle
* the given file. This should be generally be considered less fatal
* than a {@link FILE_EXCEPTION}, but if the user is specifically saying
* that a file should be imported, and an {@link UNKNOWN_FORMAT} is raised,
* then perhaps there is a configuration issue.
*
* @author Brian W. Loranger
* @author Josh Moore
*/
public static class UNKNOWN_FORMAT extends EXCEPTION_EVENT {
public final String filename;
public final Object source;
/**
* @param filename the filename
* @param exception the exception
* @param source the source (e.g., {@link ImportCandidates})
*/
public UNKNOWN_FORMAT(String filename, Exception exception, Object source) {
super(exception);
this.filename = filename;
this.source = source;
}
/* (non-Javadoc)
* @see ome.formats.importer.ImportEvent#toLog()
*/
@Override
public String toLog() {
return super.toLog() + ": "+filename;
}
}
/**
* Similar to {@link UNKNOWN_FORMAT} UNREADABLE_FILE specifies that the
* file which is being accessed is unreadable (does not exist or canRead
* is false), so if the user is specifically saying that the file should
* be imported, there may be some underlying issue.
*
* @author Brian W. Loranger
* @author Josh Moore
*/
public static class UNREADABLE_FILE extends EXCEPTION_EVENT {
public final String filename;
public final Object source;
/**
* @param filename the filename
* @param exception the exception
* @param source the source
*/
public UNREADABLE_FILE(String filename, Exception exception, Object source) {
super(exception);
this.filename = filename;
this.source = source;
}
/* (non-Javadoc)
* @see ome.formats.importer.ImportEvent#toLog()
*/
@Override
public String toLog() {
return super.toLog() + ": "+filename;
}
}
/**
* {@link FILE_EXCEPTION}s are thrown any time in the context of a particular
* file and otherwise unspecified exception takes place. An example of an
* exception which receives separate handling is {@link UNKNOWN_FORMAT} which
* can be considered less serious than {@link FILE_EXCEPTION}. Subclasses of
* this class may should receive special handling.
* {@link MISSING_LIBRARY} below is probably more of a warn situation rather
* than an error.
*/
public static class FILE_EXCEPTION extends EXCEPTION_EVENT {
public final String filename;
public final String[] usedFiles;
public final String reader;
public FILE_EXCEPTION(String filename, Exception exception, String[] usedFiles, String reader) {
super(exception);
this.filename = filename;
this.usedFiles = usedFiles;
this.reader = reader;
}
@Override
public String toLog() {
return super.toLog() + ": "+filename;
}
}
/**
* A {@link FILE_EXCEPTION} caused specifically by some library (native
* or otherwise) not being installed locally.
*/
public static class MISSING_LIBRARY extends FILE_EXCEPTION {
public MISSING_LIBRARY(String filename, MissingLibraryException exception, String[] usedFiles, String reader) {
super(filename, exception, usedFiles, reader);
}
}
final protected Logger log = LoggerFactory.getLogger(getClass());
final protected List<IObserver> observers = new ArrayList<IObserver>();
final protected List<ErrorContainer> errors = new ArrayList<ErrorContainer>();
final protected ImportConfig config;
protected boolean cancelUploads = false;
protected boolean sendFiles = true;
protected boolean sendLogs = true;
public boolean fileUploadErrors = false;
protected int totalErrors = 0;
// These values are used within the sendErrors loop. They are *very* not
// thread-safe.
private HtmlMessenger messenger;
private FileUploader fileUploader;
private String serverReply;
/** Host information about the file and its corresponding log file.*/
private Map<String, Long> logFiles;
/** Host information about the file and its corresponding import candidate.*/
protected Map<String, ImportContainer> icMap;
/**
* Initialize
*
* @param config the import configuration
*/
public ErrorHandler(ImportConfig config)
{
this.config = config;
logFiles = new HashMap<String, Long>();
icMap = new HashMap<String, ImportContainer>();
}
/* (non-Javadoc)
* @see ome.formats.importer.IObserver#update(ome.formats.importer.IObservable, ome.formats.importer.ImportEvent)
*/
public final void update(IObservable observable, ImportEvent event) {
if (event instanceof MISSING_LIBRARY) {
MISSING_LIBRARY ev = (MISSING_LIBRARY) event;
log.warn(ev.toLog(), ev.exception);
} else if (event instanceof FILE_EXCEPTION) {
FILE_EXCEPTION ev = (FILE_EXCEPTION) event;
log.error(ev.toLog(), ev.exception);
addError(ev.exception, new File(ev.filename), ev.usedFiles, ev.reader);
} else if (event instanceof INTERNAL_EXCEPTION) {
INTERNAL_EXCEPTION ev = (INTERNAL_EXCEPTION) event;
log.error(event.toLog(), ev.exception);
addError(ev.exception, new File(ev.filename), ev.usedFiles, ev.reader);
} else if (event instanceof UNKNOWN_FORMAT) {
UNKNOWN_FORMAT ev = (UNKNOWN_FORMAT) event;
String[] usedFiles = {ev.filename};
// Here it is important to not report errors which
// are coming from ImportCandidates, since that doesn't
// count as an error situation. Previously, this checked
// for (ev.source instanceof ImportLibrary), but that is
// no longer on the compile-time classpath.
if (!(ev.source instanceof ImportCandidates))
addError(ev.exception, new File(ev.filename), usedFiles, "");
log.debug(event.toLog());
} else if (event instanceof EXCEPTION_EVENT) {
EXCEPTION_EVENT ev = (EXCEPTION_EVENT) event;
log.error(ev.toLog(), ev.exception);
} else if (event instanceof ImportEvent.METADATA_IMPORTED) {
ImportEvent.METADATA_IMPORTED e =
(ImportEvent.METADATA_IMPORTED) event;
logFiles.put(e.container.getFile().getAbsolutePath(), e.logFileId);
} else if (event instanceof ImportEvent.POST_UPLOAD_EVENT) {
ImportEvent.POST_UPLOAD_EVENT e =
(ImportEvent.POST_UPLOAD_EVENT) event;
icMap.put(e.container.getFile().getAbsolutePath(), e.container);
}
onUpdate(observable, event);
}
/**
* @return number of errors in ErrorContainer array
*/
public int errorCount()
{
return errors.size();
}
/**
* abstract on update method
*
* @param importLibrary the import library
* @param event - importEvent
*/
protected abstract void onUpdate(IObservable importLibrary, ImportEvent event);
/**
* Retrieve the log file.
*
* @param id The id of the file to load.
* @param session The OMERO session.
* @return See above.
* @throws Throwable Thrown if an error occurred while loading file.
*/
private File retrieveLogFile(Long id, ServiceFactoryPrx session)
throws Throwable
{
if (id == null) return null;
//dowload the file
StringBuffer buf = new StringBuffer();
buf.append("importLog_");
buf.append(id);
File logfile = File.createTempFile(buf.toString(), ".log");
logfile.deleteOnExit();
IQueryPrx svc = session.getQueryService();
ParametersI param = new ParametersI();
param.map.put("id", omero.rtypes.rlong(id));
OriginalFile of = (OriginalFile) svc.findByQuery(
"select p from OriginalFile as p where p.id = :id", param);
if (of == null) return null;
final String path = logfile.getAbsolutePath();
RawFileStorePrx store = null;
try {
store = session.createRawFileStore();
store.setFileId(id);
} catch (Throwable e) {
store.close();
return null; // Never reached.
}
try {
long size = -1;
long offset = 0;
int INC = 262144;
FileOutputStream stream = new FileOutputStream(logfile);
try {
try {
size = store.size();
for (offset = 0; (offset+INC) < size;) {
stream.write(store.read(offset, INC));
offset += INC;
}
} finally {
stream.write(store.read(offset, (int) (size-offset)));
stream.close();
}
} catch (Exception e) {
log.error("Cannot write log file", e);
if (stream != null) stream.close();
}
} catch (IOException e) {
log.error("Cannot write log file", e);
} finally {
store.close();
}
return logfile;
}
/**
* Send existing errors in ErrorContainer array to server
*/
protected void sendErrors() {
//create an omero client.
client sc = null;
client client = null;
ServiceFactoryPrx session = null;
try {
if (sendLogs || sendFiles) {
sc = new client(config.hostname.get(), config.port.get());
ServiceFactoryPrx entryEncrypted;
if (!config.sessionKey.empty()) {
entryEncrypted = sc.joinSession(config.sessionKey.get());
} else {
entryEncrypted = sc.createSession(config.username.get(),
config.password.get());
}
client = sc.createClient(false);
session = client.getSession();
}
for (int i = 0; i < errors.size(); i++) {
if (!isSend(i))
{
onSent(i);
continue; // Don't send file if not selected
}
if (cancelUploads) {
onCancel();
break;
}
ErrorContainer errorContainer = errors.get(i);
if (errorContainer.getStatus() != -1) // if file not pending, skip
// it
continue;
Map<String, String> postList = new HashMap<String, String>();
postList.put("java_version", errorContainer.getJavaVersion());
postList.put("java_classpath", errorContainer.getJavaClasspath());
postList.put("app_version", errorContainer.getAppVersion());
postList.put("comment_type", errorContainer.getCommentType());
postList.put("os_name", errorContainer.getOSName());
postList.put("os_arch", errorContainer.getOSArch());
postList.put("os_version", errorContainer.getOSVersion());
postList.put("extra", errorContainer.getExtra());
postList.put("error", getStackTrace(errorContainer.getError()));
postList.put("comment", errorContainer.getComment());
postList.put("email", errorContainer.getEmail());
postList.put("app_name", "2");
postList.put("import_session", "test");
postList.put("absolute_path", errorContainer.getAbsolutePath() + "/");
String sendUrl = config.getTokenUrl();
if (isSend(i)) {
if (!sendFiles)
{
errorContainer.clearFiles();
}
if (sendLogs || sendFiles) {
File f = errorContainer.getSelectedFile();
if (f != null) {
Long id = logFiles.get(f.getAbsolutePath());
//load the log
File logFile = null;
try {
logFile = retrieveLogFile(id, session);
} catch (Throwable e) {
log.error("Cannot load log file", e);
}
if (logFile != null) {
sendLogs = true;
errorContainer.addFile(logFile.getAbsolutePath());
} else sendLogs = false;
} else sendLogs = false;
}
}
messenger = new HtmlMessenger(sendUrl, postList);
serverReply = messenger.executePost();
if (sendFiles || sendLogs) {
onSending(i);
log.info("Sending File(s)...");
errorContainer.setToken(serverReply);
fileUploader = new FileUploader(
messenger.getCommunicationLink(
config.getUploaderUrl()));
fileUploader.addObserver(this);
fileUploader.uploadFiles(config.getUploaderUrl(), 2000,
errorContainer);
onSent(i);
} else {
onNotSending(i, serverReply);
}
}
} catch (Exception e) {
log.error("Error during upload", e);
} finally {
if (client != null) client.__del__();
if (sc != null) sc.__del__();
}
if (cancelUploads) {
finishCancelled();
}
if (fileUploadErrors) {
finishWithErroredFiles();
notifyObservers(new ImportEvent.ERRORS_COMPLETE());
} else {
finishComplete();
notifyObservers(new ImportEvent.ERRORS_COMPLETE());
}
}
/**
* Add detailed error to error container array
* @param error - error thrown
* @param file - head file for error
* @param files - all files in import collection
* @param readerType - reader type supplied from bio-formats
*/
protected void addError(Throwable error, File file, String[] files,
String readerType) {
ErrorContainer errorContainer = new ErrorContainer();
errorContainer.setFiles(files);
errorContainer.setSelectedFile(file);
errorContainer.setReaderType(readerType);
errorContainer.setCommentType("2");
errorContainer.setJavaVersion(System.getProperty("java.version"));
errorContainer.setJavaClasspath(System.getProperty("java.class.path"));
errorContainer.setOSName(System.getProperty("os.name"));
errorContainer.setOSArch(System.getProperty("os.arch"));
errorContainer.setOSVersion(System.getProperty("os.version"));
errorContainer.setAppVersion(config.getVersionNumber());
errorContainer.setError(error);
addError(errorContainer);
}
/**
* add simple error to error container array
*
* @param errorContainer
*/
private void addError(ErrorContainer errorContainer) {
String errorMessage = errorContainer.getError().toString();
String[] splitMessage = errorMessage.split("\n");
errorMessage = errorMessage.replaceAll("\n", "<br> ");
errorContainer.setIndex(totalErrors);
totalErrors = totalErrors + 1;
errorContainer.setStatus(-1); // pending status
errors.add(errorContainer);
onAddError(errorContainer, splitMessage[0]);
notifyObservers(new ImportEvent.ERRORS_PENDING());
}
protected void clearErrors(int index) {
errors.remove(index);
}
//
// OBSERVER PATTERN
//
/* (non-Javadoc)
* @see ome.formats.importer.IObservable#addObserver(ome.formats.importer.IObserver)
*/
public final boolean addObserver(IObserver object)
{
return observers.add(object);
}
/* (non-Javadoc)
* @see ome.formats.importer.IObservable#deleteObserver(ome.formats.importer.IObserver)
*/
public final boolean deleteObserver(IObserver object)
{
return observers.remove(object);
}
/* (non-Javadoc)
* @see ome.formats.importer.IObservable#notifyObservers(ome.formats.importer.ImportEvent)
*/
public final void notifyObservers(ImportEvent event)
{
for (IObserver observer : observers)
{
observer.update(this, event);
}
}
//
// OVERRIDEABLE METHODS
//
/**
* action to take on cancel
*/
protected void onCancel()
{
}
/**
* Action to take on adding an error to container
*
* @param errorContainer - error container
* @param message - message string for action (if needed)
*/
protected void onAddError(ErrorContainer errorContainer, String message)
{
}
/**
* Check if files need sending at error container index
* @param index - index in error container
* @return - true if file is to be sent
*/
protected boolean isSend(int index)
{
if (errors.get(index).getSelectedFile() == null) {
return false;
}
return true;
}
/**
* @param index the index in the error container
*/
protected void onSending(int index)
{
}
/**
* @param index the index in the error container
*/
protected void onSent(int index)
{
}
/**
* @param index the index in the error container
* @param serverReply the reply from the server
*/
protected void onNotSending(int index, String serverReply)
{
}
/**
* Action to take on exception
* @param exception the exception
*/
protected void onException(Exception exception)
{
notifyObservers(new ImportEvent.ERRORS_FAILED());
}
/**
* Action to take when finish cancelled
*/
protected void finishCancelled()
{
}
/**
* Action to take when finish completed
*/
protected void finishComplete()
{
}
/**
* Action to take when finish completed but with some errors
* (For example, missing files)
*/
protected void finishWithErroredFiles()
{
}
/**
* Execute a post with the given post list. This can be overwritten in order
* to test error handling without touching QA. The server reply should be
* non-null, but is otherwise unimportant.
*
* @param sendUrl the HTTP POST URL
* @param postList the form values
* @throws HtmlMessengerException if POST fails
*/
public void executePost(String sendUrl, Map<String, String> postList)
throws HtmlMessengerException {
messenger = new HtmlMessenger(sendUrl, postList);
serverReply = messenger.executePost();
}
/**
* Upload a single {@link ErrorContainer}. This can be overwritten in order
* to test error handling without touching QA.
*
* @param errorContainer the error container
*/
public void uploadFile(ErrorContainer errorContainer) {
errorContainer.setToken(serverReply);
try {
fileUploader = new FileUploader(messenger.getCommunicationLink(
config.getUploaderUrl()));
fileUploader.addObserver(this);
fileUploader.uploadFiles(config.getUploaderUrl(), 2000, errorContainer);
} catch (Exception e) {
log.error("Error during upload", e);
}
}
/**
* Return the stack trace from a {@link Throwable}.
* @param throwable the {@link Throwable} to inspect
* @return the stack trace
*/
public static String getStackTrace(Throwable throwable)
{
final Writer writer = new StringWriter();
final PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
return writer.toString();
}
}