edu.udo.scaffoldhunter.model.dataimport.Importer.java Source code

Java tutorial

Introduction

Here is the source code for edu.udo.scaffoldhunter.model.dataimport.Importer.java

Source

/*
 * Scaffold Hunter
 * Copyright (C) 2006-2008 PG504
 * Copyright (C) 2010-2011 PG552
 * See README.txt in the root directory of the Scaffold Hunter source tree
 * for details.
 *
 * Scaffold Hunter is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Scaffold Hunter 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package edu.udo.scaffoldhunter.model.dataimport;

import java.awt.Dialog.ModalityType;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.AbstractListModel;
import javax.swing.JOptionPane;
import javax.swing.ListModel;

import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;

import edu.udo.scaffoldhunter.gui.data.MessageDialog;
import edu.udo.scaffoldhunter.gui.dataimport.ImportDialog;
import edu.udo.scaffoldhunter.gui.dataimport.ImportDialog.Result;
import edu.udo.scaffoldhunter.gui.dataimport.ImportMappingsDialog;
import edu.udo.scaffoldhunter.gui.util.DBExceptionHandler;
import edu.udo.scaffoldhunter.gui.util.DBFunction;
import edu.udo.scaffoldhunter.gui.util.ProgressWorker;
import edu.udo.scaffoldhunter.gui.util.ProgressWorkerUtil;
import edu.udo.scaffoldhunter.gui.util.VoidNullaryDBFunction;
import edu.udo.scaffoldhunter.gui.util.VoidUnaryDBFunction;
import edu.udo.scaffoldhunter.gui.util.WorkerExceptionListener;
import edu.udo.scaffoldhunter.model.data.Message;
import edu.udo.scaffoldhunter.model.data.MessageListener;
import edu.udo.scaffoldhunter.model.datacalculation.Calculator;
import edu.udo.scaffoldhunter.model.dataimport.ImportJob.SourcePropertyMapping;
import edu.udo.scaffoldhunter.model.db.DatabaseException;
import edu.udo.scaffoldhunter.model.db.Dataset;
import edu.udo.scaffoldhunter.model.db.DbManager;
import edu.udo.scaffoldhunter.model.db.Profile;
import edu.udo.scaffoldhunter.model.db.PropertyDefinition;
import edu.udo.scaffoldhunter.model.treegen.ScaffoldTreeGenerator;
import edu.udo.scaffoldhunter.plugins.SHPluginManager;
import edu.udo.scaffoldhunter.plugins.dataimport.ImportPlugin;
import edu.udo.scaffoldhunter.util.I18n;
import edu.udo.scaffoldhunter.util.ProgressAdapter;
import edu.udo.scaffoldhunter.util.ProgressListener;

/**
 * A control class responsible for the construction of a new dataset or for
 * adding properties to an existing one
 * 
 * @author Henning Garus
 * 
 */
public class Importer {

    private static final Logger logger = LoggerFactory.getLogger(Importer.class);

    private final SHPluginManager pluginManager = new SHPluginManager();
    private final ImmutableList<ImportPlugin> plugins;
    private final ImportProcess importProcess;
    private final Set<String> existingDatasetNames;

    private final DbManager db;
    private final Profile profile;

    private List<PropertyDefinition> newPropDefs;

    private Dataset newDataset;
    private boolean finished = false;
    private MergeWorker mergeWorker;

    /**
     * Create a new Importer
     * 
     * @param db
     *            the db manager
     * @param profile
     *            the profile of the user who is importing data
     * @param existingDatasetNames
     *            a list which contains the names of all currently existing
     *            datasets
     * @param dataset
     *            the dataset where data should be merged into or
     *            <code>null</code> if a new dataset should be created.
     * 
     */
    public Importer(DbManager db, Profile profile, List<String> existingDatasetNames, Dataset dataset) {
        this.db = db;
        this.profile = profile;
        Ordering<ImportPlugin> o = new PluginByTitleOrdering();
        plugins = o.immutableSortedCopy(pluginManager.getPlugins());
        Iterable<String> toLowercase = Iterables.transform(existingDatasetNames, new StringToLowerCase());
        this.existingDatasetNames = Sets.newHashSet(toLowercase);

        if (dataset == null)
            importProcess = new ImportProcess(getDefaultDatasetName(), this.existingDatasetNames);
        else
            importProcess = new ImportProcess(dataset);
    }

    /**
     * 
     * @return a list model containing all available plugins.
     */
    public ListModel getPluginListModel() {
        return new AbstractListModel() {

            @Override
            public int getSize() {
                return plugins.size();
            }

            @Override
            public Object getElementAt(int index) {
                return plugins.get(index);
            }
        };
    }

    /**
     * start the import process
     * 
     * @param owner
     *            the owner of the dialogs which will be shown during import
     * @return the new dataset (or null if the user canceled creation)
     */
    public Dataset runImport(Window owner) {

        ImportMappingsDialog mappingsDialog;
        ImportDialog importDialog = new ImportDialog(owner, getPluginListModel(), importProcess);

        /*
         * The import process consists (currently) of three dialogs:
         * 
         * 1. ImportDialog: Import jobs are created
         * 
         * 2. ImportMappingsDialog: Imported Properties are mapped to property
         * definitions.
         * 
         * 3. MessageDialog: Shows messages during the actual dataimport.
         * 
         * The first two dialogs each run inside a loop. When cancel is clicked
         * the loop is aborted and the user is returned to the pevious dialog.
         */
        for (;;) {
            if (importProcess.getDataset() == null) {
                newDataset = new Dataset();
                newDataset.setFilterset(null);
                newDataset.setDatabaseIdentifiers("");
                newDataset.setCreationDate(new Date());
                newDataset.setCreatedBy(profile);
            } else {
                newDataset = importProcess.getDataset();
            }
            final Map<String, PropertyDefinition> propertyDefinitions = Maps
                    .newHashMap(newDataset.getPropertyDefinitions());
            importDialog.setVisible(true);
            if (importDialog.getResult() == Result.CANCEL)
                return null;
            assert (importDialog.getResult() == Result.START_IMPORT);
            newDataset.setTitle(importProcess.getDatasetTitle());
            newDataset.setComment(importProcess.getDatasetComment());
            NewDatasetWorker w = new NewDatasetWorker(propertyDefinitions);
            ProgressWorkerUtil.executeWithProgressDialog(importDialog, I18n.get("DataImport.PreparingImport"),
                    I18n.get("DataImport.PreparingJobs"), ModalityType.DOCUMENT_MODAL, w);

            if (!importProcess.isPrepared()) {
                if (w.isNameTaken()) {
                    importDialog.validateInputs();
                    JOptionPane.showMessageDialog(importDialog, I18n.get("DataImport.DatasetNameTaken"),
                            I18n.get("Title.Error"), JOptionPane.ERROR_MESSAGE);
                } else {
                    JOptionPane.showMessageDialog(importDialog, I18n.get("DataImport.DatabaseError"),
                            I18n.get("Title.Error"), JOptionPane.ERROR_MESSAGE);
                }
                continue;
            }
            for (;;) {
                mappingsDialog = new ImportMappingsDialog(owner, importProcess);
                mappingsDialog.setVisible(true);
                if (mappingsDialog.getResult() == ImportMappingsDialog.Result.OK) {

                    saveDatasetAndPropertyDefinitions(newDataset, propertyDefinitions);

                    final MessageDialog messageDialog = new MessageDialog(owner, MessageDialog.IMPORT,
                            new ActionListener() {
                                /*
                                 * Handle cancel events from the MessageDialog
                                 * here otherwise we have the control logic for
                                 * cancel in the dialog, which would be more
                                 * weird than this already is.
                                 */
                                @Override
                                public void actionPerformed(ActionEvent e) {
                                    logger.trace("cancelling import");
                                    // if the worker is finished cancel does
                                    // nothing
                                    mergeWorker.cancel(true);
                                }
                            });
                    mergeWorker = new MergeWorker(messageDialog, importProcess.getJobs(), newDataset,
                            importProcess.getMergedPropeties());
                    mergeWorker.addProgressListener(messageDialog);
                    mergeWorker.addProgressListener(new MergeProgressListener(messageDialog));

                    mergeWorker.addExceptionListener(new WorkerExceptionListener() {
                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * edu.udo.scaffoldhunter.gui.util.WorkerExceptionListener
                         * #exceptionThrown(edu.udo.scaffoldhunter.gui.util.
                         * ProgressWorker, java.lang.Throwable)
                         */
                        @Override
                        public ExceptionHandlerResult exceptionThrown(Throwable e) {
                            if (e instanceof DatabaseException) {
                                DBExceptionHandler.callDBManager(db, new VoidNullaryDBFunction() {

                                    // TODO instead of deleting everything and
                                    // restarting ask the user and use the
                                    // cleanup worker
                                    @Override
                                    public void voidCall() throws DatabaseException {
                                        db.delete(newDataset);
                                    }
                                });
                                // if we got here we still have a database and
                                // can retry the whole import
                                return ExceptionHandlerResult.STOP;
                            } else if (e instanceof RuntimeException) {
                                throw (RuntimeException) e;
                            } else {
                                // apart from DatabaseExceptions only
                                // RuntimeExceptions should end up here.
                                throw new AssertionError(e);
                            }
                        }
                    });
                    mergeWorker.execute();
                    messageDialog.setVisible(true);
                    if (finished)
                        return newDataset;
                    else { // user canceled import

                    }
                } else { // user clicked cancel in propertyMapping dialog
                    if (importProcess.getDataset() == null)
                        DBExceptionHandler.callDBManager(db, new VoidNullaryDBFunction() {

                            @Override
                            public void voidCall() throws DatabaseException {
                                db.delete(newDataset);
                            }
                        }, true);
                    break;
                }
            } // end propertyMappingDialog loop
        } // end ImportDialog loop
    }

    private String getDefaultDatasetName() {
        String ret;
        for (int i = 1; /* return when new name is found */; i++) {
            ret = String.format("%s %d", I18n.get("DataImport.Dataset"), i);
            if (!existingDatasetNames.contains(ret.toLowerCase()))
                return ret;
        }
    }

    private List<PropertyDefinition> saveDatasetAndPropertyDefinitions(Dataset dataset,
            Map<String, PropertyDefinition> propDefs) {
        for (ImportJob j : importProcess.getJobs()) {
            for (Map.Entry<String, SourcePropertyMapping> e : j.getPropertyMappings().entrySet()) {
                if (e.getValue().getPropertyDefiniton() != null) {
                    PropertyDefinition propDef = e.getValue().getPropertyDefiniton();
                    // find unique key if key is not set
                    if (propDef.getKey() == null || propDef.getKey().isEmpty()) {
                        String newKey;
                        for (int i = 0;; i++) {
                            newKey = propDef.getTitle().toUpperCase() + i;

                            /*
                             * prevents key collision with scaffold properties
                             * and calculated properties
                             */
                            if (newKey.startsWith(ScaffoldTreeGenerator.SCAFFOLD_PROPERTY_KEY_PREFIX)
                                    || newKey.startsWith(Calculator.CALC_PLUGINS_PROPERTY_KEY_PREFIX)) {
                                newKey = "_" + newKey;
                            }

                            if (!propDefs.containsKey(newKey))
                                break;
                        }
                        propDef.setKey(newKey);
                    } else {
                        /*
                         * prevents key collision with scaffold properties and
                         * calculated properties
                         */
                        if (propDef.getKey().startsWith(ScaffoldTreeGenerator.SCAFFOLD_PROPERTY_KEY_PREFIX)
                                || propDef.getKey().startsWith(Calculator.CALC_PLUGINS_PROPERTY_KEY_PREFIX)) {
                            propDef.setKey("_" + propDef.getKey());
                        }
                    }
                    propDef.setDataset(dataset);
                    propDefs.put(propDef.getKey(), propDef);
                }
            }
        }

        if (importProcess.getDataset() != null) {
            Predicate<PropertyDefinition> notOld = Predicates.and(Predicates.notNull(),
                    Predicates.not(Predicates.in(importProcess.getDataset().getPropertyDefinitions().values())));
            newPropDefs = Lists.newArrayList(Iterables.filter(propDefs.values(), notOld));
        } else {
            newPropDefs = Lists.newArrayList(propDefs.values());
        }

        dataset.setPropertyDefinitions(propDefs);
        DBExceptionHandler.callDBManager(db, new VoidUnaryDBFunction<Dataset>(dataset) {
            @Override
            public void call(Dataset arg) throws DatabaseException {
                db.saveOrUpdate(newDataset);
                db.saveAllAsNew(newPropDefs);
            }
        });
        return newPropDefs;
    }

    private static class PluginByTitleOrdering extends Ordering<ImportPlugin> implements Serializable {

        @Override
        public int compare(ImportPlugin left, ImportPlugin right) {
            return String.CASE_INSENSITIVE_ORDER.compare(left.getTitle(), right.getTitle());
        }

    }

    private class MergeWorker extends ProgressWorker<Void, Message> implements MessageListener {

        private final List<ImportJob> importJobs;
        private final Dataset dataset;
        private final Multimap<ImportJob, PropertyDefinition> mergedProperties;
        private int overallProgress = 0;
        private final MessageDialog messageDialog;

        MergeWorker(MessageDialog dialog, List<ImportJob> jobs, Dataset dataset,
                Multimap<ImportJob, PropertyDefinition> mergedProperties) {
            this.messageDialog = dialog;
            this.importJobs = jobs;
            this.dataset = dataset;
            this.mergedProperties = mergedProperties;
        }

        @Override
        protected Void doInBackground() throws DatabaseException {
            int overallLength = 0;
            for (ImportJob j : importJobs) {
                overallLength += j.getResults().getNumMolecules();
            }
            setProgressBounds(0, overallLength);
            setProgressIndeterminate(false);

            for (ImportJob j : importJobs) {
                j.addMessageListener(this);
                boolean mergeByNumProperty = false;
                if (j.getInternalMergeBy() != null)
                    mergeByNumProperty = !j.getInternalMergeBy().isStringProperty();
                MergeIterator mergeIterator = new MergeIterator(j, dataset, mergedProperties.get(j),
                        j.getSourceMergeBy(), mergeByNumProperty);
                mergeIterator.addMessageListener(this);
                mergeIterator.addProgressListener(new ProgressListener<Void>() {

                    @Override
                    public void setProgressValue(int progress) {
                        MergeWorker.this.setProgressValue(overallProgress + progress);
                    }

                    @Override
                    public void setProgressIndeterminate(boolean indeterminate) {
                    }

                    @Override
                    public void setProgressBounds(int min, int max) {
                    }

                    @Override
                    public void finished(Void v, boolean cancelled) {
                    }
                });
                if (j.getInternalMergeBy() == null) {
                    db.mergeMoleculesIntoDBbySMILES(mergeIterator);
                } else {
                    db.mergeMoleculesIntoDBbyProperty(mergeIterator, j.getInternalMergeBy(), dataset);
                }
                if (Thread.interrupted()) {
                    return null;
                }
                overallProgress += j.getResults().getNumMolecules();
                j.removeMessageListener(this);
            }
            return null;
        }

        @Override
        public void receiveMessage(Message message) {
            publish(message);
        }

        @Override
        protected void process(List<Message> chunks) {
            super.process(chunks);
            if (!isCancelled()) {
                for (Message m : chunks)
                    messageDialog.addMessage(m);
            }
        }

    }

    private class MergeProgressListener extends ProgressAdapter<Void> {

        private final MessageDialog messageDialog;
        private boolean canceled = false;

        private MergeProgressListener(MessageDialog messageDialog) {
            super();
            this.messageDialog = messageDialog;
        }

        @Override
        public void finished(Void result, boolean canceled) {
            if (canceled) {
                this.canceled = true;
                logger.trace("creating cleanup worker");
                // on cancel delete all imported properties
                CleanupWorker cleanup = new CleanupWorker();
                cleanup.addProgressListener(new ProgressAdapter<Void>() {
                    @Override
                    public void finished(Void result, boolean cancelled) {
                        messageDialog.dispose();
                    }
                });
                messageDialog.setText(I18n.get("DataImport.ImportCanceled"));
                messageDialog.setProgressIndeterminate(true);
                cleanup.execute();
                // TODO handle exceptions thrown during cleanup
            } else {
                messageDialog.setText(I18n.get("DataImport.ImportFinished"));
                finished = true;
            }
        }

        @Override
        public void setProgressValue(int progress) {
            if (canceled)
                return;
            int overallProgress = 0;
            int i = 1;
            // determine the current job
            for (ImportJob j : importProcess.getJobs()) {
                int jobProgress = overallProgress + j.getResults().getNumMolecules();
                // just having \n working as expected would be to easy...
                // let's center this stuff, while we're at it
                if (progress < jobProgress) {
                    messageDialog
                            .setText("<html><div align=\"center\">"
                                    + I18n.get("DataImport.RunningNthJob", i, importProcess.getJobs().size(), j)
                                    + "<br>" + I18n.get("DataImport.ImportingNthMolecule",
                                            progress + 1 - overallProgress, j.getResults().getNumMolecules())
                                    + "</div></html>");
                    break;
                }
                i++;
                overallProgress = jobProgress;
            }
        }
    }

    private class CleanupWorker extends ProgressWorker<Void, Void> {

        @Override
        protected Void doInBackground() throws Exception {
            logger.trace("starting cleanup");
            for (PropertyDefinition p : newPropDefs) {
                newDataset.getPropertyDefinitions().remove(p.getKey());
            }
            DBExceptionHandler.callDBManager(db, new VoidNullaryDBFunction() {

                @Override
                public void voidCall() throws DatabaseException {
                    db.saveOrUpdate(newDataset);
                    db.deleteAll(newPropDefs);
                }
            });
            return null;
        }
    }

    private static class StringToLowerCase implements Function<String, String> {
        @Override
        public String apply(String input) {
            return input.toLowerCase();
        }
    }

    private class NewDatasetWorker extends ProgressWorker<Void, Void> {
        private boolean nameTaken = false;

        private final Map<String, PropertyDefinition> propertyDefinitions;

        NewDatasetWorker(Map<String, PropertyDefinition> propertyDefinitions) {
            this.propertyDefinitions = propertyDefinitions;
        }

        @Override
        protected Void doInBackground() throws Exception {
            if (importProcess.getDataset() == null) {
                try {
                    db.saveAsNew(newDataset);
                } catch (DatabaseException e) {
                    if (e.getCause() instanceof ConstraintViolationException) {
                        List<String> datasetNames = DBExceptionHandler.callDBManager(db,
                                new DBFunction<List<String>>() {
                                    @Override
                                    public List<String> call() throws DatabaseException {
                                        return db.getAllDatasetNames();
                                    }
                                });
                        Iterables.addAll(importProcess.getExistingDatasets(),
                                Iterables.transform(datasetNames, new StringToLowerCase()));
                        nameTaken = true;
                        return null;
                    } else {
                        return null;
                    }
                }
            }
            importProcess.prepareImport(propertyDefinitions);
            return null;
        }

        /**
         * @return if the dataset name is taken
         */
        public boolean isNameTaken() {
            return nameTaken;
        }
    }
}