/** * 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 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.apache.metamodel.util.Func; import org.apache.metamodel.util.Ref; import org.datacleaner.actions.ExportResultToHtmlActionListener; import org.datacleaner.actions.PublishResultToMonitorActionListener; 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<Func<ResultWindow, JComponent>> 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<ComponentJob, Tab<AnalyzerResultPanel>> _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 Ref<AnalysisResult> 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); final JMenuItem publishToServerItem = WidgetFactory.createMenuItem("Publish to server", IconUtils.MENU_DQ_MONITOR); publishToServerItem.addActionListener( new PublishResultToMonitorActionListener(getWindowContext(), _userPreferences, resultRef, _jobFilename)); publishToServerItem.setBorder(buttonBorder); _saveResultsPopupButton.getMenu().add(publishToServerItem); _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 Func<ResultWindow, JComponent> pluggableComponent : PLUGGABLE_BANNER_COMPONENTS) { final JComponent component = pluggableComponent.eval(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<ComponentJob, AnalyzerResult> map = result.getResultMap(); for (final Entry<ComponentJob, AnalyzerResult> 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<AnalyzerResultPanel> getOrCreateResultPanel(final ComponentJob componentJob, final boolean finished) { synchronized (_resultPanels) { final Tab<AnalyzerResultPanel> 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<AnalyzerResultPanel> 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<AnalyzerResultPanel> 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(); } }