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