]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.modeling/src/org/simantics/modeling/typicals/SyncTypicalTemplatesToInstances.java
Sync git svn branch with SVN repository r33319.
[simantics/platform.git] / bundles / org.simantics.modeling / src / org / simantics / modeling / typicals / SyncTypicalTemplatesToInstances.java
index cd326051371169ad70b442b8b986300c336cbce2..83407bb04cacafb34e35261a54798d7e7a167b02 100644 (file)
@@ -50,6 +50,7 @@ import org.simantics.diagram.handler.CopyPasteStrategy;
 import org.simantics.diagram.handler.ElementObjectAssortment;\r
 import org.simantics.diagram.handler.PasteOperation;\r
 import org.simantics.diagram.handler.Paster;\r
+import org.simantics.diagram.handler.Paster.RouteLine;\r
 import org.simantics.diagram.stubs.DiagramResource;\r
 import org.simantics.diagram.synchronization.CollectingModificationQueue;\r
 import org.simantics.diagram.synchronization.CopyAdvisor;\r
@@ -204,9 +205,9 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
      */\r
     protected Map<Object, Object>            copyMap;\r
 \r
-    final private Map<Resource, List<String>> messageLogs = new HashMap<Resource, List<String>>();\r
+    final private Map<Resource, List<String>> messageLogs = new HashMap<>();\r
     \r
-    public List<Resource> logs = new ArrayList<Resource>();\r
+    public List<Resource> logs = new ArrayList<>();\r
 \r
        private boolean writeLog;\r
 \r
@@ -293,7 +294,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
        if(indexRoot == null) throw new DatabaseException("FATAL: Diagram is not under any index root.");\r
        List<String> log = messageLogs.get(indexRoot);\r
        if(log == null) {\r
-               log = new ArrayList<String>();\r
+               log = new ArrayList<>();\r
                messageLogs.put(indexRoot, log);\r
        }\r
        return log;\r
@@ -329,7 +330,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         this.syncCtx.set(ModelingSynchronizationHints.MODELING_RESOURCE, ModelingResources.getInstance(graph));\r
 \r
         this.metadata = new TypicalSynchronizationMetadata();\r
-        this.metadata.synchronizedTypicals = new ArrayList<Resource>();\r
+        this.metadata.synchronizedTypicals = new ArrayList<>();\r
 \r
         this.temporaryDiagram = Diagram.spawnNew(DiagramClass.DEFAULT);\r
         this.temporaryDiagram.setHint(SynchronizationHints.CONTEXT, syncCtx);\r
@@ -363,7 +364,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
                 Collection<Resource> libs = graph.syncRequest(new ObjectsWithType(indexRoot, L0.ConsistsOf, DOC.DocumentLibrary));\r
                 if(libs.isEmpty()) continue;\r
 \r
-                List<NamedResource> nrs = new ArrayList<NamedResource>();\r
+                List<NamedResource> nrs = new ArrayList<>();\r
                 for(Resource lib : libs) nrs.add(new NamedResource(NameUtils.getSafeName(graph, lib), lib));\r
                 Collections.sort(nrs, AlphanumComparator.CASE_INSENSITIVE_COMPARATOR);\r
                 Resource library = nrs.iterator().next().getResource();\r
@@ -414,7 +415,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         if (instances.isEmpty())\r
             return;\r
 \r
-        Set<Resource> templateElements = new THashSet<Resource>( graph.syncRequest(\r
+        Set<Resource> templateElements = new THashSet<>( graph.syncRequest(\r
                 new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) );\r
 \r
         try {\r
@@ -435,7 +436,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         if (template == null)\r
             return;\r
 \r
-        Set<Resource> templateElements = new THashSet<Resource>( graph.syncRequest(\r
+        Set<Resource> templateElements = new THashSet<>( graph.syncRequest(\r
                 new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) );\r
 \r
         try {\r
@@ -498,7 +499,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         // therefore clone the query result.\r
         typicalInfoBean = (TypicalInfoBean) typicalInfoBean.clone();\r
         typicalInfoBean.templateElements = currentTemplateElements;\r
-        typicalInfoBean.auxiliary = new HashMap<Object, Object>(1);\r
+        typicalInfoBean.auxiliary = new HashMap<>(1);\r
 \r
         TypicalInfo info = new TypicalInfo();\r
         info.monitor = monitor;\r
@@ -522,12 +523,12 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         // instance elements that do not have a MOD.HasElementSource\r
         // relation but have a MOD.IsTemplatized tag.\r
         Set<Resource> instanceElementsRemovedFromTemplate = findInstanceElementsRemovedFromTemplate(\r
-                graph, info, new THashSet<Resource>(dSizeAbs));\r
+                graph, info, new THashSet<>(dSizeAbs));\r
 \r
         // Find elements in template that do not yet exist in the instance\r
         Set<Resource> templateElementsAddedToTemplate = findTemplateElementsMissingFromInstance(\r
                 graph, currentTemplateElements, info,\r
-                new THashSet<Resource>(dSizeAbs));\r
+                new THashSet<>(dSizeAbs));\r
 \r
         Set<Resource> changedTemplateElements = changedElementsByDiagram.removeValues(template);\r
 \r
@@ -624,7 +625,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
 \r
         ElementObjectAssortment assortment = new ElementObjectAssortment(graph, elementsAddedToTemplate);\r
         if (copyMap == null)\r
-            copyMap = new THashMap<Object, Object>();\r
+            copyMap = new THashMap<>();\r
         else\r
             copyMap.clear();\r
 \r
@@ -665,7 +666,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
 \r
         ModelingResources MOD = ModelingResources.getInstance(graph);\r
         Resource instanceComposite = graph.getPossibleObject(instance, MOD.DiagramToComposite);\r
-        List<Resource> instanceComponents = new ArrayList<Resource>(elementsAddedToTemplate.size());\r
+        List<Resource> instanceComponents = new ArrayList<>(elementsAddedToTemplate.size());\r
 \r
         // Post-process added elements after typicalInfo has been updated and\r
         // template mapping statements are in place.\r
@@ -871,6 +872,17 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         return changed;\r
     }\r
 \r
+    private static class Connector {\r
+        public final Resource attachmentRelation;\r
+        public final Resource connector;\r
+        public RouteLine attachedTo;\r
+\r
+        public Connector(Resource attachmentRelation, Resource connector) {\r
+            this.attachmentRelation = attachmentRelation;\r
+            this.connector = connector;\r
+        }\r
+    }\r
+\r
     /**\r
      * Synchronizes two route graph connection topologies if and only if the\r
      * destination connection is not attached to any node elements besides\r
@@ -901,19 +913,28 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
             cu = new ConnectionUtil(graph);\r
 \r
         // 0.1. find mappings between source and target connection connectors\r
-        Collection<Resource> targetConnectors = graph.getObjects(targetConnection, DIA.HasConnector);\r
-        for (Resource targetConnector : targetConnectors) {\r
+        Collection<Statement> toTargetConnectors = graph.getStatements(targetConnection, DIA.HasConnector);\r
+        Map<Resource, Connector> targetConnectors = new THashMap<>(toTargetConnectors.size());\r
+        for (Statement toTargetConnector : toTargetConnectors) {\r
+            Resource targetConnector = toTargetConnector.getObject();\r
+            targetConnectors.put(targetConnector, new Connector(toTargetConnector.getPredicate(), targetConnector));\r
             Statement toNode = cu.getConnectedComponentStatement(targetConnection, targetConnector);\r
             if (toNode == null) {\r
                 // Corrupted target connection!\r
-                ErrorLogger.defaultLogError("Encountered corrupted typical template connection " + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance " + NameUtils.getSafeName(graph, targetConnector, true), new Exception("trace"));\r
+                ErrorLogger.defaultLogError("Encountered corrupted typical template connection "\r
+                        + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance "\r
+                        + NameUtils.getSafeName(graph, targetConnector, true) + " that is not attached to any element.",\r
+                        new Exception("trace"));\r
                 return false;\r
             }\r
-\r
-            // Check that the target connections does not connect to\r
-            // non-templatized elements before syncing.\r
-            if (!graph.hasStatement(toNode.getObject(), MOD.IsTemplatized))\r
+            if (!graph.hasStatement(targetConnector, DIA.AreConnected)) {\r
+                // Corrupted target connection!\r
+                ErrorLogger.defaultLogError("Encountered corrupted typical template connection "\r
+                        + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance "\r
+                        + NameUtils.getSafeName(graph, targetConnector, true) + " that is not connected to any other route node.",\r
+                        new Exception("trace"));\r
                 return false;\r
+            }\r
 \r
             //Resource templateNode = typicalInfo.instanceToTemplate.get(toNode.getObject());\r
             Resource templateNode = graph.getPossibleObject(toNode.getObject(), MOD.HasElementSource);\r
@@ -928,7 +949,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
                             t2s.put(targetConnector, templateConnector);\r
 \r
                             if (DEBUG)\r
-                                System.out.println("Mapping connector "\r
+                                debug(typicalInfo, "Mapping connector "\r
                                         + NameUtils.getSafeName(graph, templateConnector, true)\r
                                         + " to " + NameUtils.getSafeName(graph, targetConnector, true));\r
                         }\r
@@ -940,14 +961,17 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         // 0.2. find mapping between source and target route lines\r
         Collection<Resource> sourceInteriorRouteNodes = graph.getObjects(sourceConnection, DIA.HasInteriorRouteNode);\r
         Collection<Resource> targetInteriorRouteNodes = graph.getObjects(targetConnection, DIA.HasInteriorRouteNode);\r
-        Map<Resource, Paster.RouteLine> sourceToRouteLine = new THashMap<Resource, Paster.RouteLine>();\r
-        Map<Resource, Paster.RouteLine> targetToRouteLine = new THashMap<Resource, Paster.RouteLine>();\r
+        Map<Resource, Paster.RouteLine> sourceToRouteLine = new THashMap<>();\r
+        Map<Resource, Paster.RouteLine> targetToRouteLine = new THashMap<>();\r
 \r
         for (Resource source : sourceInteriorRouteNodes)\r
             sourceToRouteLine.put(source, Paster.readRouteLine(graph, source));\r
         for (Resource target : targetInteriorRouteNodes)\r
             targetToRouteLine.put(target, Paster.readRouteLine(graph, target));\r
 \r
+        Map<Resource, Paster.RouteLine> originalSourceToRouteLine = new THashMap<>(sourceToRouteLine);\r
+        Map<Resource, Paster.RouteLine> originalTargetToRouteLine = new THashMap<>(targetToRouteLine);\r
+\r
         nextSourceLine:\r
             for (Iterator<Map.Entry<Resource, Paster.RouteLine>> sourceIt = sourceToRouteLine.entrySet().iterator(); !targetToRouteLine.isEmpty() && sourceIt.hasNext();) {\r
                 Map.Entry<Resource, Paster.RouteLine> sourceEntry = sourceIt.next();\r
@@ -961,7 +985,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
                         targetIt.remove();\r
 \r
                         if (DEBUG)\r
-                            System.out.println("Mapping routeline "\r
+                            debug(typicalInfo, "Mapping routeline "\r
                                     + NameUtils.getSafeName(graph, sourceEntry.getKey(), true)\r
                                     + " - " + sourceEntry.getValue()\r
                                     + " to " + NameUtils.getSafeName(graph, targetEntry.getKey(), true)\r
@@ -973,20 +997,40 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
             }\r
 \r
         if (DEBUG) {\r
-            System.out.println("Take 1: Source to target route nodes map : " + s2t);\r
-            System.out.println("Take 1: Target to source route nodes map : " + t2s);\r
+            debug(typicalInfo, "Take 1: Source to target route nodes map : " + s2t);\r
+            debug(typicalInfo, "Take 1: Target to source route nodes map : " + t2s);\r
         }\r
 \r
-        // 1.1 remove excess connectors\r
-        for (Resource targetConnector : targetConnectors) {\r
-            if (!t2s.containsKey(targetConnector)) {\r
-               typicalInfo.messageLog.add("\t\t\tremove excess connector from target connection: " + NameUtils.getSafeName(graph, targetConnector));\r
-                cu.removeConnectionPart(targetConnector);\r
-                changed = true;\r
+        // 1.1. Temporarily disconnect instance-specific connectors from the the connection .\r
+        //      They will be added back to the connection after the templatized parts of the\r
+        //      connection have been synchronized.\r
+\r
+        // Stores diagram connectors that are customizations in the synchronized instance.\r
+        List<Connector> instanceOnlyConnectors = null;\r
+\r
+        for (Connector connector : targetConnectors.values()) {\r
+            if (!t2s.containsKey(connector.connector)) {\r
+                typicalInfo.messageLog.add("\t\tencountered instance-specific diagram connector in target connection: " + NameUtils.getSafeName(graph, connector.connector));\r
+\r
+                // Find the RouteLine this connectors is connected to.\r
+                for (Resource rl : graph.getObjects(connector.connector, DIA.AreConnected)) {\r
+                    connector.attachedTo = originalTargetToRouteLine.get(rl);\r
+                    if (connector.attachedTo != null)\r
+                        break;\r
+                }\r
+\r
+                // Disconnect connector from connection\r
+                graph.deny(targetConnection, connector.attachmentRelation, connector.connector);\r
+                graph.deny(connector.connector, DIA.AreConnected);\r
+\r
+                // Keep track of the disconnected connector\r
+                if (instanceOnlyConnectors == null)\r
+                    instanceOnlyConnectors = new ArrayList<>(targetConnectors.size());\r
+                instanceOnlyConnectors.add(connector);\r
             }\r
         }\r
 \r
-        // 1.2 add missing connectors to target\r
+        // 1.2. add missing connectors to target\r
         Collection<Resource> sourceConnectors = graph.getObjects(sourceConnection, DIA.HasConnector);\r
         for (Resource sourceConnector : sourceConnectors) {\r
             if (!s2t.containsKey(sourceConnector)) {\r
@@ -1016,7 +1060,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
 \r
         // 2. sync route lines and their connectivity:\r
         // 2.1. assign correspondences in target for each source route line\r
-        //      by reusing excess routelines in target and by creating new\r
+        //      by reusing excess route lines in target and by creating new\r
         //      route lines.\r
 \r
         Resource[] targetRouteLines = targetToRouteLine.keySet().toArray(Resource.NONE);\r
@@ -1027,7 +1071,7 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
             Resource source = sourceEntry.getKey();\r
             Paster.RouteLine sourceLine = sourceEntry.getValue();\r
 \r
-            typicalInfo.messageLog.add("\t\t\tassign an instance-side routeline complement for " + NameUtils.getSafeName(graph, source, true) + " - " + sourceLine);\r
+            typicalInfo.messageLog.add("\t\t\tassign an instance-side routeline counterpart for " + NameUtils.getSafeName(graph, source, true) + " - " + sourceLine);\r
 \r
             // Assign target route line for source\r
             Resource target = null;\r
@@ -1051,16 +1095,16 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         }\r
 \r
         if (targetRouteLine >= 0) {\r
-               typicalInfo.messageLog.add("\t\t\tremove excess route lines (" + (targetRouteLine + 1) + ") from target connection");\r
+            typicalInfo.messageLog.add("\t\t\tremove excess route lines (" + (targetRouteLine + 1) + ") from target connection");\r
             for (; targetRouteLine >= 0; targetRouteLine--) {\r
-               typicalInfo.messageLog.add("\t\t\t\tremove excess route line: " + NameUtils.getSafeName(graph, targetRouteLines[targetRouteLine], true));\r
+                typicalInfo.messageLog.add("\t\t\t\tremove excess route line: " + NameUtils.getSafeName(graph, targetRouteLines[targetRouteLine], true));\r
                 cu.removeConnectionPart(targetRouteLines[targetRouteLine]);\r
             }\r
         }\r
 \r
         if (DEBUG) {\r
-            System.out.println("Take 2: Source to target route nodes map : " + s2t);\r
-            System.out.println("Take 2: Target to source route nodes map : " + t2s);\r
+            debug(typicalInfo, "Take 2: Source to target route nodes map : " + s2t);\r
+            debug(typicalInfo, "Take 2: Target to source route nodes map : " + t2s);\r
         }\r
 \r
         // 2.2. Synchronize target connection topology (DIA.AreConnected)\r
@@ -1071,9 +1115,100 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         changed |= cu.removeExtraInteriorRouteNodes(targetConnection) > 0;\r
         changed |= cu.removeUnusedConnectors(targetConnection) > 0;\r
 \r
+        // 3.1. Ensure that all mapped route nodes in the target connection\r
+        //      are tagged with MOD.IsTemplatized. Future synchronization\r
+        //      can then take advantage of this information to more easily\r
+        //      decide which parts of the connection are originated from\r
+        //      the template and which are not.\r
+        changed |= markMappedRouteNodesTemplatized(graph, s2t.values());\r
+\r
+        // 4. Add temporarily disconnected instance-specific connectors\r
+        //    back to the synchronized connection. The route line to attach\r
+        //    to is based on a simple heuristic.\r
+        if (instanceOnlyConnectors != null) {\r
+            if (originalSourceToRouteLine.isEmpty()) {\r
+                // If there are 0 route lines in the template connection,\r
+                // then one must be added to the instance connection.\r
+                // This can only happen if the template connection is\r
+                // simple, i.e. just between two terminals without any\r
+                // custom routing.\r
+\r
+                // Attach all target connection connectors to the newly created route line\r
+                Resource rl = cu.newRouteLine(targetConnection, null, null);\r
+                for (Resource sourceConnector : sourceConnectors) {\r
+                    Resource targetConnector = s2t.get(sourceConnector);\r
+                    graph.deny(targetConnector, DIA.AreConnected);\r
+                    graph.claim(targetConnector, DIA.AreConnected, DIA.AreConnected, rl);\r
+                }\r
+\r
+                // Copy orientation and position for new route line from original target route lines.\r
+                // This is a simplification that will attach any amount of route lines in the original\r
+                // target connection into just one route line. There is room for improvement here\r
+                // but it will require a more elaborate algorithm to find and cut the non-templatized\r
+                // route lines as well as connectors out of the connection before synchronizing it.\r
+                //\r
+                // TODO: This implementation chooses the added route line position at random if\r
+                //       there are multiple route lines in the target connection.\r
+                if (!originalTargetToRouteLine.isEmpty()) {\r
+                    RouteLine originalRl = originalTargetToRouteLine.values().iterator().next();\r
+                    setRouteLine(graph, rl, originalRl);\r
+                }\r
+\r
+                // Attach the instance specific connectors also to the only route line\r
+                for (Connector connector : instanceOnlyConnectors) {\r
+                    graph.claim(targetConnection, connector.attachmentRelation, connector.connector);\r
+                    graph.claim(connector.connector, DIA.AreConnected, DIA.AreConnected, rl);\r
+                }\r
+\r
+                changed = true;\r
+            } else {\r
+                for (Connector connector : instanceOnlyConnectors) {\r
+                    // Find the route line that most closely matches the original\r
+                    // route line that the connector was connected to.\r
+                    Resource closestMatch = null;\r
+                    double closestDistance = Double.MAX_VALUE;\r
+                    if (connector.attachedTo != null) {\r
+                        for (Map.Entry<Resource, Paster.RouteLine> sourceLine : originalSourceToRouteLine.entrySet()) {\r
+                            double dist = distance(sourceLine.getValue(), connector.attachedTo);\r
+                            if (dist < closestDistance) {\r
+                                closestMatch = s2t.get(sourceLine.getKey());\r
+                                closestDistance = dist;\r
+                            }\r
+                        }\r
+                    } else {\r
+                        closestMatch = originalSourceToRouteLine.keySet().iterator().next();\r
+                    }\r
+                    graph.claim(targetConnection, connector.attachmentRelation, connector.connector);\r
+                    graph.claim(connector.connector, DIA.AreConnected, DIA.AreConnected, closestMatch);\r
+                    if (closestDistance > 0)\r
+                        changed = true;\r
+                    typicalInfo.messageLog.add("\t\t\treattached instance-specific connector "\r
+                            + NameUtils.getSafeName(graph, connector.connector) + " to nearest existing route line "\r
+                            + NameUtils.getSafeName(graph, closestMatch) + " with distance " + closestDistance);\r
+                }\r
+            }\r
+        }\r
+\r
         return changed;\r
     }\r
 \r
+    private boolean markMappedRouteNodesTemplatized(WriteGraph graph, Iterable<Resource> routeNodes) throws DatabaseException {\r
+        boolean changed = false;\r
+        for (Resource rn : routeNodes) {\r
+            if (!graph.hasStatement(rn, MOD.IsTemplatized)) {\r
+                graph.claim(rn, MOD.IsTemplatized, MOD.IsTemplatized, rn);\r
+                changed = true;\r
+            }\r
+        }\r
+        return changed;\r
+    }\r
+\r
+    private static double distance(RouteLine l1, RouteLine l2) {\r
+        double dist = Math.abs(l2.getPosition() - l1.getPosition());\r
+        dist *= l2.isHorizontal() == l1.isHorizontal() ? 1 : 1000;\r
+        return dist;\r
+    }\r
+\r
     private boolean connectRouteNodes(WriteGraph graph, TypicalInfo typicalInfo, Collection<Resource> sourceRouteNodes) throws DatabaseException {\r
         boolean changed = false;\r
         for (Resource src : sourceRouteNodes) {\r
@@ -1114,6 +1249,15 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
         return changed;\r
     }\r
 \r
+    private void setRouteLine(WriteGraph graph, Resource line, double position, boolean horizontal) throws DatabaseException {\r
+        graph.claimLiteral(line, DIA.HasPosition, L0.Double, position, Bindings.DOUBLE);\r
+        graph.claimLiteral(line, DIA.IsHorizontal, L0.Boolean, horizontal, Bindings.BOOLEAN);\r
+    }\r
+\r
+    private void setRouteLine(WriteGraph graph, Resource line, RouteLine rl) throws DatabaseException {\r
+        setRouteLine(graph, line, rl.getPosition(), rl.isHorizontal());\r
+    }\r
+\r
     private void copyRouteLine(WriteGraph graph, Resource src, Resource tgt) throws DatabaseException {\r
         Double pos = graph.getPossibleRelatedValue(src, DIA.HasPosition, Bindings.DOUBLE);\r
         Boolean hor = graph.getPossibleRelatedValue(src, DIA.IsHorizontal, Bindings.BOOLEAN);\r
@@ -1142,9 +1286,16 @@ public class SyncTypicalTemplatesToInstances extends WriteRequest {
 \r
     private static <K, V> Map<K, V> newOrClear(Map<K, V> current) {\r
         if (current == null)\r
-            return new THashMap<K, V>();\r
+            return new THashMap<>();\r
         current.clear();\r
         return current;\r
     }\r
 \r
+    private void debug(TypicalInfo typicalInfo, String message) {\r
+        if (DEBUG) {\r
+            System.out.println(message);\r
+            typicalInfo.messageLog.add(message);\r
+        }\r
+    }\r
+\r
 }
\ No newline at end of file