/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* 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 com.rapidminer.studio.io.gui.internal.steps;
import java.awt.CardLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.file.Path;
import java.util.concurrent.ExecutionException;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.rapidminer.core.io.data.source.FileDataSource;
import com.rapidminer.core.io.gui.ImportWizard;
import com.rapidminer.core.io.gui.InvalidConfigurationException;
import com.rapidminer.core.io.gui.WizardDirection;
import com.rapidminer.core.io.gui.WizardStep;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.tools.ProgressThread;
import com.rapidminer.gui.tools.ProgressThreadDialog;
import com.rapidminer.gui.tools.ProgressThreadListener;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.SwingTools.ResultRunnable;
import com.rapidminer.gui.tools.dialogs.ConfirmDialog;
import com.rapidminer.repository.Entry;
import com.rapidminer.repository.Folder;
import com.rapidminer.repository.MalformedRepositoryLocationException;
import com.rapidminer.repository.RepositoryException;
import com.rapidminer.repository.RepositoryLocation;
import com.rapidminer.repository.gui.RepositoryLocationChooser;
import com.rapidminer.repository.gui.RepositoryTree;
import com.rapidminer.tools.I18N;
/**
* Abstract {@link WizardStep} that allows to select a repository location as destination of the
* import. Triggers the actual import on success.
*
* @author Michael Knopf, Gisa Schaefer, Marcel Michel
* @since 7.0.0
*
*/
public abstract class AbstractToRepositoryStep<T extends RepositoryLocationChooser> extends AbstractWizardStep {
/** Template for the text below the {@link #animationLabel} */
private static final String IMPORTING_TEXT_TEMPLATE = "<html><center><span style=\"font-size: 14\">"
+ I18N.getGUILabel("io.dataimport.step.store_data_to_repository.label") + "<br>%s</span></center></html>";
/**
* SwingWorker which periodically checks if the background job finished.
*/
private class ProgressUpdater extends SwingWorker<Void, Void> {
private static final int IDLE_TIME_MS = 500;
@Override
protected Void doInBackground() throws Exception {
while (backgroundJob != null || confirmDialog != null) {
try {
// we've finished the background job, close the obsolete
// confirm dialog
if (backgroundJob == null && confirmDialog != null) {
confirmDialog.dispose();
}
Thread.sleep(IDLE_TIME_MS);
} catch (InterruptedException e) {
// ignore this
}
}
return null;
}
}
/** IDs for the {@link CardLayout} */
private static final String CARD_ID_CHOOSER = "chooser";
private static final String CARD_ID_PROGRESS = "progress";
/** Change Listener which is registered to the {@link #chooser} */
private final ChangeListener changeListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
AbstractToRepositoryStep.this.fireStateChanged();
}
};
/** {@link ResultRunnable} for the {@link #confirmDialog} */
private final ResultRunnable<Integer> confirmResultRunnable = new ResultRunnable<Integer>() {
@Override
public Integer run() {
confirmDialog = new ConfirmDialog(wizard.getDialog(), "cancel_import", ConfirmDialog.YES_NO_OPTION, false);
confirmDialog.setVisible(true);
return confirmDialog.getReturnOption();
}
};
/** In case of a dialog, this listener will be used to control the close event */
private final WindowAdapter closeListener = new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
if (backgroundJob != null) {
if (SwingTools.invokeAndWaitWithResult(confirmResultRunnable) != ConfirmDialog.YES_OPTION) {
closeDialog = false;
} else {
closeDialog = true;
}
confirmDialog = null;
if (closeDialog && backgroundJob != null) {
stopButton.doClick();
}
}
}
};
/** default close operation of the import wizard dialog, may be -1 */
private final int defaultCloseOperation;
/** The chooser for the repository location. */
private T chooser = null;
/** The main panel contains two cards */
private JPanel mainPanel;
/** The active confirmation dialog */
private ConfirmDialog confirmDialog;
/** The import job as {@link ProgressThread} */
private ProgressThread backgroundJob;
/** Flag which is set to {@code true} if the {@link #backgroundJob} is cancelled */
private boolean isImportCancelled;
/** the label showing the data storing animation and the store location */
private JLabel animationLabel;
/** the button to stop the data storing */
private JButton stopButton;
/** Flag which is set to {@code true} if the user wants to close the dialog */
private boolean closeDialog;
/** Flag used to set the default file name once */
private boolean defaultFileNameInitialized;
/** the import wizard which holds this step */
protected final ImportWizard wizard;
public AbstractToRepositoryStep(final ImportWizard wizard) {
this.wizard = wizard;
JDialog dialog = wizard.getDialog();
if (dialog != null) {
defaultCloseOperation = dialog.getDefaultCloseOperation();
} else {
defaultCloseOperation = -1;
}
}
@Override
public synchronized JComponent getView() {
if (mainPanel == null) {
// If the user has selected a location in the repository browser, use it as initial
// location for the chooser.
String initalLocation = null;
RepositoryTree tree = RapidMinerGUI.getMainFrame().getRepositoryBrowser().getRepositoryTree();
Entry entry = tree.getSelectedEntry();
if (entry != null && !entry.isReadOnly()) {
initalLocation = entry.getLocation().getAbsoluteLocation();
}
// The validity of the step goes hand in hand with the state of the repository location
// chooser. Wrap respective events.
chooser = initializeChooser(initalLocation);
chooser.addChangeListener(changeListener);
mainPanel = new JPanel(new CardLayout());
mainPanel.add(getContentPanel(), CARD_ID_CHOOSER);
mainPanel.add(createProgressPanel(), CARD_ID_PROGRESS);
}
return mainPanel;
}
@Override
public void validate() throws InvalidConfigurationException {
// Check the location (the descriptor itself) is valid.
if (!chooser.isEntryValid()) {
throw new InvalidConfigurationException();
}
}
@Override
public void viewWillBecomeVisible(WizardDirection direction) throws InvalidConfigurationException {
wizard.setProgress(100);
if (!defaultFileNameInitialized) {
defaultFileNameInitialized = true;
// try to get a file location
Path filePath = null;
try {
// if there is a location it comes from a FileDataSource
filePath = wizard.getDataSource(FileDataSource.class).getLocation();
} catch (InvalidConfigurationException e) {
// is not a data source with a location
}
if (filePath != null) {
// extract file name without extension and set it
String fileName = filePath.getFileName().toString();
int separatorLocation = fileName.lastIndexOf('.');
if (separatorLocation > -1) {
fileName = fileName.substring(0, separatorLocation);
}
chooser.setRepositoryEntryName(fileName);
}
}
}
@Override
public void viewWillBecomeInvisible(WizardDirection direction) throws InvalidConfigurationException {
if (direction != WizardDirection.NEXT) {
// This step is the only and last of the custom steps. If the user wants to proceed,
// trigger the actual import. If they don't ignore the event.
return;
}
try {
final RepositoryLocation entryLocation = new RepositoryLocation(chooser.getRepositoryLocation());
final RepositoryLocation folderLocation = entryLocation.parent();
final Entry entry = folderLocation.locateEntry();
if (entry == null || !(entry instanceof Folder)) {
fireStateChanged();
throw new InvalidConfigurationException();
}
final Folder parent = (Folder) entry;
final Entry oldEntry = entryLocation.locateEntry();
// Ask user whether to override existing file (if any).
if (oldEntry != null) {
if (SwingTools.showConfirmDialog(wizard.getDialog(), "overwrite", ConfirmDialog.YES_NO_OPTION,
oldEntry.getName()) == ConfirmDialog.NO_OPTION) {
fireStateChanged();
throw new InvalidConfigurationException();
}
boolean retryDelete = true;
// do not update the wizard during the delete call,
// otherwise the buttons will not be in the correct state
chooser.removeChangeListener(changeListener);
while (retryDelete) {
try {
oldEntry.delete();
retryDelete = false;
} catch (RepositoryException e) {
if (SwingTools.showConfirmDialog(wizard.getDialog(), "error_in_delete_entry",
ConfirmDialog.YES_NO_OPTION) == ConfirmDialog.NO_OPTION) {
fireStateChanged();
chooser.addChangeListener(changeListener);
throw new InvalidConfigurationException();
}
}
}
chooser.addChangeListener(changeListener);
}
closeDialog = false;
isImportCancelled = false;
backgroundJob = getImportThread(entryLocation, parent);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
askBeforeClosing(true);
animationLabel.setText(String.format(IMPORTING_TEXT_TEMPLATE, entryLocation));
stopButton.setEnabled(true);
showCard(CARD_ID_PROGRESS);
// this call ensures that the progress bar runs smoothly
ProgressThreadDialog.getInstance().setVisible(true, false);
}
});
// Import data with background worker.
backgroundJob.addProgressThreadListener(new ProgressThreadListener() {
@Override
public void threadFinished(ProgressThread thread) {
backgroundJob = null;
}
});
SwingWorker<Void, Void> progressUpdater = new ProgressUpdater();
progressUpdater.execute();
backgroundJob.start();
try {
// this call will block the EDT and will continue as soon as
// the backgroundJob is completed
progressUpdater.get();
} catch (InterruptedException | ExecutionException e) {
// do nothing
}
askBeforeClosing(false);
if (closeDialog) {
return;
}
if (!isImportSuccess() || isImportCancelled) {
showCard(CARD_ID_CHOOSER);
throw new InvalidConfigurationException();
}
} catch (MalformedRepositoryLocationException | RepositoryException e) {
// This should already be covered in #validate().
throw new InvalidConfigurationException();
}
}
/**
* If the {@link #wizard} is instance of an {@link JDialog} the user will be asked before
* quitting the dialog.
*
* @param askBeforeClosing
* if {@code true} a confirmation dialog will be shown, otherwise the default
* behavior of the dialog will be used.
*/
private void askBeforeClosing(final boolean askBeforeClosing) {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
JDialog dialog = wizard.getDialog();
if (dialog != null) {
if (askBeforeClosing) {
dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
dialog.addWindowListener(closeListener);
} else {
dialog.setDefaultCloseOperation(defaultCloseOperation);
dialog.removeWindowListener(closeListener);
}
}
}
});
}
@Override
public String getNextStepID() {
return null; // last step
}
/**
* Creates a file chooser that determines where to store data.
*
* @param initialDestination
* the initial location the file chooser should show
* @return a file chooser
*/
protected abstract T initializeChooser(String initialDestination);
/**
* @return a panel that contains the chooser
*/
protected abstract JPanel getContentPanel();
/**
* @return the chooser initialized by {@link #initializeChooser(String)}.
*/
protected T getChooser() {
return chooser;
}
/**
* Indicator if the created {@link ProgressThread} which was created via
* {@link #getImportThread(RepositoryLocation, Folder)} has successfully completed the import.
*
* @return {@code true} if the import successfully completed, otherwise {@code false}
*/
protected abstract boolean isImportSuccess();
/**
* Creates a progress thread that stores the data at the entryLocation.
*
* @param entryLocation
* the location where the data should be stored
* @param parent
* the parent folder of the location
* @return a progress thread that imports the data to the location
* @throws InvalidConfigurationException
* in case the current step is configured wrong
*/
protected abstract ProgressThread getImportThread(final RepositoryLocation entryLocation, final Folder parent)
throws InvalidConfigurationException;
/**
* Shows the card specified by id, e.g. {@link #CARD_ID_CHOOSER} or {@link #CARD_ID_PROGRESS}.
*
* @param cardId
* the id of the card
*/
private void showCard(final String cardId) {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
((CardLayout) mainPanel.getLayout()).show(mainPanel, cardId);
}
});
}
/**
* Creates a panel which displays the process animation and a stop button.
*
* @return the created panel
*/
private JPanel createProgressPanel() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0.0;
gbc.weighty = 0.0;
gbc.insets = new Insets(0, 5, 5, 5);
ImageIcon animation = SwingTools
.createImage(I18N.getGUILabel("io.dataimport.step.store_data_to_repository.animation"));
animationLabel = new JLabel(animation);
animationLabel.setHorizontalTextPosition(JLabel.CENTER);
animationLabel.setVerticalTextPosition(JLabel.BOTTOM);
gbc.gridy += 1;
panel.add(animationLabel, gbc);
stopButton = new JButton(
new ResourceAction(true, "io.dataimport.step.store_data_to_repository.stop_data_import_progress") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
if (backgroundJob != null) {
setEnabled(false);
backgroundJob.cancel();
isImportCancelled = true;
}
}
});
gbc.insets = new Insets(40, 5, 5, 5);
gbc.gridy += 1;
panel.add(stopButton, gbc);
return panel;
}
}