NewStarFromObSourcePluginTask.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2009  AAVSO (http://www.aavso.org/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package org.aavso.tools.vstar.ui.task;

import java.awt.Cursor;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javax.swing.SwingWorker;

import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.exception.AuthenticationError;
import org.aavso.tools.vstar.exception.CancellationException;
import org.aavso.tools.vstar.exception.ConnectionException;
import org.aavso.tools.vstar.input.AbstractObservationRetriever;
import org.aavso.tools.vstar.input.database.Authenticator;
import org.aavso.tools.vstar.plugin.InputType;
import org.aavso.tools.vstar.plugin.ObservationSourcePluginBase;
import org.aavso.tools.vstar.plugin.PluginComponentFactory;
import org.aavso.tools.vstar.ui.dialog.AdditiveLoadFileOrUrlChooser;
import org.aavso.tools.vstar.ui.dialog.Checkbox;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.MultiEntryComponentDialog;
import org.aavso.tools.vstar.ui.dialog.TextField;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.mediator.message.ProgressInfo;
import org.aavso.tools.vstar.ui.mediator.message.ProgressType;
import org.aavso.tools.vstar.ui.resources.ResourceAccessor;
import org.aavso.tools.vstar.util.plugin.URLAuthenticator;

/**
 * A concurrent task in which a new star from observation source plug-in request
 * is handled.
 */
public class NewStarFromObSourcePluginTask extends SwingWorker<Void, Void> {

    private Mediator mediator = Mediator.getInstance();

    private ObservationSourcePluginBase obSourcePlugin;

    private List<InputStream> streams;

    private AbstractObservationRetriever retriever;

    private int obsCount;

    private boolean cancelled;

    /**
     * Constructor.
     * 
     * @param obSourcePlugin The plugin that will be used to obtain observations.
     */
    public NewStarFromObSourcePluginTask(ObservationSourcePluginBase obSourcePlugin) {
        this.obSourcePlugin = obSourcePlugin;
        obsCount = 0;
        cancelled = false;
    }

    /**
     * Configure the plug-in for observation retrieval.
     */
    public void configure() {
        try {
            if (obSourcePlugin.requiresAuthentication()) {
                Mediator.getUI().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

                Authenticator.getInstance().authenticate();

                if (!obSourcePlugin.additionalAuthenticationSatisfied(ResourceAccessor.getLoginInfo())) {
                    throw new AuthenticationError("Plug-in authentication failed");
                }
            }

            // Set input streams and name, if requested by the plug-in.
            streams = new ArrayList<InputStream>();

            switch (obSourcePlugin.getInputType()) {
            case FILE:
            case FILE_OR_URL:
                // Does the plug-in supply files? Or do we have to ask the user
                // for a file?
                List<File> files = obSourcePlugin.getFiles();
                if (files != null) {
                    String fileNames = "";
                    obSourcePlugin.clearStreamNameMap();
                    for (File file : files) {
                        InputStream stream = new FileInputStream(file);
                        obSourcePlugin.addStreamNamePair(stream, file.getName());
                        streams.add(stream);
                        fileNames += file.getName() + ", ";
                    }
                    fileNames = fileNames.substring(0, fileNames.lastIndexOf(", "));
                    obSourcePlugin.setInputInfo(streams, fileNames);
                } else {
                    // Request a file or URL from the user.
                    AdditiveLoadFileOrUrlChooser fileChooser = PluginComponentFactory.chooseFileForReading(
                            obSourcePlugin.getDisplayName(), obSourcePlugin.getAdditionalFileExtensions(),
                            obSourcePlugin.getInputType() == InputType.FILE_OR_URL,
                            true,
                            obSourcePlugin.isMultipleFileSelectionAllowed());

                    if (fileChooser != null) {
                        // Which plugin was selected in the end?
                        // We only ask this for plugins that can share the
                        // common file/URL dialog approach (with a list of
                        // plugins). There will only be a non-empty optional
                        // value if all observation source plugins
                        // have been not been requested to be shown in the file
                        // menu.
                        if (obSourcePlugin.getInputType() == InputType.FILE_OR_URL) {
                            Optional<ObservationSourcePluginBase> selectedPlugin = fileChooser.getSelectedPlugin();
                            if (selectedPlugin.isPresent()) {
                                obSourcePlugin = selectedPlugin.get();
                            }
                        }

                        // If a file was chosen or a URL obtained, use as input.
                        obSourcePlugin.setAdditive(fileChooser.isLoadAdditive());

                        if (fileChooser.isUrlProvided()) {
                            // User-supplied URL source
                            String urlStr = fileChooser.getUrlString();
                            URL url = new URL(urlStr);
                            InputStream stream = url.openConnection().getInputStream();
                            streams.add(stream);
                            obSourcePlugin.setInputInfo(streams, urlStr);
                            obSourcePlugin.clearStreamNameMap();
                            obSourcePlugin.addStreamNamePair(stream, urlStr);
                        } else if (fileChooser.isObsTextProvided()) {
                            // Text source (from text input dialog)
                            String obsTextStr = fileChooser.getObsTextString();
                            InputStream stream = new ByteArrayInputStream(obsTextStr.toString().getBytes());
                            streams.add(stream);
                            obSourcePlugin.setInputInfo(streams, "Observation Text");
                            obSourcePlugin.clearStreamNameMap();
                            obSourcePlugin.addStreamNamePair(stream, obsTextStr);
                        } else {
                            // One or more selected files
                            File[] selectedFiles = fileChooser.getSelectedFiles();
                            if (selectedFiles.length != 0) {
                                String fileNames = "";
                                obSourcePlugin.clearStreamNameMap();
                                for (File file : selectedFiles) {
                                    InputStream stream = new FileInputStream(file);
                                    streams.add(stream);
                                    obSourcePlugin.addStreamNamePair(stream, file.getName());
                                    fileNames += file.getName() + ", ";
                                }
                                fileNames = fileNames.substring(0, fileNames.lastIndexOf(", "));
                                obSourcePlugin.setInputInfo(streams, fileNames);
                            } else {
                                throw new CancellationException();
                            }
                        }

                        obSourcePlugin.setVelaFilterStr(fileChooser.getVeLaFilter());
                    } else {
                        throw new CancellationException();
                    }
                }
                break;

            case URL:
                // If the plug-in specifies a basic auth http username and
                // password, create and set an authenticator.
                String userName = obSourcePlugin.getUsername();
                String password = obSourcePlugin.getPassword();

                if (userName != null && password != null) {
                    java.net.Authenticator.setDefault(new URLAuthenticator(userName, password));
                }

                // Does the plug-in supply URLs? Or do we have to ask the user
                // for a URL?
                List<URL> urls = obSourcePlugin.getURLs();
                if (urls != null) {
                    String urlStrs = "";
                    obSourcePlugin.clearStreamNameMap();
                    for (URL url : urls) {
                        InputStream stream = url.openStream();
                        streams.add(stream);
                        obSourcePlugin.addStreamNamePair(stream, url.getPath());
                        urlStrs += url.getPath() + ", ";
                    }
                    urlStrs = urlStrs.substring(0, urlStrs.lastIndexOf(", "));
                    obSourcePlugin.setInputInfo(streams, urlStrs);
                } else {
                    // Request a URL from the user.
                    TextField urlField = new TextField("URL");
                    TextField velaFilterField = new TextField("VeLa Filter");
                    Checkbox additiveLoadCheckbox = new Checkbox("Add to current?", false);
                    MultiEntryComponentDialog urlDialog = new MultiEntryComponentDialog("Enter URL", urlField,
                            additiveLoadCheckbox);
                    if (!urlDialog.isCancelled() && !urlField.getValue().matches("^\\s*$")) {
                        String urlStr = urlField.getValue();
                        obSourcePlugin.setVelaFilterStr(velaFilterField.getValue());
                        obSourcePlugin.setAdditive(additiveLoadCheckbox.getValue());
                        URL url = new URL(urlStr);
                        InputStream stream = url.openStream();
                        streams.add(stream);
                        obSourcePlugin.setInputInfo(streams, urlStr);
                        obSourcePlugin.clearStreamNameMap();
                        obSourcePlugin.addStreamNamePair(stream, urlStr);
                    } else {
                        throw new CancellationException();
                    }
                }

                break;

            case NONE:
                obSourcePlugin.setInputInfo(null, null);
                break;
            }

            // Retrieve the observations. If the retriever can return
            // the number of records, we can show updated progress,
            // otherwise just show busy state.
            retriever = obSourcePlugin.getObservationRetriever();

            // #PMAK#20211229#1#:
            // if the retriever has configuration dialog (see, for example, ASAS-SN plug-in)
            // that was canceled, getObservationRetriever() returns null.
            // It is essentially the same as CancellationException
            // that cannot be thrown from within getObservationRetriever()
            if (retriever == null)
                cancelled = true;
        } catch (CancellationException ex) {
            cancelled = true;
        } catch (ConnectionException ex) {
            MessageBox.showErrorDialog("Authentication Source Error", ex.getLocalizedMessage());
        } catch (AuthenticationError ex) {
            MessageBox.showErrorDialog("Authentication Error", ex.getLocalizedMessage());
        } catch (Exception ex) {
            MessageBox.showErrorDialog("Observation Source Error", ex.getLocalizedMessage());
        } finally {
            // #PMAK#20201121#1#: close input streams if no retriever returned.
            if (retriever == null)
                try {
                    closeStreams();
                } catch (IOException ex) {
                    // we unlikely be here
                    MessageBox.showErrorDialog("Observation Source Error", ex.getLocalizedMessage());
                }
            Mediator.getUI().setCursor(null);
        }
    }

    public boolean isConfigured() {
        return retriever != null;
    }

    /**
     * Main task. Executed in background thread.
     */
    public Void doInBackground() {
        try {

            createObservationArtefacts();

        } catch (Exception ex) {
            // Experience shows that if we get to this point, a different
            // exception has already been caught and reported in the event
            // thread. There is no point in reporting a null exception!
            if (ex.getLocalizedMessage() != null) {
                MessageBox.showErrorDialog("Observation Source Error", ex.getLocalizedMessage());
            }
        } finally {
            Mediator.getUI().setCursor(null);
        }

        return null;
    }

    /**
     * Create observation table and plot models from an observation source plug-in.
     */
    protected void createObservationArtefacts() {
        try {
            int plotPortion = 0;
            Integer numRecords = retriever.getNumberOfRecords();
            if (numRecords == null) {
                // Show busy.
                mediator.getProgressNotifier().notifyListeners(ProgressInfo.BUSY_PROGRESS);
            } else {
                // Start progress tracking.
                plotPortion = (int) (numRecords * 0.2);

                mediator.getProgressNotifier()
                        .notifyListeners(new ProgressInfo(ProgressType.MAX_PROGRESS, numRecords + plotPortion));

                mediator.getProgressNotifier().notifyListeners(ProgressInfo.START_PROGRESS);
            }

            // Note: A reset may cause problems if isAdditive() is true, so make
            // conditional.
            if (!obSourcePlugin.isAdditive()) {
                ValidObservation.reset();
            }

            try {
                retriever.retrieveObservations();

                if (retriever.getValidObservations().isEmpty()) {
                    String msg = "No observations found.";
                    MessageBox.showErrorDialog("Observation Read Error", msg);
                } else {
                    // Create plots, tables.
                    mediator.createNewStarObservationArtefacts(obSourcePlugin.getNewStarType(), retriever.getStarInfo(),
                            plotPortion, obSourcePlugin.isAdditive());

                    obsCount = retriever.getValidObservations().size();
                }
            } finally {
                closeStreams();
            }
        } catch (InterruptedException e) {
            ValidObservation.restore();
            done();
        } catch (Throwable t) {
            ValidObservation.restore();
            done();
            MessageBox.showErrorDialog("Observation Source Read Error", t.getLocalizedMessage());
        }
    }

    /**
     * Executed in event dispatching thread.
     */
    public void done() {
        if (cancelled || obsCount != 0
                || (obSourcePlugin.isAdditive() && !mediator.getNewStarMessageList().isEmpty())) {
            // Either there were observations loaded or this was a failed
            // additive load and there exist previously loaded observations, so
            // we want to complete progress. We also want to complete progress
            // if the dialog is cancelled. Doing so ensures menu and tool bar
            // icons have their state restored. Question: why not just do this
            // unconditionally then?
            mediator.getProgressNotifier().notifyListeners(ProgressInfo.COMPLETE_PROGRESS);
        }

        mediator.getProgressNotifier().notifyListeners(ProgressInfo.CLEAR_PROGRESS);
    }

    private void closeStreams() throws IOException {
        // Close all streams
        for (InputStream stream : streams) {
            stream.close();
        }
    }
}