8b0431f38e9cde7d6236b146ab5d239034eca37a
[simantics/platform.git] / bundles / org.simantics.workbench / src / org / simantics / workbench / internal / SimanticsWorkbenchApplication.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.workbench.internal;
13
14 import java.io.File;
15 import java.io.FileInputStream;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.io.OutputStream;
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.util.Map;
22 import java.util.Properties;
23
24 import org.eclipse.core.runtime.IConfigurationElement;
25 import org.eclipse.core.runtime.IExecutableExtension;
26 import org.eclipse.core.runtime.IStatus;
27 import org.eclipse.core.runtime.Platform;
28 import org.eclipse.core.runtime.Status;
29 import org.eclipse.equinox.app.IApplication;
30 import org.eclipse.equinox.app.IApplicationContext;
31 import org.eclipse.jface.dialogs.Dialog;
32 import org.eclipse.jface.dialogs.MessageDialog;
33 import org.eclipse.osgi.service.datalocation.Location;
34 import org.eclipse.osgi.util.NLS;
35 import org.eclipse.swt.SWT;
36 import org.eclipse.swt.widgets.Display;
37 import org.eclipse.swt.widgets.MessageBox;
38 import org.eclipse.swt.widgets.Shell;
39 import org.eclipse.ui.IWorkbench;
40 import org.eclipse.ui.PlatformUI;
41 import org.eclipse.ui.application.WorkbenchAdvisor;
42 import org.eclipse.ui.internal.WorkbenchPlugin;
43 import org.eclipse.ui.internal.ide.ChooseWorkspaceData;
44 import org.eclipse.ui.internal.ide.ChooseWorkspaceDialog;
45 import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
46 import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
47 import org.eclipse.ui.internal.ide.StatusUtil;
48 import org.simantics.application.arguments.ApplicationUtils;
49 import org.simantics.application.arguments.Arguments;
50 import org.simantics.application.arguments.IArgumentFactory;
51 import org.simantics.application.arguments.IArguments;
52 import org.simantics.application.arguments.SimanticsArguments;
53 import org.simantics.db.management.ISessionContextProvider;
54 import org.simantics.db.management.ISessionContextProviderSource;
55 import org.simantics.db.management.SessionContextProvider;
56 import org.simantics.db.management.SingleSessionContextProviderSource;
57 import org.simantics.ui.SimanticsUI;
58 import org.simantics.utils.ui.BundleUtils;
59
60
61 /**
62  * The "main program" for the Eclipse IDE.
63  * 
64  * @since 3.0
65  */
66 public class SimanticsWorkbenchApplication implements IApplication, IExecutableExtension {
67
68     /**
69      * The name of the folder containing metadata information for the workspace.
70      */
71     public static final String METADATA_FOLDER = ".metadata"; //$NON-NLS-1$
72
73     private static final String VERSION_FILENAME = "version.ini"; //$NON-NLS-1$
74
75     private static final String WORKSPACE_VERSION_KEY = "org.eclipse.core.runtime"; //$NON-NLS-1$
76
77     private static final String WORKSPACE_VERSION_VALUE = "1"; //$NON-NLS-1$
78
79     private static final String PROP_EXIT_CODE = "eclipse.exitcode"; //$NON-NLS-1$
80
81     /**
82      * A special return code that will be recognized by the launcher and used to
83      * restart the workbench.
84      */
85     private static final Integer EXIT_RELAUNCH = new Integer(24);
86
87     /**
88      * A special return code that will be recognized by the PDE launcher and used to
89      * show an error dialog if the workspace is locked.
90      */
91     private static final Integer EXIT_WORKSPACE_LOCKED = new Integer(15);
92
93     /**
94      * Creates a new IDE application.
95      */
96     public SimanticsWorkbenchApplication() {
97         // There is nothing to do for WorkbenchApplication
98     }
99
100     public WorkbenchAdvisor createWorkbenchAdvisor(IArguments args, DelayedEventsProcessor processor) {
101         return new SimanticsWorkbenchAdvisor(args, processor);
102     }
103
104     /* (non-Javadoc)
105      * @see org.eclipse.equinox.app.IApplication#start(org.eclipse.equinox.app.IApplicationContext context)
106      */
107     @Override
108     public Object start(IApplicationContext appContext) throws Exception {
109         ApplicationUtils.loadSystemProperties(BundleUtils.find(Activator.PLUGIN_ID, "system.properties"));
110         IArguments args = parseArguments((String[]) appContext.getArguments().get(IApplicationContext.APPLICATION_ARGS));
111
112         Display display = createDisplay();
113         // processor must be created before we start event loop
114         DelayedEventsProcessor processor = new DelayedEventsProcessor(display);
115
116         try {
117             Object argCheck = verifyArguments(args);
118             if (argCheck != null)
119                 return argCheck;
120
121             // look and see if there's a splash shell we can parent off of
122             Shell shell = WorkbenchPlugin.getSplashShell(display);
123             if (shell != null) {
124                 // should should set the icon and message for this shell to be the 
125                 // same as the chooser dialog - this will be the guy that lives in
126                 // the task bar and without these calls you'd have the default icon 
127                 // with no message.
128                 shell.setText(ChooseWorkspaceDialog.getWindowTitle());
129                 shell.setImages(Dialog.getDefaultImages());
130             }
131
132             Object instanceLocationCheck = checkInstanceLocation(shell, appContext.getArguments(), args);
133             if (instanceLocationCheck != null) {
134                 WorkbenchPlugin.unsetSplashShell(display);
135                 Platform.endSplash();
136                 return instanceLocationCheck;
137             }
138
139             final ISessionContextProvider provider = new SessionContextProvider(null);
140             final ISessionContextProviderSource contextProviderSource = new SingleSessionContextProviderSource(provider);
141             //final ISessionContextProviderSource contextProviderSource = new WorkbenchWindowSessionContextProviderSource(PlatformUI.getWorkbench());
142             SimanticsUI.setSessionContextProviderSource(contextProviderSource);
143             org.simantics.db.layer0.internal.SimanticsInternal.setSessionContextProviderSource(contextProviderSource);
144             org.simantics.Simantics.setSessionContextProviderSource(contextProviderSource);
145             
146             // create the workbench with this advisor and run it until it exits
147             // N.B. createWorkbench remembers the advisor, and also registers
148             // the workbench globally so that all UI plug-ins can find it using
149             // PlatformUI.getWorkbench() or AbstractUIPlugin.getWorkbench()
150             int returnCode = PlatformUI.createAndRunWorkbench(display,
151                     createWorkbenchAdvisor(args, processor));
152
153             // the workbench doesn't support relaunch yet (bug 61809) so
154             // for now restart is used, and exit data properties are checked
155             // here to substitute in the relaunch return code if needed
156             if (returnCode != PlatformUI.RETURN_RESTART) {
157                 return EXIT_OK;
158             }
159
160             // if the exit code property has been set to the relaunch code, then
161             // return that code now, otherwise this is a normal restart
162             return EXIT_RELAUNCH.equals(Integer.getInteger(PROP_EXIT_CODE)) ? EXIT_RELAUNCH
163                     : EXIT_RESTART;
164         } finally {
165             if (display != null) {
166                 display.dispose();
167             }
168             Location instanceLoc = Platform.getInstanceLocation();
169             if (instanceLoc != null)
170                 instanceLoc.release();
171         }
172     }
173
174     /*************************************************************************/
175
176     private IArguments parseArguments(String[] args) {
177         IArgumentFactory<?>[] accepted = {
178                 SimanticsArguments.RECOVERY_POLICY_FIX_ERRORS,
179                 SimanticsArguments.ONTOLOGY_RECOVERY_POLICY_REINSTALL,
180                 SimanticsArguments.DEFAULT_WORKSPACE_LOCATION,
181                 SimanticsArguments.WORKSPACE_CHOOSER,
182                 SimanticsArguments.WORKSPACE_NO_REMEMBER,
183                 SimanticsArguments.PERSPECTIVE,
184                 SimanticsArguments.SERVER,
185                 SimanticsArguments.NEW_MODEL,
186                 SimanticsArguments.EXPERIMENT,
187                 SimanticsArguments.DISABLE_INDEX,
188                 SimanticsArguments.DATABASE_ID,
189         };
190         IArguments result = Arguments.parse(args, accepted);
191         return result;
192     }
193
194     private Object verifyArguments(IArguments args) {
195         StringBuilder report = new StringBuilder();
196
197 //        if (args.contains(SimanticsArguments.NEW_PROJECT)) {
198 //            if (args.contains(SimanticsArguments.PROJECT)) {
199 //                exclusiveArguments(report, SimanticsArguments.PROJECT, SimanticsArguments.NEW_PROJECT);
200 //            }
201 //            // Must have a server to checkout from when creating a new
202 //            // project right from the beginning.
203 //            if (!args.contains(SimanticsArguments.SERVER)) {
204 //                missingArgument(report, SimanticsArguments.SERVER);
205 //            }
206 //        } else if (args.contains(SimanticsArguments.PROJECT)) {
207 //            // To load a project, a server must be defined to checkout from
208 //            if (!args.contains(SimanticsArguments.SERVER)) {
209 //                missingArgument(report, SimanticsArguments.SERVER);
210 //            }
211 //        }
212
213         // NEW_MODEL and MODEL arguments are optional
214         // EXPERIMENT argument is optional
215
216         String result = report.toString();
217         boolean valid = result.length() == 0;
218
219         if (!valid) {
220             String msg = NLS.bind(Messages.Application_1, result);
221             MessageDialog.openInformation(null, Messages.Application_2, msg);
222         }
223         return valid ? null : EXIT_OK;
224     }
225
226 //    private void exclusiveArguments(StringBuilder sb, IArgumentFactory<?> arg1, IArgumentFactory<?> arg2) {
227 //        sb.append(NLS.bind(Messages.Application_3, arg1.getArgument(), arg2.getArgument()));
228 //        sb.append('\n');
229 //    }
230 //
231 //    private void missingArgument(StringBuilder sb, IArgumentFactory<?> arg) {
232 //        sb.append(NLS.bind(Messages.Application_0, arg.getArgument()));
233 //        sb.append('\n');
234 //    }
235
236     /*************************************************************************/
237
238     /**
239      * Creates the display used by the application.
240      * 
241      * @return the display used by the application
242      */
243     protected Display createDisplay() {
244         return PlatformUI.createDisplay();
245     }
246
247     /* (non-Javadoc)
248      * @see org.eclipse.core.runtime.IExecutableExtension#setInitializationData(org.eclipse.core.runtime.IConfigurationElement, java.lang.String, java.lang.Object)
249      */
250     @Override
251     public void setInitializationData(IConfigurationElement config,
252             String propertyName, Object data) {
253         // There is nothing to do for ProConfApplication
254     }
255
256     /**
257      * Return true if a valid workspace path has been set and false otherwise.
258      * Prompt for and set the path if possible and required.
259      * @param applicationArguments 
260      * 
261      * @return true if a valid instance location has been set and false
262      *         otherwise
263      */
264     private Object checkInstanceLocation(Shell shell, Map<?,?> applicationArguments, IArguments args) {
265         // -data @none was specified but an ide requires workspace
266         Location instanceLoc = Platform.getInstanceLocation();
267         if (instanceLoc == null) {
268             MessageDialog
269             .openError(
270                     shell,
271                     IDEWorkbenchMessages.IDEApplication_workspaceMandatoryTitle,
272                     IDEWorkbenchMessages.IDEApplication_workspaceMandatoryMessage);
273             return EXIT_OK;
274         }
275
276         // -data "/valid/path", workspace already set
277         // This information is stored in configuration/.settings/org.eclipse.ui.ide.prefs
278         if (instanceLoc.isSet()) {
279             // make sure the meta data version is compatible (or the user has
280             // chosen to overwrite it).
281             if (!checkValidWorkspace(shell, instanceLoc.getURL())) {
282                 return EXIT_OK;
283             }
284
285             // at this point its valid, so try to lock it and update the
286             // metadata version information if successful
287             try {
288                 if (instanceLoc.lock()) {
289                     writeWorkspaceVersion();
290                     return null;
291                 }
292
293                 // we failed to create the directory.
294                 // Two possibilities:
295                 // 1. directory is already in use
296                 // 2. directory could not be created
297                 File workspaceDirectory = new File(instanceLoc.getURL().getFile());
298                 if (workspaceDirectory.exists()) {
299                     if (isDevLaunchMode(applicationArguments)) {
300                         return EXIT_WORKSPACE_LOCKED;
301                     }
302                     MessageDialog.openError(
303                             shell,
304                             IDEWorkbenchMessages.IDEApplication_workspaceCannotLockTitle,
305                             IDEWorkbenchMessages.IDEApplication_workspaceCannotLockMessage);
306                 } else {
307                     MessageDialog.openError(
308                             shell,
309                             IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetTitle,
310                             IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetMessage);
311                 }
312             } catch (IOException e) {
313                 IDEWorkbenchPlugin.log("Could not obtain lock for workspace location", //$NON-NLS-1$
314                         e);
315                 MessageDialog
316                 .openError(
317                         shell,
318                         IDEWorkbenchMessages.InternalError,
319                         e.getMessage());
320             }
321             return EXIT_OK;
322         }
323
324         // -data @noDefault or -data not specified, prompt and set
325         ChooseWorkspaceData launchData = null;
326         if (args.contains(SimanticsArguments.DEFAULT_WORKSPACE_LOCATION)) {
327             launchData = new ChooseWorkspaceData(args.get(SimanticsArguments.DEFAULT_WORKSPACE_LOCATION));
328         } else {
329             launchData = new ChooseWorkspaceData(instanceLoc.getDefault());
330         }
331
332         boolean force = args.contains(SimanticsArguments.WORKSPACE_CHOOSER);
333         boolean suppressAskAgain = args.contains(SimanticsArguments.WORKSPACE_NO_REMEMBER);
334
335         while (true) {
336             URL workspaceUrl = promptForWorkspace(shell, launchData, force, suppressAskAgain);
337             if (workspaceUrl == null) {
338                 return EXIT_OK;
339             }
340
341             // if there is an error with the first selection, then force the
342             // dialog to open to give the user a chance to correct
343             force = true;
344
345             try {
346                 // the operation will fail if the url is not a valid
347                 // instance data area, so other checking is unneeded
348                 if (instanceLoc.setURL(workspaceUrl, true)) {
349                     launchData.writePersistedData();
350                     writeWorkspaceVersion();
351                     return null;
352                 }
353             } catch (IllegalStateException e) {
354                 MessageDialog
355                 .openError(
356                         shell,
357                         IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetTitle,
358                         IDEWorkbenchMessages.IDEApplication_workspaceCannotBeSetMessage);
359                 return EXIT_OK;
360             }
361
362             // by this point it has been determined that the workspace is
363             // already in use -- force the user to choose again
364             MessageDialog.openError(shell, IDEWorkbenchMessages.IDEApplication_workspaceInUseTitle,
365                     IDEWorkbenchMessages.IDEApplication_workspaceInUseMessage);
366         }
367     }
368
369     private static boolean isDevLaunchMode(Map<?,?> args) {
370         // see org.eclipse.pde.internal.core.PluginPathFinder.isDevLaunchMode()
371         if (Boolean.getBoolean("eclipse.pde.launch")) //$NON-NLS-1$
372             return true;
373         return args.containsKey("-pdelaunch"); //$NON-NLS-1$
374     }
375
376     /**
377      * Open a workspace selection dialog on the argument shell, populating the
378      * argument data with the user's selection. Perform first level validation
379      * on the selection by comparing the version information. This method does
380      * not examine the runtime state (e.g., is the workspace already locked?).
381      * 
382      * @param shell
383      * @param launchData
384      * @param force
385      *            setting to true makes the dialog open regardless of the
386      *            showDialog value
387      * @return An URL storing the selected workspace or null if the user has
388      *         canceled the launch operation.
389      */
390     private URL promptForWorkspace(Shell shell, ChooseWorkspaceData launchData,
391             boolean force, boolean suppressAskAgain) {
392         URL url = null;
393         do {
394             // okay to use the shell now - this is the splash shell
395             new ChooseWorkspaceDialog(shell, launchData, suppressAskAgain, true).prompt(force);
396             String instancePath = launchData.getSelection();
397             if (instancePath == null) {
398                 return null;
399             }
400
401             // the dialog is not forced on the first iteration, but is on every
402             // subsequent one -- if there was an error then the user needs to be
403             // allowed to fix it
404             force = true;
405
406             // 70576: don't accept empty input
407             if (instancePath.length() <= 0) {
408                 MessageDialog
409                 .openError(
410                         shell,
411                         IDEWorkbenchMessages.IDEApplication_workspaceEmptyTitle,
412                         IDEWorkbenchMessages.IDEApplication_workspaceEmptyMessage);
413                 continue;
414             }
415
416             // create the workspace if it does not already exist
417             File workspace = new File(instancePath);
418             if (!workspace.exists()) {
419                 workspace.mkdir();
420             }
421
422             try {
423                 // Don't use File.toURL() since it adds a leading slash that Platform does not
424                 // handle properly.  See bug 54081 for more details.
425                 String path = workspace.getAbsolutePath().replace(
426                         File.separatorChar, '/');
427                 url = new URL("file", null, path); //$NON-NLS-1$
428             } catch (MalformedURLException e) {
429                 MessageDialog
430                 .openError(
431                         shell,
432                         IDEWorkbenchMessages.IDEApplication_workspaceInvalidTitle,
433                         IDEWorkbenchMessages.IDEApplication_workspaceInvalidMessage);
434                 continue;
435             }
436         } while (!checkValidWorkspace(shell, url));
437
438         return url;
439     }
440
441     /**
442      * Return true if the argument directory is ok to use as a workspace and
443      * false otherwise. A version check will be performed, and a confirmation
444      * box may be displayed on the argument shell if an older version is
445      * detected.
446      * 
447      * @return true if the argument URL is ok to use as a workspace and false
448      *         otherwise.
449      */
450     private boolean checkValidWorkspace(Shell shell, URL url) {
451         // a null url is not a valid workspace
452         if (url == null) {
453             return false;
454         }
455
456         String version = readWorkspaceVersion(url);
457
458         // if the version could not be read, then there is not any existing
459         // workspace data to trample, e.g., perhaps its a new directory that
460         // is just starting to be used as a workspace
461         if (version == null) {
462             return true;
463         }
464
465         final int ide_version = Integer.parseInt(WORKSPACE_VERSION_VALUE);
466         int workspace_version = Integer.parseInt(version);
467
468         // equality test is required since any version difference (newer
469         // or older) may result in data being trampled
470         if (workspace_version == ide_version) {
471             return true;
472         }
473
474         // At this point workspace has been detected to be from a version
475         // other than the current ide version -- find out if the user wants
476         // to use it anyhow.
477                 int severity;
478                 String title;
479                 String message;
480                 if (workspace_version < ide_version) {
481                         // Workspace < IDE. Update must be possible without issues,
482                         // so only inform user about it.
483                         severity = MessageDialog.INFORMATION;
484                         title = IDEWorkbenchMessages.IDEApplication_versionTitle_olderWorkspace;
485                         message = NLS.bind(IDEWorkbenchMessages.IDEApplication_versionMessage_olderWorkspace, url.getFile());
486                 } else {
487                         // Workspace > IDE. It must have been opened with a newer IDE version.
488                         // Downgrade might be problematic, so warn user about it.
489                         severity = MessageDialog.WARNING;
490                         title = IDEWorkbenchMessages.IDEApplication_versionTitle_newerWorkspace;
491                         message = NLS.bind(IDEWorkbenchMessages.IDEApplication_versionMessage_newerWorkspace, url.getFile());
492                 }
493
494         MessageBox mbox = new MessageBox(shell, SWT.OK | SWT.CANCEL
495                 | SWT.ICON_WARNING | SWT.APPLICATION_MODAL);
496         mbox.setText(title);
497         mbox.setMessage(message);
498         return mbox.open() == SWT.OK;
499     }
500
501     /**
502      * Look at the argument URL for the workspace's version information. Return
503      * that version if found and null otherwise.
504      */
505     private static String readWorkspaceVersion(URL workspace) {
506         File versionFile = getVersionFile(workspace, false);
507         if (versionFile == null || !versionFile.exists()) {
508             return null;
509         }
510
511         try {
512             // Although the version file is not spec'ed to be a Java properties
513             // file, it happens to follow the same format currently, so using
514             // Properties to read it is convenient.
515             Properties props = new Properties();
516             FileInputStream is = new FileInputStream(versionFile);
517             try {
518                 props.load(is);
519             } finally {
520                 is.close();
521             }
522
523             return props.getProperty(WORKSPACE_VERSION_KEY);
524         } catch (IOException e) {
525             IDEWorkbenchPlugin.log("Could not read version file", new Status( //$NON-NLS-1$
526                     IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH,
527                     IStatus.ERROR,
528                     e.getMessage() == null ? "" : e.getMessage(), //$NON-NLS-1$,
529                             e));
530             return null;
531         }
532     }
533
534     /**
535      * Write the version of the metadata into a known file overwriting any
536      * existing file contents. Writing the version file isn't really crucial,
537      * so the function is silent about failure
538      */
539     private static void writeWorkspaceVersion() {
540         Location instanceLoc = Platform.getInstanceLocation();
541         if (instanceLoc == null || instanceLoc.isReadOnly()) {
542             return;
543         }
544
545         File versionFile = getVersionFile(instanceLoc.getURL(), true);
546         if (versionFile == null) {
547             return;
548         }
549
550         OutputStream output = null;
551         try {
552             String versionLine = WORKSPACE_VERSION_KEY + '='
553             + WORKSPACE_VERSION_VALUE;
554
555             output = new FileOutputStream(versionFile);
556             output.write(versionLine.getBytes("UTF-8")); //$NON-NLS-1$
557         } catch (IOException e) {
558             IDEWorkbenchPlugin.log("Could not write version file", //$NON-NLS-1$
559                     StatusUtil.newStatus(IStatus.ERROR, e.getMessage(), e));
560         } finally {
561             try {
562                 if (output != null) {
563                     output.close();
564                 }
565             } catch (IOException e) {
566                 // do nothing
567             }
568         }
569     }
570
571     /**
572      * The version file is stored in the metadata area of the workspace. This
573      * method returns an URL to the file or null if the directory or file does
574      * not exist (and the create parameter is false).
575      * 
576      * @param create
577      *            If the directory and file does not exist this parameter
578      *            controls whether it will be created.
579      * @return An url to the file or null if the version file does not exist or
580      *         could not be created.
581      */
582     private static File getVersionFile(URL workspaceUrl, boolean create) {
583         if (workspaceUrl == null) {
584             return null;
585         }
586
587         try {
588             // make sure the directory exists
589             File metaDir = new File(workspaceUrl.getPath(), METADATA_FOLDER);
590             if (!metaDir.exists() && (!create || !metaDir.mkdir())) {
591                 return null;
592             }
593
594             // make sure the file exists
595             File versionFile = new File(metaDir, VERSION_FILENAME);
596             if (!versionFile.exists()
597                     && (!create || !versionFile.createNewFile())) {
598                 return null;
599             }
600
601             return versionFile;
602         } catch (IOException e) {
603             // cannot log because instance area has not been set
604             return null;
605         }
606     }
607
608     /* (non-Javadoc)
609      * @see org.eclipse.equinox.app.IApplication#stop()
610      */
611     @Override
612     public void stop() {
613         final IWorkbench workbench = PlatformUI.getWorkbench();
614         if (workbench == null)
615             return;
616         final Display display = workbench.getDisplay();
617         display.syncExec(new Runnable() {
618             @Override
619             public void run() {
620                 if (!display.isDisposed())
621                     workbench.close();
622             }
623         });
624     }
625
626 }