All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.datacleaner.windows.ResultWindow Maven / Gradle / Ivy

/**
 * DataCleaner (community edition)
 * Copyright (C) 2014 Neopost - Customer Information Management
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.datacleaner.windows;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.inject.Inject;
import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;

import org.apache.commons.vfs2.FileObject;
import org.apache.metamodel.schema.Table;
import org.datacleaner.actions.ExportResultToHtmlActionListener;
import org.datacleaner.actions.SaveAnalysisResultActionListener;
import org.datacleaner.api.AnalyzerResult;
import org.datacleaner.api.ComponentMessage;
import org.datacleaner.api.ExecutionLogMessage;
import org.datacleaner.api.InputRow;
import org.datacleaner.api.RestrictedFunctionalityMessage;
import org.datacleaner.bootstrap.WindowContext;
import org.datacleaner.configuration.DataCleanerConfiguration;
import org.datacleaner.connection.Datastore;
import org.datacleaner.guice.JobFile;
import org.datacleaner.guice.Nullable;
import org.datacleaner.job.AnalysisJob;
import org.datacleaner.job.ComponentJob;
import org.datacleaner.job.ImmutableAnalyzerJob;
import org.datacleaner.job.concurrent.PreviousErrorsExistException;
import org.datacleaner.job.runner.AnalysisJobCancellation;
import org.datacleaner.job.runner.AnalysisJobMetrics;
import org.datacleaner.job.runner.AnalysisListener;
import org.datacleaner.job.runner.AnalysisListenerAdaptor;
import org.datacleaner.job.runner.ComponentMetrics;
import org.datacleaner.job.runner.RowProcessingMetrics;
import org.datacleaner.panels.DCBannerPanel;
import org.datacleaner.panels.DCPanel;
import org.datacleaner.panels.result.AnalyzerResultPanel;
import org.datacleaner.panels.result.ProgressInformationPanel;
import org.datacleaner.result.AnalysisResult;
import org.datacleaner.result.renderer.RendererFactory;
import org.datacleaner.user.UserPreferences;
import org.datacleaner.util.AnalysisRunnerSwingWorker;
import org.datacleaner.util.ErrorUtils;
import org.datacleaner.util.IconUtils;
import org.datacleaner.util.ImageManager;
import org.datacleaner.util.LabelUtils;
import org.datacleaner.util.StringUtils;
import org.datacleaner.util.WidgetFactory;
import org.datacleaner.util.WidgetUtils;
import org.datacleaner.util.WindowSizePreferences;
import org.datacleaner.widgets.DCPersistentSizedPanel;
import org.datacleaner.widgets.PopupButton;
import org.datacleaner.widgets.PopupButton.MenuPosition;
import org.datacleaner.widgets.tabs.Tab;
import org.datacleaner.widgets.tabs.VerticalTabbedPane;
import org.jdesktop.swingx.VerticalLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Window in which the result (and running progress information) of job
 * execution is shown.
 */
public final class ResultWindow extends AbstractWindow implements WindowListener {
    public static final List> PLUGGABLE_BANNER_COMPONENTS = new ArrayList<>(0);
    private static final Logger logger = LoggerFactory.getLogger(ResultWindow.class);
    private static final long serialVersionUID = 1L;
    private static final ImageManager imageManager = ImageManager.get();

    private final VerticalTabbedPane _tabbedPane;
    private final Map> _resultPanels;
    private final AnalysisJob _job;
    private final DataCleanerConfiguration _configuration;
    private final ProgressInformationPanel _progressInformationPanel;
    private final RendererFactory _rendererFactory;
    private final FileObject _jobFilename;
    private final AnalysisRunnerSwingWorker _worker;
    private final UserPreferences _userPreferences;
    private final JButton _cancelButton;
    private final PopupButton _saveResultsPopupButton;
    private final WindowSizePreferences _windowSizePreference;

    private AnalysisResult _result;

    /**
     *
     * @param configuration
     * @param job
     *            either this or result must be available
     * @param result
     *            either this or job must be available
     * @param jobFilename
     * @param windowContext
     * @param userPreferences
     * @param rendererFactory
     */
    @Inject
    protected ResultWindow(final DataCleanerConfiguration configuration, @Nullable final AnalysisJob job,
            @Nullable final AnalysisResult result, @Nullable @JobFile final FileObject jobFilename,
            final WindowContext windowContext, final UserPreferences userPreferences,
            final RendererFactory rendererFactory) {
        super(windowContext);
        final boolean running = (result == null);

        _resultPanels = new IdentityHashMap<>();
        _configuration = configuration;
        _job = job;
        _jobFilename = jobFilename;
        _userPreferences = userPreferences;
        _rendererFactory = rendererFactory;

        final Supplier resultRef = this::getResult;

        final Border buttonBorder =
                new CompoundBorder(WidgetUtils.BORDER_LIST_ITEM_SUBTLE, new EmptyBorder(10, 4, 10, 4));
        _cancelButton = WidgetFactory.createDefaultButton("Cancel job", IconUtils.ACTION_STOP);
        _cancelButton.setHorizontalAlignment(SwingConstants.LEFT);
        _cancelButton.setBorder(buttonBorder);

        _saveResultsPopupButton = WidgetFactory.createDefaultPopupButton("Save results", IconUtils.ACTION_SAVE_DARK);
        _saveResultsPopupButton.setHorizontalAlignment(SwingConstants.LEFT);
        _saveResultsPopupButton.setBorder(buttonBorder);
        _saveResultsPopupButton.setMenuPosition(MenuPosition.TOP);
        _saveResultsPopupButton.getMenu().setBorder(new MatteBorder(1, 0, 0, 1, WidgetUtils.BG_COLOR_MEDIUM));

        final JMenuItem saveAsFileItem =
                WidgetFactory.createMenuItem("Save as result file", IconUtils.ACTION_SAVE_DARK);
        saveAsFileItem.addActionListener(new SaveAnalysisResultActionListener(resultRef, _userPreferences));
        saveAsFileItem.setBorder(buttonBorder);
        _saveResultsPopupButton.getMenu().add(saveAsFileItem);

        final JMenuItem exportToHtmlItem = WidgetFactory.createMenuItem("Export to HTML", IconUtils.WEBSITE);
        exportToHtmlItem
                .addActionListener(new ExportResultToHtmlActionListener(resultRef, _configuration, _userPreferences));
        exportToHtmlItem.setBorder(buttonBorder);
        _saveResultsPopupButton.getMenu().add(exportToHtmlItem);

        _tabbedPane = new VerticalTabbedPane() {
            private static final long serialVersionUID = 1L;

            @Override
            protected JComponent wrapInCollapsiblePane(final JComponent originalPanel) {
                final DCPanel buttonPanel = new DCPanel();
                buttonPanel.setLayout(new VerticalLayout());
                buttonPanel.setBorder(new MatteBorder(1, 0, 0, 0, WidgetUtils.BG_COLOR_MEDIUM));

                buttonPanel.add(_saveResultsPopupButton);
                buttonPanel.add(_cancelButton);

                final DCPanel wrappedPanel = new DCPanel();
                wrappedPanel.setLayout(new BorderLayout());
                wrappedPanel.add(originalPanel, BorderLayout.CENTER);
                wrappedPanel.add(buttonPanel, BorderLayout.SOUTH);
                return super.wrapInCollapsiblePane(wrappedPanel);
            }

            @Override
            public void setSelectedIndex(final int index) {
                if (index == 0) {
                    super.setSelectedIndex(index, false);
                } else {
                    super.setSelectedIndex(index, true);
                }
            }
        };

        final Dimension size = getDefaultWindowSize();
        _windowSizePreference = new WindowSizePreferences(_userPreferences, getClass(), size.width, size.height);
        _progressInformationPanel = new ProgressInformationPanel(running);
        _tabbedPane.addTab("Progress information",
                imageManager.getImageIcon("images/model/progress_information.png", IconUtils.ICON_SIZE_TAB),
                _progressInformationPanel);

        for (final Function pluggableComponent : PLUGGABLE_BANNER_COMPONENTS) {
            final JComponent component = pluggableComponent.apply(this);
            if (component != null) {
                if (component instanceof JMenuItem) {
                    final JMenuItem menuItem = (JMenuItem) component;
                    menuItem.setBorder(buttonBorder);
                    _saveResultsPopupButton.getMenu().add(menuItem);
                } else if (component instanceof AbstractButton) {
                    final AbstractButton button = (AbstractButton) component;
                    final JMenuItem menuItem = WidgetFactory.createMenuItem(button.getText(), button.getIcon());
                    for (final ActionListener listener : button.getActionListeners()) {
                        menuItem.addActionListener(listener);
                    }
                    menuItem.setBorder(buttonBorder);
                    _saveResultsPopupButton.getMenu().add(menuItem);
                }
            }
        }

        if (running) {
            // run the job in a swing worker
            _result = null;
            _worker = new AnalysisRunnerSwingWorker(_configuration, _job, this);

            _cancelButton.addActionListener(e -> _worker.cancelIfRunning());
        } else {
            // don't add the progress information, simply render the job asap
            _result = result;
            _worker = null;

            final Map map = result.getResultMap();
            for (final Entry entry : map.entrySet()) {
                final ComponentJob componentJob = entry.getKey();
                final AnalyzerResult analyzerResult = entry.getValue();

                addResult(componentJob, analyzerResult);
            }
            _progressInformationPanel.onSuccess();

            WidgetUtils.invokeSwingAction(() -> {
                if (_tabbedPane.getTabCount() > 1) {
                    // switch to the first available result panel
                    _tabbedPane.setSelectedIndex(1);
                }
            });
        }

        updateButtonVisibility(running);
    }

    public void startAnalysis() {
        _worker.execute();
    }

    private Dimension getDefaultWindowSize() {
        final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        final int screenWidth = screenSize.width;
        final int screenHeight = screenSize.height;

        int height = 550;
        if (screenHeight > 1000) {
            height = 900;
        } else if (screenHeight > 750) {
            height = 700;
        }

        int width = 750;
        if (screenWidth > 1200) {
            width = 1100;
        } else if (screenWidth > 1000) {
            width = 900;
        }

        return new Dimension(width, height);
    }

    public Tab getOrCreateResultPanel(final ComponentJob componentJob, final boolean finished) {
        synchronized (_resultPanels) {
            final Tab existingTab = _resultPanels.get(componentJob);
            if (existingTab != null) {
                return existingTab;
            }

            String title = LabelUtils.getLabel(componentJob, false, false, false);
            if (title.length() > 40) {
                title = title.substring(0, 39) + "...";
            }

            final Icon icon = IconUtils.getDescriptorIcon(componentJob.getDescriptor(), IconUtils.ICON_SIZE_TAB);
            final AnalyzerResultPanel resultPanel =
                    new AnalyzerResultPanel(_rendererFactory, _progressInformationPanel, componentJob);
            final Tab tab = _tabbedPane.addTab(title, icon, resultPanel);
            tab.setTooltip(LabelUtils.getLabel(componentJob, false, true, true));

            _resultPanels.put(componentJob, tab);

            return tab;
        }
    }

    public void addResult(final ComponentJob componentJob, final AnalyzerResult result) {
        final Tab tab = getOrCreateResultPanel(componentJob, true);
        tab.getContents().setResult(result);
    }

    @Override
    protected boolean isWindowResizable() {
        return true;
    }

    @Override
    protected boolean isCentered() {
        return true;
    }

    @Override
    public String getWindowTitle() {
        String title = "Analysis results";

        final String datastoreName = getDatastoreName();
        if (!StringUtils.isNullOrEmpty(datastoreName)) {
            title = datastoreName + " | " + title;
        }

        if (_jobFilename != null) {
            title = _jobFilename.getName().getBaseName() + " | " + title;
        }
        return title;
    }

    private String getDatastoreName() {
        if (_job != null) {
            final Datastore datastore = _job.getDatastore();
            if (datastore != null) {
                final String datastoreName = datastore.getName();
                if (!StringUtils.isNullOrEmpty(datastoreName)) {
                    return datastoreName;
                }
            }
        }
        return null;
    }

    @Override
    public Image getWindowIcon() {
        return imageManager.getImage(IconUtils.MODEL_RESULT);
    }

    @Override
    protected boolean onWindowClosing() {
        final boolean closing = super.onWindowClosing();
        if (closing) {
            if (_worker != null) {
                _worker.cancelIfRunning();
            }
        }
        return closing;
    }

    @Override
    protected JComponent getWindowContent() {
        final String datastoreName = getDatastoreName();
        String bannerTitle = "Analysis results";
        if (!StringUtils.isNullOrEmpty(datastoreName)) {
            bannerTitle = bannerTitle + " | " + datastoreName;

            if (_jobFilename != null) {
                bannerTitle = bannerTitle + " | " + _jobFilename.getName().getBaseName();
            }
        }

        final DCBannerPanel banner =
                new DCBannerPanel(imageManager.getImage("images/window/banner-results.png"), bannerTitle);
        _tabbedPane.addListener((newIndex, newTab) -> {
            banner.setTitle2(newTab.getTitle());
            banner.updateUI();
        });

        final DCPanel panel = new DCPersistentSizedPanel(WidgetUtils.COLOR_DEFAULT_BACKGROUND, _windowSizePreference);
        panel.setLayout(new BorderLayout());
        panel.add(banner, BorderLayout.NORTH);
        panel.add(_tabbedPane, BorderLayout.CENTER);
        return panel;
    }

    public AnalysisResult getResult() {
        if (_result == null && _worker != null) {
            try {
                _result = _worker.get();
            } catch (final Exception e) {
                WidgetUtils.showErrorMessage("Unable to fetch result", e);
            }
        }
        return _result;
    }

    /**
     * Sets the result, when it is ready for eg. saving
     *
     * @param result
     */
    public void setResult(final AnalysisResult result) {
        _result = result;
    }

    public RendererFactory getRendererFactory() {
        return _rendererFactory;
    }

    public ProgressInformationPanel getProgressInformationPanel() {
        return _progressInformationPanel;
    }

    public FileObject getJobFilename() {
        return _jobFilename;
    }

    public DataCleanerConfiguration getConfiguration() {
        return _configuration;
    }

    public AnalysisJob getJob() {
        return _job;
    }

    public UserPreferences getUserPreferences() {
        return _userPreferences;
    }

    public void onUnexpectedError(final AnalysisJob job, Throwable throwable) {
        throwable = ErrorUtils.unwrapForPresentation(throwable);
        if (throwable instanceof AnalysisJobCancellation) {
            _progressInformationPanel.onCancelled();
            _cancelButton.setEnabled(false);
            return;
        } else if (throwable instanceof PreviousErrorsExistException) {
            // do nothing
            return;
        }
        _progressInformationPanel.addUserLog("An error occurred in the analysis job!", throwable, true);
    }

    public AnalysisListener createAnalysisListener() {
        return new AnalysisListenerAdaptor() {
            @Override
            public void jobBegin(final AnalysisJob job, final AnalysisJobMetrics metrics) {
                updateButtonVisibility(true);
                _progressInformationPanel.onBegin();
            }

            @Override
            public void jobSuccess(final AnalysisJob job, final AnalysisJobMetrics metrics) {
                WidgetUtils.invokeSwingAction(() -> {
                    updateButtonVisibility(false);
                    _progressInformationPanel.onSuccess();
                    if (_tabbedPane.getTabCount() > 1) {
                        // switch to the first available result panel
                        _tabbedPane.setSelectedIndex(1);
                    }
                });
            }

            @Override
            public void onComponentMessage(final AnalysisJob job, final ComponentJob componentJob,
                    final ComponentMessage message) {
                if (message instanceof ExecutionLogMessage) {
                    final String messageString = ((ExecutionLogMessage) message).getMessage();
                    final String componentLabel = LabelUtils.getLabel(componentJob);

                    _progressInformationPanel.addUserLog(messageString + " (" + componentLabel + ")");
                } else if (message instanceof RestrictedFunctionalityMessage) {
                    final RestrictedFunctionalityMessage restrictedFunctionalityMessage =
                            (RestrictedFunctionalityMessage) message;
                    final String messageString = restrictedFunctionalityMessage.getMessage();
                    _progressInformationPanel.addRestrictedFunctionalityMessage(messageString,
                            restrictedFunctionalityMessage.getCallToActions());
                }
            }

            @Override
            public void rowProcessingBegin(final AnalysisJob job, final RowProcessingMetrics metrics) {
                logger.info("rowProcessingBegin: {}", job.getDatastore().getName());
                final int expectedRows = metrics.getExpectedRows();
                final Table table = metrics.getTable();

                WidgetUtils.invokeSwingAction(() -> {
                    final ComponentJob[] componentJobs = metrics.getResultProducers();
                    // Put analyzers at the top, then the rest (untouched)
                    Arrays.sort(componentJobs, (o1, o2) -> {
                        if ((o1 instanceof ImmutableAnalyzerJob) && !(o2 instanceof ImmutableAnalyzerJob)) {
                            return -1;
                        }
                        if ((o2 instanceof ImmutableAnalyzerJob) && !(o1 instanceof ImmutableAnalyzerJob)) {
                            return 1;
                        }
                        return 0;
                    });

                    for (final ComponentJob componentJob : componentJobs) {
                        // instantiate result panels
                        getOrCreateResultPanel(componentJob, false);
                    }
                    _tabbedPane.updateUI();

                    final String startingProcessingString = "Starting processing of " + table.getName();

                    if (expectedRows != -1) {
                        _progressInformationPanel
                                .addUserLog(startingProcessingString + " (approx. " + expectedRows + " rows)");
                    } else {
                        _progressInformationPanel.addUserLog(startingProcessingString);
                    }

                    _progressInformationPanel.addProgressBar(table, expectedRows);
                });
            }

            @Override
            public void rowProcessingProgress(final AnalysisJob job, final RowProcessingMetrics metrics,
                    final InputRow row, final int currentRow) {
                _progressInformationPanel.updateProgress(metrics.getTable(), currentRow);
            }

            @Override
            public void rowProcessingSuccess(final AnalysisJob job, final RowProcessingMetrics metrics) {
                logger.info("rowProcessingSuccess: {}", job.getDatastore().getName());
                _progressInformationPanel.updateProgressFinished(metrics.getTable());
                _progressInformationPanel.addUserLog(
                        "Processing of " + metrics.getTable().getName() + " is successful. Generating results...");
            }

            @Override
            public void componentBegin(final AnalysisJob job, final ComponentJob componentJob,
                    final ComponentMetrics metrics) {
                _progressInformationPanel.addUserLog("Starting component '" + LabelUtils.getLabel(componentJob) + "'");
            }

            @Override
            public void componentSuccess(final AnalysisJob job, final ComponentJob componentJob,
                    final AnalyzerResult result) {
                final StringBuilder sb = new StringBuilder();
                sb.append("Component ");
                sb.append(LabelUtils.getLabel(componentJob));
                sb.append(" finished.");

                if (result == null) {
                    _progressInformationPanel.addUserLog(sb.toString());
                } else {
                    sb.append(" Adding result...");
                    _progressInformationPanel.addUserLog(sb.toString());
                    WidgetUtils.invokeSwingAction(() -> addResult(componentJob, result));
                }
            }

            @Override
            public void errorInComponent(final AnalysisJob job, final ComponentJob componentJob, final InputRow row,
                    final Throwable throwable) {
                if (!(throwable instanceof PreviousErrorsExistException)) {
                    _progressInformationPanel
                            .addUserLog("An error occurred in the component: " + LabelUtils.getLabel(componentJob),
                                    throwable, true);
                }
            }

            @Override
            public void errorUnknown(final AnalysisJob job, final Throwable throwable) {
                onUnexpectedError(job, throwable);
            }
        };
    }

    protected void updateButtonVisibility(final boolean running) {
        SwingUtilities.invokeLater(() -> {
            _cancelButton.setVisible(running);
            _saveResultsPopupButton.setVisible(!running);
        });
    }

    public void windowClosed(final WindowEvent e) {
        if (this.getExtendedState() == JFrame.MAXIMIZED_BOTH) {
            _windowSizePreference.setUserPreferredSize(null, true);
        } else {
            _windowSizePreference.setUserPreferredSize(getSize(), false);
        }
    }

    @Override
    protected boolean maximizeWindow() {
        return _windowSizePreference.isWindowMaximized();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy