/**
* 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.bootstrap;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.SplashScreen;
import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.swing.JOptionPane;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.provider.AbstractFileSystem;
import org.apache.commons.vfs2.provider.DelegateFileObject;
import org.apache.commons.vfs2.provider.url.UrlFileName;
import org.apache.metamodel.util.FileHelper;
import org.datacleaner.Version;
import org.datacleaner.actions.DownloadFilesActionListener;
import org.datacleaner.actions.OpenAnalysisJobActionListener;
import org.datacleaner.cli.CliArguments;
import org.datacleaner.cli.CliRunType;
import org.datacleaner.cli.CliRunner;
import org.datacleaner.configuration.DataCleanerConfiguration;
import org.datacleaner.configuration.DataCleanerConfigurationImpl;
import org.datacleaner.connection.Datastore;
import org.datacleaner.connection.DatastoreCatalog;
import org.datacleaner.connection.DatastoreConnection;
import org.datacleaner.extensionswap.ExtensionSwapClient;
import org.datacleaner.extensionswap.ExtensionSwapInstallationHttpContainer;
import org.datacleaner.guice.DCModuleImpl;
import org.datacleaner.guice.InjectorBuilder;
import org.datacleaner.job.builder.AnalysisJobBuilder;
import org.datacleaner.macos.MacOSManager;
import org.datacleaner.user.DataCleanerHome;
import org.datacleaner.user.MonitorConnection;
import org.datacleaner.user.UsageLogger;
import org.datacleaner.user.UserPreferences;
import org.datacleaner.user.UserPreferencesImpl;
import org.datacleaner.util.DCUncaughtExceptionHandler;
import org.datacleaner.util.LookAndFeelManager;
import org.datacleaner.util.StringUtils;
import org.datacleaner.util.VFSUtils;
import org.datacleaner.util.WidgetUtils;
import org.datacleaner.util.http.MonitorHttpClient;
import org.datacleaner.util.http.SimpleWebServiceHttpClient;
import org.datacleaner.windows.AbstractWindow;
import org.datacleaner.windows.AnalysisJobBuilderWindow;
import org.datacleaner.windows.DataCloudLogInWindow;
import org.datacleaner.windows.MonitorConnectionDialog;
import org.datacleaner.windows.WelcomeDialog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
/**
* Bootstraps an instance of DataCleaner into a running state. The initial state
* of the application will be dependent on specified options (or defaults).
*/
public final class Bootstrap {
private static final Logger logger = LoggerFactory.getLogger(Bootstrap.class);
private final BootstrapOptions _options;
public Bootstrap(final BootstrapOptions options) {
if (options == null) {
throw new IllegalArgumentException("Bootstrap options cannot be null");
}
_options = options;
}
public void run() {
try {
runInternal();
} catch (final Exception e) {
logger.error("An unexpected error has occurred during bootstrap. Exiting with status code -2.", e);
exitCommandLine(null, -2);
}
}
private void runInternal() throws FileSystemException {
logger.info("Welcome to DataCleaner {}", Version.getVersion());
// determine whether to run in command line interface mode
final boolean cliMode = _options.isCommandLineMode();
final CliArguments arguments = _options.getCommandLineArguments();
logger.info("CLI mode={}, use -usage to view usage options", cliMode);
if (cliMode) {
try {
if (!GraphicsEnvironment.isHeadless()) {
// hide splash screen
final SplashScreen splashScreen = SplashScreen.getSplashScreen();
if (splashScreen != null) {
splashScreen.close();
}
}
} catch (final Exception e) {
// ignore this condition - may happen rarely on e.g. X windows
// systems when the user is not authorized to access the
// graphics environment.
logger.trace("Swallowing exception caused by trying to hide splash screen", e);
}
if (arguments.isUsageMode()) {
final PrintWriter out = new PrintWriter(System.out);
try {
CliArguments.printUsage(out);
} finally {
FileHelper.safeClose(out);
}
exitCommandLine(null, 1);
return;
}
if (arguments.isVersionMode()) {
final PrintWriter out = new PrintWriter(System.out);
try {
CliArguments.printVersion(out);
} finally {
FileHelper.safeClose(out);
}
exitCommandLine(null, 1);
return;
}
}
if (!cliMode) {
// set up error handling that displays an error dialog
final DCUncaughtExceptionHandler exceptionHandler = new DCUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
// init the look and feel
LookAndFeelManager.get().init();
}
if (arguments.getRunType() == CliRunType.SPARK) {
runCli(arguments, null);
return;
}
// initially use a temporary non-persistent user preferences object.
// This is just to have basic settings available for eg. resolving
// files.
final UserPreferences initialUserPreferences = new UserPreferencesImpl(null);
final String configurationFilePath = arguments.getConfigurationFile();
final FileObject configurationFile =
resolveFile(configurationFilePath, DataCleanerConfigurationImpl.DEFAULT_FILENAME,
initialUserPreferences);
Injector injector = Guice.createInjector(new DCModuleImpl(DataCleanerHome.get(), configurationFile));
// configuration loading can be multithreaded, so begin early
final DataCleanerConfiguration configuration = injector.getInstance(DataCleanerConfiguration.class);
// log usage
final UsageLogger usageLogger = injector.getInstance(UsageLogger.class);
usageLogger.logApplicationStartup();
if (cliMode) {
runCli(arguments, configuration);
} else {
// run in GUI mode
final AnalysisJobBuilderWindow analysisJobBuilderWindow;
// initialize Mac OS specific settings
final MacOSManager macOsManager = injector.getInstance(MacOSManager.class);
macOsManager.init();
// check for job file
final String jobFilePath = _options.getCommandLineArguments().getJobFile();
if (jobFilePath != null) {
final FileObject jobFile = resolveFile(jobFilePath, null, initialUserPreferences);
injector = OpenAnalysisJobActionListener.open(jobFile, configuration, injector);
}
final UserPreferences userPreferences = injector.getInstance(UserPreferences.class);
final WindowContext windowContext = injector.getInstance(WindowContext.class);
analysisJobBuilderWindow = injector.getInstance(AnalysisJobBuilderWindow.class);
final Datastore singleDatastore;
if (_options.isSingleDatastoreMode()) {
final DatastoreCatalog datastoreCatalog = configuration.getDatastoreCatalog();
singleDatastore = _options.getSingleDatastore(datastoreCatalog);
if (singleDatastore == null) {
logger.info("Single datastore mode was enabled, but datastore was null!");
} else {
logger.info("Initializing single datastore mode with {}", singleDatastore);
analysisJobBuilderWindow.setDatastoreSelectionEnabled(false);
analysisJobBuilderWindow.setDatastore(singleDatastore, true);
}
} else {
singleDatastore = null;
}
// show the window
analysisJobBuilderWindow.open();
if (singleDatastore != null) {
// this part has to be done after displaying the window (a lot
// of initialization goes on there)
final AnalysisJobBuilder analysisJobBuilder = analysisJobBuilderWindow.getAnalysisJobBuilder();
try (DatastoreConnection con = singleDatastore.openConnection()) {
final InjectorBuilder injectorBuilder = injector.getInstance(InjectorBuilder.class);
_options.initializeSingleDatastoreJob(analysisJobBuilder, con.getDataContext(), injectorBuilder);
}
}
final Image welcomeImage = _options.getWelcomeImage();
if (welcomeImage != null) {
// Ticket #834: make sure to show welcome dialog in swing's
// dispatch thread.
WidgetUtils.invokeSwingAction(() -> {
final WelcomeDialog welcomeDialog = new WelcomeDialog(analysisJobBuilderWindow, welcomeImage);
welcomeDialog.setVisible(true);
});
}
WidgetUtils.invokeSwingAction(() -> {
if (DataCloudLogInWindow.isRelevantToShow(userPreferences, configuration, true)) {
final DataCloudLogInWindow dataCloudLogInWindow =
new DataCloudLogInWindow(configuration, userPreferences, windowContext,
(AbstractWindow) analysisJobBuilderWindow);
dataCloudLogInWindow.open();
}
});
// set up HTTP service for ExtensionSwap installation
loadExtensionSwapService(userPreferences, windowContext, configuration, usageLogger);
final ExitActionListener exitActionListener = _options.getExitActionListener();
if (exitActionListener != null) {
windowContext.addExitActionListener(exitActionListener);
}
}
}
private void runCli(final CliArguments arguments, final DataCleanerConfiguration configuration) {
// run in CLI mode
int exitCode = 0;
try (CliRunner runner = new CliRunner(arguments)) {
runner.run(configuration);
} catch (final Throwable e) {
logger.error("Error occurred while running DataCleaner command line mode", e);
exitCode = 1;
} finally {
exitCommandLine(configuration, exitCode);
}
}
/**
* Looks up a file, either based on a user requested filename (typically a
* CLI parameter, may be a URL) or by a relative filename defined in the
* system-
*
* @param userRequestedFilename
* the user requested filename, may be null
* @param localFilename
* the relative filename defined by the system
* @param userPreferences
* @return
* @throws FileSystemException
*/
private FileObject resolveFile(final String userRequestedFilename, final String localFilename,
final UserPreferences userPreferences) throws FileSystemException {
final File dataCleanerHome = DataCleanerHome.getAsFile();
if (userRequestedFilename == null) {
final File file = new File(dataCleanerHome, localFilename);
return VFSUtils.toFileObject(file);
} else {
final String lowerCaseFilename = userRequestedFilename.toLowerCase();
if (lowerCaseFilename.startsWith("http://") || lowerCaseFilename.startsWith("https://")) {
if (!GraphicsEnvironment.isHeadless()) {
// download to a RAM file.
final FileObject targetDirectory =
VFSUtils.getFileSystemManager().resolveFile("ram:///datacleaner/temp");
if (!targetDirectory.exists()) {
targetDirectory.createFolder();
}
final URI uri;
try {
uri = new URI(userRequestedFilename);
} catch (final URISyntaxException e) {
throw new IllegalArgumentException("Illegal URI: " + userRequestedFilename, e);
}
final WindowContext windowContext = new SimpleWindowContext();
MonitorConnection monitorConnection = null;
// check if URI points to DC monitor. If so, make sure
// credentials are entered.
if (userPreferences != null && userPreferences.getMonitorConnection() != null) {
monitorConnection = userPreferences.getMonitorConnection();
if (monitorConnection.matchesURI(uri)) {
if (monitorConnection.isAuthenticationEnabled()) {
if (monitorConnection.getEncodedPassword() == null) {
final MonitorConnectionDialog dialog =
new MonitorConnectionDialog(windowContext, userPreferences);
dialog.openBlocking();
}
monitorConnection = userPreferences.getMonitorConnection();
}
}
}
try (MonitorHttpClient httpClient = getHttpClient(monitorConnection)) {
final String[] urls = new String[] { userRequestedFilename };
final String[] targetFilenames = DownloadFilesActionListener.createTargetFilenames(urls);
final FileObject[] files =
downloadFiles(urls, targetDirectory, targetFilenames, windowContext, httpClient,
monitorConnection);
assert files.length == 1;
final FileObject ramFile = files[0];
if (logger.isInfoEnabled()) {
final InputStream in = ramFile.getContent().getInputStream();
try {
final String str = FileHelper
.readInputStreamAsString(ramFile.getContent().getInputStream(), "UTF8");
logger.info("Downloaded file contents: {}\n{}", userRequestedFilename, str);
} finally {
FileHelper.safeClose(in);
}
}
final String scheme = uri.getScheme();
final int defaultPort;
if ("http".equals(scheme)) {
defaultPort = 80;
} else {
defaultPort = 443;
}
final UrlFileName fileName =
new UrlFileName(scheme, uri.getHost(), uri.getPort(), defaultPort, null, null,
uri.getPath(), FileType.FILE, uri.getQuery());
final AbstractFileSystem fileSystem = (AbstractFileSystem) VFSUtils.getBaseFileSystem();
return new DelegateFileObject(fileName, fileSystem, ramFile);
} finally {
userPreferences.setMonitorConnection(monitorConnection);
}
}
}
return VFSUtils.getFileSystemManager().resolveFile(userRequestedFilename);
}
}
private MonitorHttpClient getHttpClient(final MonitorConnection monitorConnection) {
if (monitorConnection == null) {
return new SimpleWebServiceHttpClient();
} else {
return monitorConnection.getHttpClient();
}
}
private FileObject[] downloadFiles(final String[] urls, final FileObject targetDirectory,
final String[] targetFilenames, final WindowContext windowContext, MonitorHttpClient httpClient,
final MonitorConnection monitorConnection) {
final DownloadFilesActionListener downloadAction =
new DownloadFilesActionListener(urls, targetDirectory, targetFilenames, null, windowContext,
httpClient);
try {
downloadAction.actionPerformed(null);
final FileObject[] files = downloadAction.getFiles();
if (logger.isInfoEnabled()) {
logger.info("Succesfully downloaded urls: {}", Arrays.toString(urls));
}
return files;
} catch (final SSLPeerUnverifiedException e) {
downloadAction.cancelDownload(true);
if (monitorConnection == null || monitorConnection.isAcceptUnverifiedSslPeers()) {
throw new IllegalStateException("Failed to verify SSL peer", e);
}
if (logger.isInfoEnabled()) {
logger.info("SSL peer not verified. Asking user for confirmation to accept urls: {}",
Arrays.toString(urls));
}
final int confirmation = JOptionPane.showConfirmDialog(null,
"Unverified SSL peer.\n\n" + "The certificate presented by the server could not be verified.\n\n"
+ "Do you want to continue, accepting the unverified certificate?", "Unverified SSL peer",
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
if (confirmation != JOptionPane.YES_OPTION) {
throw new IllegalStateException(e);
}
monitorConnection.setAcceptUnverifiedSslPeers(true);
httpClient = monitorConnection.getHttpClient();
return downloadFiles(urls, targetDirectory, targetFilenames, windowContext, httpClient, monitorConnection);
}
}
private void exitCommandLine(final DataCleanerConfiguration configuration, final int statusCode) {
if (configuration != null) {
logger.debug("Shutting down task runner");
try {
configuration.getEnvironment().getTaskRunner().shutdown();
} catch (final Exception e) {
logger.warn("Shutting down TaskRunner threw unexpected exception", e);
}
}
final ExitActionListener exitActionListener = _options.getExitActionListener();
if (exitActionListener != null) {
exitActionListener.exit(statusCode);
}
}
private void loadExtensionSwapService(final UserPreferences userPreferences, final WindowContext windowContext,
final DataCleanerConfiguration configuration, final UsageLogger usageLogger) {
String websiteHostname = userPreferences.getAdditionalProperties().get("extensionswap.hostname");
if (StringUtils.isNullOrEmpty(websiteHostname)) {
websiteHostname = System.getProperty("extensionswap.hostname");
}
final ExtensionSwapClient extensionSwapClient;
if (StringUtils.isNullOrEmpty(websiteHostname)) {
logger.info("Using default ExtensionSwap website hostname");
extensionSwapClient = new ExtensionSwapClient(windowContext, userPreferences, configuration);
} else {
logger.info("Using custom ExtensionSwap website hostname: {}", websiteHostname);
extensionSwapClient =
new ExtensionSwapClient(websiteHostname, windowContext, userPreferences, configuration);
}
final ExtensionSwapInstallationHttpContainer container =
new ExtensionSwapInstallationHttpContainer(extensionSwapClient, usageLogger);
final Closeable closeableConnection = container.initialize();
if (closeableConnection != null) {
windowContext.addExitActionListener(statusCode -> FileHelper.safeClose(closeableConnection));
}
}
}