/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.workbench.internal; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import java.util.Properties; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExecutableExtension; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.equinox.app.IApplication; import org.eclipse.equinox.app.IApplicationContext; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.osgi.service.datalocation.Location; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.application.WorkbenchAdvisor; import org.eclipse.ui.internal.WorkbenchPlugin; import org.eclipse.ui.internal.ide.ChooseWorkspaceData; import org.eclipse.ui.internal.ide.ChooseWorkspaceDialog; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; import org.eclipse.ui.internal.ide.StatusUtil; import org.simantics.application.arguments.ApplicationUtils; import org.simantics.application.arguments.Arguments; import org.simantics.application.arguments.IArgumentFactory; import org.simantics.application.arguments.IArguments; import org.simantics.application.arguments.SimanticsArguments; import org.simantics.db.management.ISessionContextProvider; import org.simantics.db.management.ISessionContextProviderSource; import org.simantics.db.management.SessionContextProvider; import org.simantics.db.management.SingleSessionContextProviderSource; import org.simantics.ui.SimanticsUI; import org.simantics.utils.ui.BundleUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The "main program" for the Eclipse IDE. * * @since 3.0 */ public class SimanticsWorkbenchApplication implements IApplication, IExecutableExtension { private static final Logger LOGGER = LoggerFactory.getLogger(SimanticsWorkbenchApplication.class); /** * The name of the folder containing metadata information for the workspace. */ public static final String METADATA_FOLDER = ".metadata"; //$NON-NLS-1$ private static final String VERSION_FILENAME = "version.ini"; //$NON-NLS-1$ private static final String WORKSPACE_VERSION_KEY = "org.eclipse.core.runtime"; //$NON-NLS-1$ private static final String WORKSPACE_VERSION_VALUE = "1"; //$NON-NLS-1$ private static final String PROP_EXIT_CODE = "eclipse.exitcode"; //$NON-NLS-1$ private static final String PROP_SHUTDOWN_GRACE_PERIOD = "simantics.shutdownGracePeriod"; //$NON-NLS-1$ private static final long DEFAULT_SHUTDOWN_GRACE_PERIOD = 5000L; /** * A special return code that will be recognized by the launcher and used to * restart the workbench. */ private static final Integer EXIT_RELAUNCH = new Integer(24); /** * A special return code that will be recognized by the PDE launcher and used to * show an error dialog if the workspace is locked. */ private static final Integer EXIT_WORKSPACE_LOCKED = new Integer(15); /** * Creates a new IDE application. */ public SimanticsWorkbenchApplication() { // There is nothing to do for WorkbenchApplication } public WorkbenchAdvisor createWorkbenchAdvisor(IArguments args, DelayedEventsProcessor processor) { return new SimanticsWorkbenchAdvisor(args, processor); } /* (non-Javadoc) * @see org.eclipse.equinox.app.IApplication#start(org.eclipse.equinox.app.IApplicationContext context) */ @Override public Object start(IApplicationContext appContext) throws Exception { ApplicationUtils.loadSystemProperties(BundleUtils.find(Activator.PLUGIN_ID, "system.properties")); IArguments args = parseArguments((String[]) appContext.getArguments().get(IApplicationContext.APPLICATION_ARGS)); Display display = createDisplay(); // processor must be created before we start event loop DelayedEventsProcessor processor = new DelayedEventsProcessor(display); try { Object argCheck = verifyArguments(args); if (argCheck != null) return argCheck; // look and see if there's a splash shell we can parent off of Shell shell = WorkbenchPlugin.getSplashShell(display); if (shell != null) { // should should set the icon and message for this shell to be the // same as the chooser dialog - this will be the guy that lives in // the task bar and without these calls you'd have the default icon // with no message. shell.setText(ChooseWorkspaceDialog.getWindowTitle()); shell.setImages(Dialog.getDefaultImages()); } Object instanceLocationCheck = checkInstanceLocation(shell, appContext.getArguments(), args); if (instanceLocationCheck != null) { WorkbenchPlugin.unsetSplashShell(display); Platform.endSplash(); return instanceLocationCheck; } final ISessionContextProvider provider = new SessionContextProvider(null); final ISessionContextProviderSource contextProviderSource = new SingleSessionContextProviderSource(provider); //final ISessionContextProviderSource contextProviderSource = new WorkbenchWindowSessionContextProviderSource(PlatformUI.getWorkbench()); SimanticsUI.setSessionContextProviderSource(contextProviderSource); org.simantics.db.layer0.internal.SimanticsInternal.setSessionContextProviderSource(contextProviderSource); org.simantics.Simantics.setSessionContextProviderSource(contextProviderSource); // create the workbench with this advisor and run it until it exits // N.B. createWorkbench remembers the advisor, and also registers // the workbench globally so that all UI plug-ins can find it using // PlatformUI.getWorkbench() or AbstractUIPlugin.getWorkbench() int returnCode = PlatformUI.createAndRunWorkbench(display, createWorkbenchAdvisor(args, processor)); Long shutdownGracePeriodPropValue = Long.getLong(PROP_SHUTDOWN_GRACE_PERIOD); long shutdownGracePeriod = shutdownGracePeriodPropValue == null ? DEFAULT_SHUTDOWN_GRACE_PERIOD : shutdownGracePeriodPropValue; // the workbench doesn't support relaunch yet (bug 61809) so // for now restart is used, and exit data properties are checked // here to substitute in the relaunch return code if needed if (returnCode != PlatformUI.RETURN_RESTART) { delayedShutdown(EXIT_OK, shutdownGracePeriod); return EXIT_OK; } // if the exit code property has been set to the relaunch code, then // return that code now, otherwise this is a normal restart int exitCode = EXIT_RELAUNCH.equals(Integer.getInteger(PROP_EXIT_CODE)) ? EXIT_RELAUNCH : EXIT_RESTART; delayedShutdown(exitCode, shutdownGracePeriod); return exitCode; } finally { if (display != null) { display.dispose(); } Location instanceLoc = Platform.getInstanceLocation(); if (instanceLoc != null) instanceLoc.release(); } } private void delayedShutdown(int exitCode, long delayMs) { LOGGER.info("Started delayed shutdown with delay {} ms.", delayMs); Thread shutdownThread = new Thread() { @Override public void run() { try { Thread.sleep(delayMs); LOGGER.warn("Delayed shutdown forced the application to exit with code {}.", exitCode); // Method halt is used instead of System.exit, because running // of shutdown hooks hangs the application in some cases. Runtime.getRuntime().halt(exitCode); } catch (InterruptedException e) { e.printStackTrace(); } } }; shutdownThread.setDaemon(true); shutdownThread.setName("delayed-shutdown"); shutdownThread.start(); } /*************************************************************************/ private IArguments parseArguments(String[] args) { IArgumentFactory[] accepted = { SimanticsArguments.RECOVERY_POLICY_FIX_ERRORS, SimanticsArguments.ONTOLOGY_RECOVERY_POLICY_REINSTALL, SimanticsArguments.DEFAULT_WORKSPACE_LOCATION, SimanticsArguments.WORKSPACE_CHOOSER, SimanticsArguments.WORKSPACE_NO_REMEMBER, SimanticsArguments.PERSPECTIVE, SimanticsArguments.SERVER, SimanticsArguments.NEW_MODEL, SimanticsArguments.EXPERIMENT, SimanticsArguments.DISABLE_INDEX, SimanticsArguments.DATABASE_ID, SimanticsArguments.DO_NOT_SYNCHRONIZE_ONTOLOGIES }; IArguments result = Arguments.parse(args, accepted); return result; } private Object verifyArguments(IArguments args) { StringBuilder report = new StringBuilder(); // if (args.contains(SimanticsArguments.NEW_PROJECT)) { // if (args.contains(SimanticsArguments.PROJECT)) { // exclusiveArguments(report, SimanticsArguments.PROJECT, SimanticsArguments.NEW_PROJECT); // } // // Must have a server to checkout from when creating a new // // project right from the beginning. // if (!args.contains(SimanticsArguments.SERVER)) { // missingArgument(report, SimanticsArguments.SERVER); // } // } else if (args.contains(SimanticsArguments.PROJECT)) { // // To load a project, a server must be defined to checkout from // if (!args.contains(SimanticsArguments.SERVER)) { // missingArgument(report, SimanticsArguments.SERVER); // } // } // NEW_MODEL and MODEL arguments are optional // EXPERIMENT argument is optional String result = report.toString(); boolean valid = result.length() == 0; if (!valid) { String msg = NLS.bind(Messages.Application_1, result); MessageDialog.openInformation(null, Messages.Application_2, msg); } return valid ? null : EXIT_OK; } // private void exclusiveArguments(StringBuilder sb, IArgumentFactory arg1, IArgumentFactory arg2) { // sb.append(NLS.bind(Messages.Application_3, arg1.getArgument(), arg2.getArgument())); // sb.append('\n'); // } // // private void missingArgument(StringBuilder sb, IArgumentFactory arg) { // sb.append(NLS.bind(Messages.Application_0, arg.getArgument())); // sb.append('\n'); // } /*************************************************************************/ /** * Creates the display used by the application. * * @return the display used by the application */ protected Display createDisplay() { return PlatformUI.createDisplay(); } /* (non-Javadoc) * @see org.eclipse.core.runtime.IExecutableExtension#setInitializationData(org.eclipse.core.runtime.IConfigurationElement, java.lang.String, java.lang.Object) */ @Override public void setInitializationData(IConfigurationElement config, String propertyName, Object data) { // There is nothing to do for ProConfApplication } /** * Return true if a valid workspace path has been set and false otherwise. * Prompt for and set the path if possible and required. * @param applicationArguments * * @return true if a valid instance location has been set and false * otherwise */ private Object checkInstanceLocation(Shell shell, Map applicationArguments, IArguments args) { // -data @none was specified but an ide requires workspace Location instanceLoc = Platform.getInstanceLocation(); if (instanceLoc == null) { MessageDialog .openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceMandatoryTitle, IDEWorkbenchMessages.IDEApplication_workspaceMandatoryMessage); return EXIT_OK; } // -data "/valid/path", workspace already set // This information is stored in configuration/.settings/org.eclipse.ui.ide.prefs if (instanceLoc.isSet()) { // make sure the meta data version is compatible (or the user has // chosen to overwrite it). if (!checkValidWorkspace(shell, instanceLoc.getURL())) { return EXIT_OK; } // at this point its valid, so try to lock it and update the // metadata version information if successful try { if (instanceLoc.lock()) { writeWorkspaceVersion(); return null; } // we failed to create the directory. // Two possibilities: // 1. directory is already in use // 2. directory could not be created File workspaceDirectory = new File(instanceLoc.getURL().getFile()); if (workspaceDirectory.exists()) { if (isDevLaunchMode(applicationArguments)) { return EXIT_WORKSPACE_LOCKED; } MessageDialog.openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceCannotLockTitle, IDEWorkbenchMessages.IDEApplication_workspaceCannotLockMessage); } else { MessageDialog.openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetTitle, IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetMessage); } } catch (IOException e) { IDEWorkbenchPlugin.log("Could not obtain lock for workspace location", //$NON-NLS-1$ e); MessageDialog .openError( shell, IDEWorkbenchMessages.InternalError, e.getMessage()); } return EXIT_OK; } // -data @noDefault or -data not specified, prompt and set ChooseWorkspaceData launchData = null; if (args.contains(SimanticsArguments.DEFAULT_WORKSPACE_LOCATION)) { launchData = new ChooseWorkspaceData(args.get(SimanticsArguments.DEFAULT_WORKSPACE_LOCATION)); } else { launchData = new ChooseWorkspaceData(instanceLoc.getDefault()); } boolean force = args.contains(SimanticsArguments.WORKSPACE_CHOOSER); boolean suppressAskAgain = args.contains(SimanticsArguments.WORKSPACE_NO_REMEMBER); while (true) { URL workspaceUrl = promptForWorkspace(shell, launchData, force, suppressAskAgain); if (workspaceUrl == null) { return EXIT_OK; } // if there is an error with the first selection, then force the // dialog to open to give the user a chance to correct force = true; try { // the operation will fail if the url is not a valid // instance data area, so other checking is unneeded if (instanceLoc.setURL(workspaceUrl, true)) { launchData.writePersistedData(); writeWorkspaceVersion(); return null; } } catch (IllegalStateException e) { MessageDialog .openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetTitle, IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetMessage); return EXIT_OK; } // by this point it has been determined that the workspace is // already in use -- force the user to choose again MessageDialog.openError(shell, IDEWorkbenchMessages.IDEApplication_workspaceInUseTitle, IDEWorkbenchMessages.IDEApplication_workspaceInUseMessage); } } private static boolean isDevLaunchMode(Map args) { // see org.eclipse.pde.internal.core.PluginPathFinder.isDevLaunchMode() if (Boolean.getBoolean("eclipse.pde.launch")) //$NON-NLS-1$ return true; return args.containsKey("-pdelaunch"); //$NON-NLS-1$ } private static class ChooseSimanticsWorkspaceDialog extends ChooseWorkspaceDialog { public ChooseSimanticsWorkspaceDialog(Shell parentShell, ChooseWorkspaceData launchData, boolean suppressAskAgain, boolean centerOnMonitor) { super(parentShell, launchData, suppressAskAgain, centerOnMonitor); } @Override protected void configureShell(Shell shell) { super.configureShell(shell); // Use product name in shell title instead of generic "Eclipse Launcher" shell.setText(getWindowTitle()); } } /** * Open a workspace selection dialog on the argument shell, populating the * argument data with the user's selection. Perform first level validation * on the selection by comparing the version information. This method does * not examine the runtime state (e.g., is the workspace already locked?). * * @param shell * @param launchData * @param force * setting to true makes the dialog open regardless of the * showDialog value * @return An URL storing the selected workspace or null if the user has * canceled the launch operation. */ private URL promptForWorkspace(Shell shell, ChooseWorkspaceData launchData, boolean force, boolean suppressAskAgain) { URL url = null; do { // okay to use the shell now - this is the splash shell new ChooseSimanticsWorkspaceDialog(shell, launchData, suppressAskAgain, true).prompt(force); String instancePath = launchData.getSelection(); if (instancePath == null) { return null; } // the dialog is not forced on the first iteration, but is on every // subsequent one -- if there was an error then the user needs to be // allowed to fix it force = true; // 70576: don't accept empty input if (instancePath.length() <= 0) { MessageDialog .openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceEmptyTitle, IDEWorkbenchMessages.IDEApplication_workspaceEmptyMessage); continue; } // create the workspace if it does not already exist File workspace = new File(instancePath); if (!workspace.exists()) { workspace.mkdir(); } try { // Don't use File.toURL() since it adds a leading slash that Platform does not // handle properly. See bug 54081 for more details. String path = workspace.getAbsolutePath().replace( File.separatorChar, '/'); url = new URL("file", null, path); //$NON-NLS-1$ } catch (MalformedURLException e) { MessageDialog .openError( shell, IDEWorkbenchMessages.IDEApplication_workspaceInvalidTitle, IDEWorkbenchMessages.IDEApplication_workspaceInvalidMessage); continue; } } while (!checkValidWorkspace(shell, url)); return url; } /** * Return true if the argument directory is ok to use as a workspace and * false otherwise. A version check will be performed, and a confirmation * box may be displayed on the argument shell if an older version is * detected. * * @return true if the argument URL is ok to use as a workspace and false * otherwise. */ private boolean checkValidWorkspace(Shell shell, URL url) { // a null url is not a valid workspace if (url == null) { return false; } String version = readWorkspaceVersion(url); // if the version could not be read, then there is not any existing // workspace data to trample, e.g., perhaps its a new directory that // is just starting to be used as a workspace if (version == null) { return true; } final int ide_version = Integer.parseInt(WORKSPACE_VERSION_VALUE); int workspace_version = Integer.parseInt(version); // equality test is required since any version difference (newer // or older) may result in data being trampled if (workspace_version == ide_version) { return true; } // At this point workspace has been detected to be from a version // other than the current ide version -- find out if the user wants // to use it anyhow. int severity; String title; String message; if (workspace_version < ide_version) { // Workspace < IDE. Update must be possible without issues, // so only inform user about it. severity = MessageDialog.INFORMATION; title = IDEWorkbenchMessages.IDEApplication_versionTitle_olderWorkspace; message = NLS.bind(IDEWorkbenchMessages.IDEApplication_versionMessage_olderWorkspace, url.getFile()); } else { // Workspace > IDE. It must have been opened with a newer IDE version. // Downgrade might be problematic, so warn user about it. severity = MessageDialog.WARNING; title = IDEWorkbenchMessages.IDEApplication_versionTitle_newerWorkspace; message = NLS.bind(IDEWorkbenchMessages.IDEApplication_versionMessage_newerWorkspace, url.getFile()); } MessageBox mbox = new MessageBox(shell, SWT.OK | SWT.CANCEL | SWT.ICON_WARNING | SWT.APPLICATION_MODAL); mbox.setText(title); mbox.setMessage(message); return mbox.open() == SWT.OK; } /** * Look at the argument URL for the workspace's version information. Return * that version if found and null otherwise. */ private static String readWorkspaceVersion(URL workspace) { File versionFile = getVersionFile(workspace, false); if (versionFile == null || !versionFile.exists()) { return null; } try { // Although the version file is not spec'ed to be a Java properties // file, it happens to follow the same format currently, so using // Properties to read it is convenient. Properties props = new Properties(); FileInputStream is = new FileInputStream(versionFile); try { props.load(is); } finally { is.close(); } return props.getProperty(WORKSPACE_VERSION_KEY); } catch (IOException e) { IDEWorkbenchPlugin.log("Could not read version file", new Status( //$NON-NLS-1$ IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, IStatus.ERROR, e.getMessage() == null ? "" : e.getMessage(), //$NON-NLS-1$, e)); return null; } } /** * Write the version of the metadata into a known file overwriting any * existing file contents. Writing the version file isn't really crucial, * so the function is silent about failure */ private static void writeWorkspaceVersion() { Location instanceLoc = Platform.getInstanceLocation(); if (instanceLoc == null || instanceLoc.isReadOnly()) { return; } File versionFile = getVersionFile(instanceLoc.getURL(), true); if (versionFile == null) { return; } OutputStream output = null; try { String versionLine = WORKSPACE_VERSION_KEY + '=' + WORKSPACE_VERSION_VALUE; output = new FileOutputStream(versionFile); output.write(versionLine.getBytes("UTF-8")); //$NON-NLS-1$ } catch (IOException e) { IDEWorkbenchPlugin.log("Could not write version file", //$NON-NLS-1$ StatusUtil.newStatus(IStatus.ERROR, e.getMessage(), e)); } finally { try { if (output != null) { output.close(); } } catch (IOException e) { // do nothing } } } /** * The version file is stored in the metadata area of the workspace. This * method returns an URL to the file or null if the directory or file does * not exist (and the create parameter is false). * * @param create * If the directory and file does not exist this parameter * controls whether it will be created. * @return An url to the file or null if the version file does not exist or * could not be created. */ private static File getVersionFile(URL workspaceUrl, boolean create) { if (workspaceUrl == null) { return null; } try { // make sure the directory exists File metaDir = new File(workspaceUrl.getPath(), METADATA_FOLDER); if (!metaDir.exists() && (!create || !metaDir.mkdir())) { return null; } // make sure the file exists File versionFile = new File(metaDir, VERSION_FILENAME); if (!versionFile.exists() && (!create || !versionFile.createNewFile())) { return null; } return versionFile; } catch (IOException e) { // cannot log because instance area has not been set return null; } } /* (non-Javadoc) * @see org.eclipse.equinox.app.IApplication#stop() */ @Override public void stop() { final IWorkbench workbench = PlatformUI.getWorkbench(); if (workbench == null) return; final Display display = workbench.getDisplay(); display.syncExec(new Runnable() { @Override public void run() { if (!display.isDisposed()) workbench.close(); } }); } }