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