Improve startup time for fresh or rollback'd session in index writing
[simantics/platform.git] / bundles / org.simantics / src / org / simantics / SimanticsPlatform.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;
13
14 import static org.simantics.db.common.utils.Transaction.commit;
15 import static org.simantics.db.common.utils.Transaction.endTransaction;
16 import static org.simantics.db.common.utils.Transaction.readGraph;
17 import static org.simantics.db.common.utils.Transaction.startTransaction;
18 import static org.simantics.db.common.utils.Transaction.writeGraph;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.time.Instant;
26 import java.time.ZoneId;
27 import java.time.format.DateTimeFormatter;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.Properties;
35 import java.util.Set;
36 import java.util.TreeMap;
37 import java.util.UUID;
38 import java.util.zip.ZipEntry;
39 import java.util.zip.ZipFile;
40
41 import org.eclipse.core.runtime.ILog;
42 import org.eclipse.core.runtime.IProduct;
43 import org.eclipse.core.runtime.IProgressMonitor;
44 import org.eclipse.core.runtime.IStatus;
45 import org.eclipse.core.runtime.NullProgressMonitor;
46 import org.eclipse.core.runtime.Platform;
47 import org.eclipse.core.runtime.Status;
48 import org.eclipse.core.runtime.SubMonitor;
49 import org.eclipse.osgi.service.resolver.BundleDescription;
50 import org.ini4j.Ini;
51 import org.ini4j.InvalidFileFormatException;
52 import org.simantics.databoard.Bindings;
53 import org.simantics.databoard.Databoard;
54 import org.simantics.datatypes.literal.Font;
55 import org.simantics.datatypes.literal.RGB;
56 import org.simantics.db.Driver;
57 import org.simantics.db.Driver.Management;
58 import org.simantics.db.Manager;
59 import org.simantics.db.ReadGraph;
60 import org.simantics.db.Resource;
61 import org.simantics.db.Session;
62 import org.simantics.db.SessionModel;
63 import org.simantics.db.UndoContext;
64 import org.simantics.db.VirtualGraph;
65 import org.simantics.db.WriteGraph;
66 import org.simantics.db.common.request.ObjectsWithType;
67 import org.simantics.db.common.request.Queries;
68 import org.simantics.db.common.request.WriteResultRequest;
69 import org.simantics.db.common.utils.Transaction;
70 import org.simantics.db.exception.ClusterSetExistException;
71 import org.simantics.db.exception.DatabaseException;
72 import org.simantics.db.exception.ResourceNotFoundException;
73 import org.simantics.db.indexing.DatabaseIndexing;
74 import org.simantics.db.layer0.genericrelation.DependenciesRelation;
75 import org.simantics.db.layer0.genericrelation.IndexException;
76 import org.simantics.db.layer0.genericrelation.IndexedRelations;
77 import org.simantics.db.layer0.util.SimanticsClipboardImpl;
78 import org.simantics.db.layer0.util.SimanticsKeys;
79 import org.simantics.db.layer0.util.TGTransferableGraphSource;
80 import org.simantics.db.layer0.variable.VariableRepository;
81 import org.simantics.db.management.SessionContext;
82 import org.simantics.db.request.Read;
83 import org.simantics.db.request.Write;
84 import org.simantics.db.service.LifecycleSupport.LifecycleListener;
85 import org.simantics.db.service.LifecycleSupport.LifecycleState;
86 import org.simantics.db.service.QueryControl;
87 import org.simantics.db.service.UndoRedoSupport;
88 import org.simantics.db.service.VirtualGraphSupport;
89 import org.simantics.db.service.XSupport;
90 import org.simantics.graph.db.GraphDependencyAnalyzer;
91 import org.simantics.graph.db.GraphDependencyAnalyzer.IU;
92 import org.simantics.graph.db.GraphDependencyAnalyzer.IdentityNode;
93 import org.simantics.graph.db.IImportAdvisor;
94 import org.simantics.graph.db.ImportResult;
95 import org.simantics.graph.db.TransferableGraphs;
96 import org.simantics.graph.diff.Diff;
97 import org.simantics.graph.diff.TransferableGraphDelta1;
98 import org.simantics.internal.Activator;
99 import org.simantics.internal.startup.StartupExtensions;
100 import org.simantics.layer0.Layer0;
101 import org.simantics.operation.Layer0X;
102 import org.simantics.project.IProject;
103 import org.simantics.project.ProjectFeatures;
104 import org.simantics.project.ProjectKeys;
105 import org.simantics.project.Projects;
106 import org.simantics.project.SessionDescriptor;
107 import org.simantics.project.exception.ProjectException;
108 import org.simantics.project.features.registry.GroupReference;
109 import org.simantics.project.management.DatabaseManagement;
110 import org.simantics.project.management.GraphBundle;
111 import org.simantics.project.management.GraphBundleEx;
112 import org.simantics.project.management.GraphBundleRef;
113 import org.simantics.project.management.PlatformUtil;
114 import org.simantics.project.management.ServerManager;
115 import org.simantics.project.management.ServerManagerFactory;
116 import org.simantics.project.management.WorkspaceUtil;
117 import org.simantics.utils.FileUtils;
118 import org.simantics.utils.datastructures.Pair;
119 import org.simantics.utils.logging.TimeLogger;
120 import org.simantics.utils.strings.EString;
121 import org.slf4j.Logger;
122 import org.slf4j.LoggerFactory;
123
124 /**
125  * SimanticsPlatform performs procedures required in order to get simantics
126  * workbench into operational state. This consists of the following steps:
127  * <ul>
128  *     <li> Asserting there is Database
129  *     </li>
130  *     <li> Starting Database process
131  *     </li>
132  *     <li> Opening a session to Database process
133  *     </li>
134  *     <li> Asserting required ontologies or other transferable graphs are installed in the database
135  *     </li>
136  *     <li> Asserting required project is installed in the database
137  *     </li>
138  *     <li> Asserting Simantics Features are installed in the database
139  *     </li>
140  *     <li> Asserting Simantics Features are installed to the project
141  *     </li>
142  *     <li> Shutdown: Save Session, Close session, Kill Database process
143  *     </li>
144  * </ul>
145  *
146  * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
147  */
148 public class SimanticsPlatform implements LifecycleListener {
149
150     private static final Logger LOGGER = LoggerFactory.getLogger(SimanticsPlatform.class);
151     
152     /**
153      * The policy is relevant when developing Simantics from Eclipse IDE.
154      * It is applied when the ontology in the database of a workspace doesn't match
155      * a newer ontology in the Eclipse workspace.
156      */
157     public static enum OntologyRecoveryPolicy { ThrowError, Merge, ReinstallDatabase }
158
159     /**
160      * This policy dictates how the Simantics platform startup should react if
161      * the started workspace is not set up properly. The alternatives are to
162      * just throw an error and fail or to attempt all possible measures to fix
163      * the encountered problems.
164      */
165     public static enum RecoveryPolicy { ThrowError, FixError }
166
167     /** Singleton instance, started in SimanticsWorkbenchAdvisor */
168     public static final SimanticsPlatform INSTANCE = new SimanticsPlatform();
169
170     /** Set to true when the Simantics Platform is in good-and-go condition */
171     public boolean running;
172
173     /** ID of the database driver that the platform is currently using */
174     private String currentDatabaseDriver;
175
176     /** Database Session */
177     public Session session;
178     private Management databasebManagement;
179
180     /** Database session context */
181     public SessionContext sessionContext;
182
183     /** Project identifier in Database */
184     public String projectURI;
185
186     /** Project name */
187     public String projectName;
188
189     /** Project resource */
190     public Resource projectResource;
191
192     /** Session specific bindings */
193     public SimanticsBindings simanticsBindings;
194     public SimanticsBindings simanticsBindings2;
195
196     public Thread mainThread;
197
198     private Thread shutdownHook = new Thread() {
199         @Override
200         public void run() {
201             try {
202                 LOGGER.warn("Simantics platform was not properly shut down. Executing safety shutdown hook.");
203                 shutdown(null, false);
204             } catch (PlatformException e) {
205                 LOGGER.error("Simantics Platform shutdown hook execution failed.", e);
206                 log.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Simantics Platform shutdown hook execution failed.", e));
207             }
208         }
209     };
210
211     /**
212      * The {@link IProject} activated by
213      * {@link #startUp(IProgressMonitor, RecoveryPolicy, OntologyRecoveryPolicy, ServerAddress, PlatformUserAgent)}
214      */
215     private IProject project;
216
217     protected ILog log;
218
219     /**
220      * Create a new simantics plaform manager in uninitialized state and
221      * with default policies. <p>
222      */
223     public SimanticsPlatform() {
224         log = Platform.getLog(Activator.getBundleContext().getBundle());
225         mainThread = Thread.currentThread();
226     }
227
228     public String getApplicationClientId() {
229         IProduct product = Platform.getProduct();
230         if(product == null) return "noProduct";//UUID.randomUUID().toString();
231         String application = product.getApplication();
232         return application != null ? application : UUID.randomUUID().toString();
233     }
234
235     private SessionDescriptor setupDatabase(String databaseDriverId, IProgressMonitor progressMonitor, RecoveryPolicy workspacePolicy, PlatformUserAgent userAgent) throws PlatformException {
236         if (progressMonitor == null)
237             progressMonitor = new NullProgressMonitor();
238         Path workspaceLocation = Platform.getLocation().toFile().toPath();
239         Path dbLocation = workspaceLocation.resolve("db");
240         Path dbIniPath = workspaceLocation.resolve("db.ini");
241         // The driver file overrides any command line arguments to prevent
242         // using the wrong driver for an existing database directory.
243         ServerManager serverManager;
244         try {
245             Ini dbIni = loadOrCreateDatabaseIni(dbIniPath, databaseDriverId);
246             databaseDriverId = dbIni.get("driver", "id");
247             serverManager = ServerManagerFactory.create(databaseDriverId, dbLocation.toAbsolutePath().toString());
248         } catch (DatabaseException | IOException e) {
249             throw new PlatformException("Failed to initialize database ServerManager with driver " + databaseDriverId, e);
250         }
251         progressMonitor.beginTask("Setting up Simantics Database", 100);
252         progressMonitor.setTaskName("Asserting Database is installed.");
253         String msg = "Failed to initialize Simantics database.";
254         try {
255             // Create database
256             log.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, "Initializing database at " + dbLocation + " with driver " + databaseDriverId));
257             progressMonitor.setTaskName("Creating database at " + dbLocation);
258             databasebManagement = serverManager.getManagement(dbLocation.toFile());
259             databasebManagement.create();
260             currentDatabaseDriver = databaseDriverId;
261             // Create layer0.
262             return serverManager.createDatabase(dbLocation.toFile());
263         } catch (DatabaseException e) {
264             throw new PlatformException(msg, e);
265         } catch (Throwable e) {
266             throw new PlatformException(msg, e);
267         } finally {
268             progressMonitor.worked(20);
269         }
270     }
271
272     public void synchronizeOntologies(IProgressMonitor progressMonitor, OntologyRecoveryPolicy ontologyPolicy, boolean requireSynchronize) throws PlatformException {
273
274         if (progressMonitor == null) progressMonitor = new NullProgressMonitor();
275
276         final DatabaseManagement mgmt = new DatabaseManagement();
277
278         PlatformUtil.compileAllDynamicOntologies();
279
280         String message = "Asserting all ontologies are installed";
281         LOGGER.info(message);
282         progressMonitor.setTaskName(message);
283         final Map<GraphBundleRef, GraphBundleEx> platformTGs = new HashMap<GraphBundleRef, GraphBundleEx>();
284         try {
285
286             // Get a list of bundles installed into the database
287             message = "find installed bundles from database";
288             progressMonitor.subTask(message);
289             LOGGER.info(message);
290             Map<GraphBundleRef, GraphBundleEx> installedTGs = new HashMap<GraphBundleRef, GraphBundleEx>();
291             for (GraphBundle b : session.syncRequest( mgmt.GraphBundleQuery )) {
292                 installedTGs.put(GraphBundleRef.of(b), GraphBundleEx.extend(b));
293             }
294
295             if(!requireSynchronize && installedTGs.size() > 1 && !Platform.inDevelopmentMode()) return;
296 //            if(installedTGs.size() > 1) return;
297
298             // Get a list of all bundles in the platform (Bundle Context)
299             message = "load all transferable graphs from platform";
300             progressMonitor.subTask(message);
301             LOGGER.info(message);
302             Collection<GraphBundle> tgs = PlatformUtil.getAllGraphs();
303             message = "extend bundles to compile versions";
304             progressMonitor.subTask(message);
305             LOGGER.info(message);
306             for (GraphBundle b : tgs) {
307                 GraphBundleEx gbe = GraphBundleEx.extend(b);
308                 gbe.build();
309                 platformTGs.put(GraphBundleRef.of(b), gbe);
310             }
311
312             // Compile a list of TGs that need to be installed or reinstalled in the database
313             message = "check bundle reinstallation demand";
314             progressMonitor.subTask(message);
315             LOGGER.info(message);
316             List<GraphBundleEx> installTGs = new ArrayList<GraphBundleEx>();
317             // Create list of TGs to update, <newTg, oldTg>
318             Map<GraphBundleEx,GraphBundleEx> reinstallTGs = new TreeMap<GraphBundleEx,GraphBundleEx>();
319             for (Entry<GraphBundleRef, GraphBundleEx> e : platformTGs.entrySet()) {
320                 GraphBundleRef key = e.getKey();
321                 GraphBundleEx platformBundle = e.getValue();
322                 GraphBundleEx existingBundle = installedTGs.get(key);
323                 
324 //                System.out.println("GraphBundleRef key=" + key.toString());
325                 
326                 if (existingBundle == null) {
327                     // Bundle did not exist in the database, put it into list of bundles to install
328                     installTGs.add(platformBundle);
329                 }
330                 else {
331                     // Bundle exists in the database
332                     boolean platformBundleIsNewer = existingBundle.getVersion().compareTo(platformBundle.getVersion())<0;
333                     if (!platformBundleIsNewer)
334                         continue;
335                     // Check hash of transferable graph to know whether to update or not.
336                     if (platformBundle.getHashcode() == existingBundle.getHashcode())
337                         continue;
338                     //System.out.println("Ontology hashcodes do not match: platform bundle="
339                     //        + platformBundle.getVersionedId() + ", hash=" + platformBundle.getHashcode()
340                     //        + "; existing bundle=" + existingBundle.getVersionedId() + ", hash=" + existingBundle.getHashcode());
341                     reinstallTGs.put(platformBundle, existingBundle);
342                 }
343             }
344             // INSTALL
345             // Database is missing graphs
346             if (!installTGs.isEmpty() || !reinstallTGs.isEmpty()) {
347                 session.getService(XSupport.class).setServiceMode(true, true);
348
349                 // Throw error
350                 if (ontologyPolicy == OntologyRecoveryPolicy.ThrowError) {
351                     StringBuilder sb = new StringBuilder("The following graphs are not installed in the database: ");
352                     if (!installTGs.isEmpty()) {
353                         int i = 0;
354                         for (GraphBundleEx e : installTGs) {
355                             if (i>0) sb.append(", ");
356                             i++;
357                             sb.append(e.toString());
358                         }
359                         sb.append(" is missing from the database.\n");
360                     }
361                     if (!reinstallTGs.isEmpty()) {
362                         int i = 0;
363                         for (Entry<GraphBundleEx, GraphBundleEx> e : reinstallTGs.entrySet()) {
364                             if (i>0) sb.append(", ");
365                             i++;
366                             sb.append(e.getKey().toString());
367                         }
368                         sb.append(" Database/Platform Bundle version mismatch.\n");
369                     }
370                     sb.append("Hint: Use -fixErrors to install the graphs.");
371                     throw new PlatformException(sb.toString());
372                 }
373                 // Reinstall database
374                 if (ontologyPolicy == OntologyRecoveryPolicy.ReinstallDatabase) {
375                     log.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, "Reinstalling the database."));
376                     // TODO Install DB
377                     // Stop Session
378                     // Kill Process
379                     // Delete Database
380                     // Create Database
381                     // Start Database
382                     // Open Session
383                     // Install TGs
384                     throw new PlatformException("Reinstalling Database, NOT IMPLEMENTED");
385                 }
386
387                 if (ontologyPolicy == OntologyRecoveryPolicy.Merge) {
388                     message = "Merging ontology changes";
389                     progressMonitor.subTask(message);
390                     LOGGER.info(message);
391                     // Sort missing TGs into install order
392                     GraphDependencyAnalyzer<GraphBundle> analyzer = new GraphDependencyAnalyzer<GraphBundle>();
393                     for(GraphBundle tg : installTGs) analyzer.addGraph(tg, tg.getGraph());
394                     for(GraphBundle tg : reinstallTGs.keySet()) analyzer.addGraph(tg, tg.getGraph());
395                     if(!analyzer.analyzeDependency()) {
396                         Collection<Pair<GraphBundle, GraphBundle>> problems = analyzer.getConflicts();
397                         StringBuilder sb = new StringBuilder();
398                         for (Pair<GraphBundle, GraphBundle> problem : problems) {
399                             sb.append("Conflict with "+problem.first+" and "+problem.second+".\n");
400                         }
401                         throw new PlatformException(sb.toString());
402                     }
403                     else if(!session.syncRequest( analyzer.queryExternalDependenciesSatisfied )) {
404                         Collection<IdentityNode> unsatisfiedDependencies = analyzer.getUnsatisfiedDependencies();
405                         StringBuilder sb = new StringBuilder();
406                         for (IdentityNode dep: unsatisfiedDependencies) {
407                             sb.append("Unsatisfied Dependency "+dep+". Required by\n");
408                             for(IU iu : GraphDependencyAnalyzer.toCollection(dep.getRequires())) {
409                                 sb.append("    " + ((GraphBundle)iu.getId()).getId() + "\n");
410                             }
411                         }
412                         throw new PlatformException(sb.toString());
413                     }
414
415                     List<GraphBundle> sortedBundles = analyzer.getSortedGraphs();
416                     if(!sortedBundles.isEmpty()) {
417
418                         session.syncRequest((Write) graph -> {
419                             try {
420                                 graph.newClusterSet(graph.getRootLibrary());
421                             } catch (ClusterSetExistException e) {
422                                 // Cluster set exist already, no problem.
423                             }
424                             graph.setClusterSet4NewResource(graph.getRootLibrary());
425                             graph.flushCluster();
426                         });
427
428                         boolean mergedOntologies = false;
429
430                         // Install TGs
431                         for(final GraphBundle tg : sortedBundles) {
432
433                                 final IImportAdvisor advisor = new OntologyImportAdvisor(tg, mgmt);
434                                 final GraphBundle oldTG = reinstallTGs.get(tg);
435
436                                 boolean createImmutable = tg.getImmutable();
437
438                                 if (oldTG==null) {
439
440                                 session.getService(XSupport.class).setServiceMode(true, createImmutable);
441
442                                         // Install TG
443                                         log.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, "Installing "+tg.toString()+" - "+tg.getName()));
444                                         ImportResult result = TransferableGraphs.importGraph1(session, new TGTransferableGraphSource(tg.getGraph()), advisor, null);
445                                         if (!result.missingExternals.isEmpty()) {
446                                                 log.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Import of " + tg.toString() + " was missing the following external entities:\n" + EString.implode(result.missingExternals)));
447                                         }
448                                 } else {
449                                         if(!createImmutable)
450                                                 continue;
451
452                                         // Merge TG
453                                         startTransaction(session, false);
454                                         TransferableGraphDelta1 delta = new Diff(oldTG.getGraph(), tg.getGraph()).diff();
455                                         final long[] oldResources = oldTG.getResourceArray();
456                                         boolean changes = TransferableGraphs.hasChanges(readGraph(), oldResources, delta);
457                                         endTransaction();
458                                         if (!changes) {
459                                             //log.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, "Nothing to merge for "+tg.toString()));
460                                             continue;
461                                         }
462
463                                 log.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, "Merging new version of "+tg.toString()));
464
465                                         startTransaction(session, true);
466                                         
467                                         //delta.print();
468                                         try {
469                                                 
470                                                 
471                                                 long[] resourceArray = TransferableGraphs.applyDelta(writeGraph(), oldResources, delta);
472                                                 tg.setResourceArray(resourceArray);
473                                                 mgmt.setGraphBundleEntry(tg);
474                                                 commit();
475                                                 mergedOntologies = true;
476                                         } catch (Throwable t) {
477                                                 throw new PlatformException(t);
478                                         } finally {
479                                                 endTransaction();
480                                         }
481                                 }
482                         }
483
484                         session.syncRequest((Write) graph -> {
485                             graph.setClusterSet4NewResource(graph.getRootLibrary());
486                             graph.flushCluster();
487                         });
488
489                         if (mergedOntologies)
490                             DatabaseIndexing.deleteAllIndexes();
491                     }
492
493                     TimeLogger.log("Ontologies synchronized.");
494                     
495                 }
496                 session.getService(XSupport.class).setServiceMode(false, false);
497             }
498             progressMonitor.worked(20);
499         } catch (IOException e) {
500             throw new PlatformException(e);
501         } catch (DatabaseException e) {
502             throw new PlatformException(e);
503         }
504
505     }
506
507     public boolean assertConfiguration(IProgressMonitor progressMonitor, RecoveryPolicy workspacePolicy) throws PlatformException {
508
509         if (progressMonitor == null) progressMonitor = new NullProgressMonitor();
510
511         File workspaceLocation = Platform.getLocation().toFile();
512
513         boolean installProject = false;
514         progressMonitor.setTaskName("Asserting simantics.cfg is installed");
515         try {
516             File propertyFile = new File(workspaceLocation, "simantics.cfg");
517             Properties properties;
518             try {
519                 properties = WorkspaceUtil.readProperties(propertyFile);
520             } catch (IOException e) {
521                 if (workspacePolicy == RecoveryPolicy.ThrowError) throw new PlatformException("Could not load "+propertyFile);
522
523                 // Create a project and write Property file
524                 properties = new Properties();
525                 properties.setProperty("project_uri", "http://Projects/Development%20Project");
526                 properties.setProperty("project_name", "Development Project");
527                 WorkspaceUtil.writeProperties(propertyFile, properties);
528                 installProject |= true;
529             }
530             projectURI = properties.getProperty("project_uri");
531             projectName = properties.getProperty("project_name");
532             progressMonitor.worked(10);
533         } catch (IOException e) {
534             throw new PlatformException(e);
535         }
536
537         return installProject;
538
539     }
540
541     public boolean assertProject(IProgressMonitor progressMonitor, RecoveryPolicy workspacePolicy, boolean installProject) throws PlatformException {
542
543         SubMonitor monitor = SubMonitor.convert(progressMonitor, 10);
544
545         final DatabaseManagement mgmt = new DatabaseManagement();
546
547         monitor.setTaskName("Asserting project resource exists in the database");
548         try {
549             projectResource = session.syncRequest( Queries.resource( projectURI ) );
550         } catch (ResourceNotFoundException nfe) {
551             // Project was not found
552             if (workspacePolicy == RecoveryPolicy.ThrowError)
553                 throw new PlatformException("Project Resource "+projectURI+" is not found in the database.");
554             // Create empty project with no features
555             try {
556                 Transaction.startTransaction(session, true);
557                 try {
558                     // The project needs to be created mutable.
559                     session.getService(XSupport.class).setServiceMode(true, false);
560
561                     ArrayList<String> empty = new ArrayList<String>();
562                     projectResource = mgmt.createProject(projectName, empty);
563                     installProject |= true;
564
565                     session.getService(XSupport.class).setServiceMode(false, false);
566                     Transaction.commit();
567                 } finally {
568                     Transaction.endTransaction();
569                 }
570                 //session.getService( LifecycleSupport.class ).save();
571             } catch (DatabaseException e) {
572                 throw new PlatformException("Failed to create "+projectURI, e);
573             }
574         } catch (DatabaseException e) {
575             throw new PlatformException("Failed to create "+projectURI, e);
576         }
577         monitor.worked(10);
578
579         return installProject;
580
581     }
582
583     public void updateInstalledGroups(IProgressMonitor progressMonitor, boolean installProject) throws PlatformException {
584
585         if (installProject)
586         {
587             // Attach all feature groups available in platform to created project
588             progressMonitor.setTaskName("Install all features");
589             Set<GroupReference> publishedFeatureGroups = ProjectFeatures.getInstallGroupsOfPublishedFeatures();
590             Collection<GroupReference> groupsWithoutVersion = GroupReference.stripVersions(publishedFeatureGroups);
591
592             try {
593                 session.syncRequest(
594                         (Write) graph ->
595                         Projects.setProjectInstalledGroups(graph, projectResource, groupsWithoutVersion));
596             } catch (DatabaseException ae) {
597                 throw new PlatformException("Failed to install features", ae);
598             }
599             progressMonitor.worked(10);
600         }
601
602     }
603
604     public void assertSessionModel(IProgressMonitor progressMonitor) throws PlatformException {
605
606         Properties properties = session.getService(Properties.class);
607         final String clientId = properties.getProperty("clientId");
608
609         try {
610
611             // Currently this needs to be done before data becomes available
612             VirtualGraphSupport support = session.getService(VirtualGraphSupport.class);
613             VirtualGraph activations = support.getWorkspacePersistent("activations");
614
615             Resource sessionModel = session.syncRequest(new Read<Resource>() {
616
617                 @Override
618                 public Resource perform(ReadGraph graph) throws DatabaseException {
619
620                     Layer0X L0X = Layer0X.getInstance(graph);
621                     for(Resource sessionModel : graph.syncRequest(new ObjectsWithType(graph.getRootLibrary(), L0X.HasSession, L0X.Session))) {
622                         String id = graph.getPossibleRelatedValue(sessionModel, L0X.Session_HasClientId);
623                         if(id != null && id.equals(clientId)) return sessionModel;
624                     }
625                     return null;
626
627                 }
628
629             });
630
631             if(sessionModel == null) {
632
633                 sessionModel = session.syncRequest(new WriteResultRequest<Resource>(activations) {
634
635                     @Override
636                     public Resource perform(WriteGraph graph) throws DatabaseException {
637                         Layer0 L0 = Layer0.getInstance(graph);
638                         Layer0X L0X = Layer0X.getInstance(graph);
639                         Resource session = graph.newResource();
640                         graph.claim(session, L0.InstanceOf, null, L0X.Session);
641                         graph.claim(session, L0X.Session_HasUser, null, graph.getResource("http://Users/AdminUser"));
642                         graph.addLiteral(session, L0X.Session_HasClientId, L0X.Session_HasClientId_Inverse, clientId, Bindings.STRING);
643                         graph.claim(graph.getRootLibrary(), L0X.HasSession, session);
644                         return session;
645                     }
646                 });
647
648             }
649
650             session.registerService(SessionModel.class, new PlatformSessionModel(sessionModel));
651         } catch (DatabaseException e) {
652             throw new PlatformException(e);
653         }
654
655     }
656
657     static class PlatformSessionModel implements SessionModel {
658         private final Resource sessionModel;
659
660         public PlatformSessionModel(Resource model) {
661             this.sessionModel = model;
662         }
663
664         @Override
665         public Resource getResource() {
666             return sessionModel;
667         }
668     }
669
670     public void resetDatabase(IProgressMonitor monitor) throws PlatformException {
671         File dbLocation = Platform.getLocation().append("db").toFile();
672         if(!dbLocation.exists()) return;
673         try { // Load driver
674             Driver driver = Manager.getDriver("procore");
675             Management management = driver.getManagement(dbLocation.getAbsolutePath(), null);
676             management.delete();
677         } catch (DatabaseException e) {
678             throw new PlatformException("Failed to remove database at " + dbLocation.getAbsolutePath(), e);
679         }
680         // We have created extra files to database folder which have to be deleted also.
681         // This is an awful idea! Do not create extra files to database folder!
682         Throwable t = null;
683         for (int i=0; i<10; ++i) {
684             try {
685                 FileUtils.deleteAll(dbLocation);
686                 t = null;
687                 break;
688             } catch (IOException e) {
689                 // Assuming this has been thrown because delete file/folder failed.
690                 t = e;
691             }
692             try {
693                 Thread.sleep(200);
694             } catch (InterruptedException e) {
695                 // Ignoring interrupted exception.
696             }
697         }
698         if (null != t)
699             throw new PlatformException("Failed to remove database folder at " + dbLocation.getAbsolutePath(), t);
700     }
701     public void resetWorkspace(IProgressMonitor monitor, ArrayList<String> fileFilter) throws PlatformException, IllegalStateException, IOException {
702         File file = Platform.getLocation().toFile();
703         if (null != fileFilter)
704             FileUtils.deleteAllWithFilter(file , fileFilter);
705         resetDatabase(monitor);
706     }
707
708     public boolean handleBaselineDatabase() throws PlatformException {
709         Path workspaceLocation = Platform.getLocation().toFile().toPath();
710         Path baselineIndicatorFile = workspaceLocation.resolve(".baselined");
711         if (Files.isRegularFile(baselineIndicatorFile)) {
712             // This means that the workspace has already been initialized from
713             // a database baseline and further initialization is not necessary.
714             return true;
715         }
716
717         String dbBaselineArchive = System.getProperty("org.simantics.db.baseline", null);
718         if (dbBaselineArchive == null)
719             return false;
720
721         Path baseline = Paths.get(dbBaselineArchive);
722         if (!Files.isRegularFile(baseline))
723             throw new PlatformException("Specified database baseline archive " + baseline + " does not exist. Cannot initialize workspace database.");
724
725         validateBaselineFile(baseline);
726         validateWorkspaceForBaselineInitialization(workspaceLocation);
727
728         try {
729             Files.createDirectories(workspaceLocation);
730             FileUtils.extractZip(baseline.toFile(), workspaceLocation.toFile());
731             Files.write(baselineIndicatorFile, baselineIndicatorContents(baselineIndicatorFile));
732             return true;
733         } catch (IOException e) {
734             throw new PlatformException(e);
735         }
736     }
737
738     private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("d. MMM yyyy HH:mm:ss");
739
740     private static byte[] baselineIndicatorContents(Path path) throws IOException {
741         return String.format("%s%n%s%n",
742                 path.toString(),
743                 Instant.now().atZone(ZoneId.systemDefault()).format(TIMESTAMP_FORMAT))
744                 .getBytes("UTF-8");
745     }
746
747     private void validateWorkspaceForBaselineInitialization(Path workspaceLocation) throws PlatformException {
748         try {
749             Path db = workspaceLocation.resolve("db");
750             if (Files.exists(db))
751                 throw new PlatformException("Database location " + db + " already exists. Cannot re-initialize workspace from baseline.");
752             Path index = workspaceLocation.resolve(".metadata/.plugins/org.simantics.db.indexing");
753             if (!Files.exists(index) || !isEmptyDirectory(index))
754                 throw new PlatformException("Index location " + index + " already exists. Cannot re-initialize workspace from baseline.");
755         } catch (IOException e) {
756             throw new PlatformException("Failed to validate workspace for baseline initialization", e);
757         }
758     }
759
760     private static boolean isEmptyDirectory(Path dir) throws IOException {
761         return Files.walk(dir).count() == 1;
762     }
763
764     private void validateBaselineFile(Path baseline) throws PlatformException {
765         try (ZipFile zip = new ZipFile(baseline.toFile())) {
766             ZipEntry db = zip.getEntry("db");
767             if (db == null)
768                throw new PlatformException("Baseline archive does not contain database directory 'db'");
769             ZipEntry index = zip.getEntry(".metadata/.plugins/org.simantics.db.indexing");
770             if (index == null)
771                 throw new PlatformException("Baseline archive does not contain database index directory '.metadata/.plugins/org.simantics.db.indexing'");
772         } catch (IOException e) {
773             throw new PlatformException("Failed to validate baseline archive " + baseline, e);
774         }
775     }
776
777     /**
778      * Start-up the platform. The procedure consists of 8 steps. Once everything
779      * is up and running, all fields are set property.
780      * <p>
781      *
782      * If workspacePolicy is FixErrors, there is an attempt to fix unexpected
783      * errors. It includes installing database files, installing ontologies, and
784      * installing project features.
785      * <p>
786      *
787      * In Simantics Workbench this is handled in
788      * <code>SimanticsWorkbenchAdvisor#openWindows()</code>.
789      * <p>
790      *
791      * If remote server is given, simantics plaform takes connection there
792      * instead of local server at "db/".
793      *
794      * @param workspacePolicy action to take on workspace/database related
795      *        errors
796      * @param ontologyPolicy action to take on ontology mismatch
797      * @param progressMonitor optional progress monitor
798      * @param userAgent interface for resorting to user feedback during platform
799      *        startup or <code>null</code> to resort to default measures
800      * @throws PlatformException
801      */
802     public synchronized SessionContext startUp(String databaseDriverId, IProgressMonitor progressMonitor, RecoveryPolicy workspacePolicy,
803             OntologyRecoveryPolicy ontologyPolicy, boolean requireSynchronize, PlatformUserAgent userAgent)
804     throws PlatformException
805     {
806
807         assert(!running);
808         TimeLogger.log("Beginning of SimanticsPlatform.startUp");
809
810         LOGGER.info("Beginning of SimanticsPlatform.startUp");
811
812         SubMonitor monitor = SubMonitor.convert(progressMonitor, 1000);
813
814         // For debugging on what kind of platform automatic tests are running in
815         // case there are problems.
816         if ("true".equals(System.getProperty("org.simantics.dumpBundleState")))
817             dumpPlatformBundleState();
818
819         // 0. Consult all startup extensions before doing anything with the workspace.
820         StartupExtensions.consultStartupExtensions();
821         TimeLogger.log("Consulted platform pre-startup extensions");
822
823         // 0.1. Clear all temporary files
824         Simantics.clearTemporaryDirectory();
825         TimeLogger.log("Cleared temporary directory");
826
827         // 0.2 Clear VariableRepository.repository static map which holds references to SessionImplDb
828         VariableRepository.clear();
829         
830         // 0.3 Handle baseline database before opening db
831         boolean usingBaseline = handleBaselineDatabase();
832         
833         // 1. Assert there is a database at <workspace>/db
834         SessionDescriptor sessionDescriptor = setupDatabase(databaseDriverId, monitor.newChild(200, SubMonitor.SUPPRESS_NONE), workspacePolicy, userAgent);
835         session = sessionDescriptor.getSession();
836         TimeLogger.log("Database setup complete");
837         
838         // 1.1 Delete all indexes if we cannot be certain they are up-to-date
839         //     A full index rebuild will be done later, before project activation.
840         XSupport support = session.getService(XSupport.class);
841         if (support.rolledback()) {
842             try {
843                 DatabaseIndexing.deleteAllIndexes();
844             } catch (IOException e) {
845                 throw new PlatformException(e);
846             }
847         }
848         
849         // 2. Assert all graphs, and correct versions, are installed to the database
850         if(!usingBaseline) {
851             synchronizeOntologies(monitor.newChild(400, SubMonitor.SUPPRESS_NONE), ontologyPolicy, requireSynchronize);
852             TimeLogger.log("Synchronized ontologies");
853         }
854
855         // 4. Assert simantics.cfg exists
856         boolean installProject = assertConfiguration(monitor.newChild(25, SubMonitor.SUPPRESS_NONE),workspacePolicy);
857
858         // 5. Assert Project Resource is installed in the database
859         installProject = assertProject(monitor.newChild(25, SubMonitor.SUPPRESS_NONE), workspacePolicy, installProject);
860
861         // 6. Install all features into project, if in debug mode
862         updateInstalledGroups(monitor.newChild(25), true); //installProject);
863         TimeLogger.log("Installed all features into project");
864
865         // 7. Assert L0.Session in database for this session
866         assertSessionModel(monitor.newChild(25, SubMonitor.SUPPRESS_NONE));
867
868         session.getService(XSupport.class).setServiceMode(false, false);
869
870         try {
871             monitor.setTaskName("Flush query cache");
872             session.syncRequest((Write) graph -> {
873                 QueryControl qc = graph.getService(QueryControl.class);
874                 qc.flush(graph);
875             });
876             TimeLogger.log("Flushed queries");
877         } catch (DatabaseException e) {
878             LOGGER.error("Flushing queries failed.", e);
879         }
880         boolean loadProject = true;
881         try {
882
883             monitor.setTaskName("Open database session");
884                 sessionContext = SimanticsPlatform.INSTANCE.createSessionContext(true);
885                 // This must be before setSessionContext since some listeners might query this
886             sessionContext.setHint(SimanticsKeys.KEY_PROJECT, SimanticsPlatform.INSTANCE.projectResource);
887
888             Simantics.setSessionContext(sessionContext);
889
890             // 1. Put ResourceBinding that throws an exception to General Bindings
891             simanticsBindings = new SimanticsBindings( null );
892             Bindings.classBindingFactory.addFactory( simanticsBindings );
893
894
895             // 2. Create session-specific second Binding context (Databoard) and
896             //    put that to Session as a service
897             Session session = sessionContext.getSession();
898             Databoard sessionDataboard = new Databoard();
899             session.registerService(Databoard.class, sessionDataboard);
900             simanticsBindings2 = new SimanticsBindings( session );
901             sessionDataboard.classBindingFactory.addFactory( simanticsBindings2 );
902
903             // Register datatype bindings
904             Bindings.defaultBindingFactory.getRepository().put(RGB.Integer.BINDING.type(), RGB.Integer.BINDING);
905             Bindings.defaultBindingFactory.getRepository().put(Font.BINDING.type(), Font.BINDING);
906
907             if (support.rolledback() || sessionDescriptor.isFreshDatabase()) {
908                 monitor.setTaskName("Rebuilding all indexes");
909                 try {
910                     session.getService(IndexedRelations.class).fullRebuild(monitor.newChild(100), session);
911                 } catch (IndexException e) {
912                     LOGGER.error("Failed to re-build all indexes", e);
913                 }
914             } else {
915                 monitor.worked(100);
916             }
917
918             if(loadProject) {
919                 TimeLogger.log("Load projects");
920                 project = Projects.loadProject(sessionContext.getSession(), SimanticsPlatform.INSTANCE.projectResource);
921                 monitor.worked(100);
922
923                 sessionContext.setHint(ProjectKeys.KEY_PROJECT, project);
924                 TimeLogger.log("Loading projects complete");
925
926                 project.activate();
927                 TimeLogger.log("Project activated");
928                 monitor.worked(100);
929             }
930
931         } catch (DatabaseException e) {
932             LOGGER.error("Platform startup failed.", e);
933             throw new PlatformException(e);
934         } catch (ProjectException e) {
935             boolean hasStackTrace = e.getStackTrace().length > 0;
936             if (!hasStackTrace)
937                 throw new PlatformException(e.getMessage(), hasStackTrace);
938             throw new PlatformException(e, hasStackTrace);
939         }
940
941         running = true;
942
943         // #7650: improve shutdown robustness in all applications that use the platform
944         Runtime.getRuntime().addShutdownHook(shutdownHook);
945
946         // Discard database session undo history at this point to prevent
947         // the user from undoing any initialization operations performed
948         // by the platform startup.
949         SimanticsPlatform.INSTANCE.discardSessionUndoHistory();
950         TimeLogger.log("Discarded session undo history");
951
952         return sessionContext;
953
954     }
955
956     public SessionContext createSessionContext(boolean init) throws PlatformException {
957         try {
958             // Construct and initialize SessionContext from Session.
959             SessionContext sessionContext = SessionContext.create(session, init);
960             TimeLogger.log("Session context created");
961             if (init) {
962                 sessionContext.registerServices();
963                 TimeLogger.log("Session services registered");
964             }
965             return sessionContext;
966         } catch (DatabaseException e) {
967             throw new PlatformException(e);
968         }
969     }
970
971     /**
972      * Perform normal shutdown for the Simantics Platform.
973      *
974      * @param progressMonitor optional progress monitor
975      * @throws PlatformException
976      * @see {@link #shutdown(IProgressMonitor, boolean)}
977      */
978     public synchronized void shutdown(IProgressMonitor progressMonitor) throws PlatformException {
979         shutdown(progressMonitor, true);
980     }
981
982     /**
983      * Shutdown Simantics Platform.
984      *
985      * In Simantics Workbench this is handled in
986      * <code>SimanticsWorkbenchAdvisor#disconnectFromWorkspace</code>.
987      *
988      * @param progressMonitor
989      *            optional progress monitor
990      * @param clearTemporaryFiles
991      *            allow or prevent deletion of temporary files at the end of the
992      *            shutdown procedure
993      * @throws PlatformException
994      */
995     public synchronized void shutdown(IProgressMonitor progressMonitor, boolean clearTemporaryFiles) throws PlatformException
996     {
997         SubMonitor progress = SubMonitor.convert(progressMonitor, 100);
998         PlatformException platformException = null;
999         try {
1000             progress.subTask("Close Project");
1001             if (project != null) {
1002                 project.safeDispose();
1003             }
1004             progress.worked(10);
1005
1006             running = false;
1007             progress.subTask("Close Database Session");
1008             Databoard databoard = null;
1009             if (sessionContext != null) {
1010                 Session s = sessionContext.peekSession();
1011                 if (s != null) {
1012                     databoard = s.peekService(Databoard.class);
1013
1014                     progress.subTask("Flushing Index Caches");
1015                     try {
1016                         Simantics.flushIndexCaches(progress.newChild(20), s);
1017                     } catch (Throwable t) {
1018                         LOGGER.error("Failed to flush index caches.", t);
1019                     }
1020                 }
1021
1022                 progress.subTask("Close Database Session");
1023                 sessionContext.safeDispose();
1024                 sessionContext = null;
1025                 Simantics.setSessionContext(null);
1026             }
1027             if (simanticsBindings != null) {
1028                 Bindings.classBindingFactory.removeFactory( simanticsBindings );
1029                 simanticsBindings = null;
1030             }
1031             if (databoard != null) {
1032                 if (simanticsBindings2 != null) {
1033                         databoard.classBindingFactory.removeFactory( simanticsBindings2 );
1034                         simanticsBindings2 = null;
1035                 }
1036                 databoard.clear();
1037             }
1038
1039             // Make sure Simantics clipboard doesn't store unwanted session data references.
1040             Simantics.setClipboard(new SimanticsClipboardImpl());
1041
1042             progress.worked(30);
1043
1044             session = null;
1045             projectResource = null;
1046             currentDatabaseDriver = null;
1047
1048             DependenciesRelation.assertFinishedTracking();
1049
1050         } catch (Exception e) {
1051             platformException = new PlatformException("Failed to shutdown Simantics Platform", e);
1052         }
1053
1054         progress.worked(10);
1055         progress.subTask("Shutting down database");
1056         try {
1057             if (null != databasebManagement)
1058                 databasebManagement.shutdown();
1059         } catch (Throwable t) {
1060             LOGGER.error("Database shutdown failed.", t);
1061         }
1062         progress.worked(10);
1063
1064         if (clearTemporaryFiles) {
1065             progress.subTask("Clearing Workspace Temporary Directory");
1066             try {
1067                 Simantics.clearTemporaryDirectory();
1068             } catch (Throwable t) {
1069                 LOGGER.error("Failed to clear the temporary directory.", t);
1070             }
1071         }
1072         progress.worked(10);
1073         if (null != platformException)
1074             throw platformException;
1075
1076         // #7650: improve shutdown robustness in all applications that use the platform
1077         Runtime.getRuntime().removeShutdownHook(shutdownHook);
1078     }
1079
1080     // TODO: consider removing this in the future ??
1081     @Override
1082     public void stateChanged(LifecycleState newState) {
1083         if(newState == LifecycleState.CLOSED) {
1084             if(running) {
1085                 if(Platform.isRunning()) {
1086                     mainThread.interrupt();
1087                 }
1088             }
1089         }
1090     }
1091
1092     /**
1093      * @return <code>true</code> if discard was successful, <code>false</code>
1094      *         if there was no session, {@link UndoRedoSupport} or
1095      *         {@link UndoContext} to discard through
1096      */
1097     public boolean discardSessionUndoHistory() {
1098         Session s = session;
1099         if (s != null) {
1100             UndoRedoSupport urs = s.peekService(UndoRedoSupport.class);
1101             if (urs != null) {
1102                 UndoContext uc = urs.getUndoContext(s);
1103                 if (uc != null) {
1104                     uc.clear();
1105                     return true;
1106                 }
1107             }
1108         }
1109         return false;
1110     }
1111
1112     public void reconnect(String databaseDriverId) throws Exception {
1113         // Starts database server.
1114         if (currentDatabaseDriver != null)
1115             databaseDriverId = currentDatabaseDriver;
1116         SimanticsPlatform.INSTANCE.startUp(databaseDriverId, null, RecoveryPolicy.ThrowError, OntologyRecoveryPolicy.ThrowError, true, null);
1117     }
1118
1119     private void dumpPlatformBundleState() {
1120         BundleDescription[] bs = Platform.getPlatformAdmin().getState().getBundles();
1121         System.out.println("Total bundles: " + bs.length);
1122         for (BundleDescription b : bs) {
1123             System.out.format("%-80s @ %s\n", b.toString(), b.getLocation());
1124         }
1125     }
1126
1127     private Ini loadOrCreateDatabaseIni(Path path, String databaseDriverId)
1128             throws InvalidFileFormatException, IOException
1129     {
1130         File f = path.toFile();
1131         Ini dbIni = Files.isRegularFile(path) ? new Ini(f) : new Ini();
1132         String iniId = dbIni != null ? dbIni.get("driver", "id") : null;
1133         if (iniId == null) {
1134             dbIni.put("driver", "id", databaseDriverId);
1135             dbIni.store(f);
1136         }
1137         return dbIni;
1138     }
1139
1140 }