batch) {
+
+ for (String[] strings : batch) {
+ execute(strings);
+ }
+
+ }
+
+
+ public class TwoBr {
+ public BufferedReader output;
+ public BufferedReader error;
+ }
+}
diff --git a/src/main/java/com/olexyn/ensync/Flow.java b/src/main/java/com/olexyn/ensync/Flow.java
new file mode 100644
index 0000000..1c06c65
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/Flow.java
@@ -0,0 +1,93 @@
+package com.olexyn.ensync;
+
+import com.olexyn.ensync.artifacts.SyncDirectory;
+import com.olexyn.ensync.artifacts.SyncMap;
+
+import java.io.File;
+import static com.olexyn.ensync.Main.MAP_OF_SYNCMAPS;
+
+public class Flow implements Runnable {
+
+
+ Tools tools = new Tools();
+
+
+ private String state;
+
+
+ public void run() {
+
+
+ while (true) {
+
+
+ synchronized (MAP_OF_SYNCMAPS) {
+ readOrMakeStateFile();
+
+ for (var syncMapEntry : MAP_OF_SYNCMAPS.entrySet()) {
+
+
+ for (var SDEntry : syncMapEntry.getValue().syncDirectories.entrySet()) {
+
+ SyncDirectory SD = SDEntry.getValue();
+
+ state = "READ";
+ SD.readFreshState();
+
+ SD.listCreated = SD.makeListCreated();
+ SD.listDeleted = SD.makeListDeleted();
+ SD.listModified = SD.makeListModified();
+
+ SD.doCreate();
+ SD.doDelete();
+ SD.doModify();
+
+ SD.writeStateFile(SD.path);
+ }
+
+
+ }
+ }
+
+
+ try {
+ long pause = 2000;
+ System.out.println("Pausing... for "+pause+ "ms.");
+ Thread.sleep(pause);
+ } catch (InterruptedException ignored) {
+
+ }
+
+ }
+ }
+
+
+ public String getState() {
+ return state == null ? "NONE" : state;
+ }
+
+
+ /**
+ * For every single SyncDirectory try to read it's StateFile.
+ * If the StateFile is missing, then create a StateFile.
+ */
+ private void readOrMakeStateFile() {
+ for (var syncMapEntry : MAP_OF_SYNCMAPS.entrySet()) {
+ SyncMap syncMap = syncMapEntry.getValue();
+ state = syncMap.toString();
+
+ for (var stringSyncDirectoryEntry : syncMap.syncDirectories.entrySet()) {
+ SyncDirectory SD = stringSyncDirectoryEntry.getValue();
+ String path = SD.path;
+ String stateFilePath = tools.stateFilePath(path);
+
+ if (new File(stateFilePath).exists()) {
+ state = "READ-STATE-FILE-" + SD.readStateFile();
+ } else {
+ SD.writeStateFile(path);
+ }
+ }
+
+ }
+ }
+}
diff --git a/src/main/java/com/olexyn/ensync/Main.java b/src/main/java/com/olexyn/ensync/Main.java
new file mode 100644
index 0000000..00ea739
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/Main.java
@@ -0,0 +1,45 @@
+package com.olexyn.ensync;
+
+import com.olexyn.ensync.artifacts.SyncMap;
+import com.olexyn.ensync.ui.UI;
+
+import java.util.HashMap;
+
+
+public class Main{
+
+
+
+
+
+
+ final public static Thread UI_THREAD = new Thread(new UI(), "ui");
+
+ final public static Thread FLOW_THREAD = new Thread(new Flow(), "flow");
+
+ final public static HashMap MAP_OF_SYNCMAPS = new HashMap<>();
+
+
+
+ public static void main(String[] args) {
+
+
+
+
+
+ UI_THREAD.start();
+
+ FLOW_THREAD.start();
+
+
+
+
+
+
+
+ }
+
+
+
+
+}
diff --git a/src/main/java/com/olexyn/ensync/Tools.java b/src/main/java/com/olexyn/ensync/Tools.java
new file mode 100644
index 0000000..5c96cf7
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/Tools.java
@@ -0,0 +1,151 @@
+package com.olexyn.ensync;
+
+import com.olexyn.ensync.artifacts.SyncFile;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Tools {
+
+ private final Execute x;
+
+ public Tools() {
+ x = new Execute();
+ }
+
+
+ /**
+ * Convert BufferedReader to String.
+ *
+ * @param br BufferedReader
+ * @return String
+ */
+ public String brToString(BufferedReader br) {
+ StringBuilder sb = new StringBuilder();
+ Object[] br_array = br.lines().toArray();
+ for (int i = 0; i < br_array.length; i++) {
+ sb.append(br_array[i].toString() + "\n");
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Convert BufferedReader to List of Strings.
+ *
+ * @param br BufferedReader
+ * @return List
+ */
+ public List brToListString(BufferedReader br) {
+ List list = new ArrayList<>();
+ Object[] br_array = br.lines().toArray();
+ for (int i = 0; i < br_array.length; i++) {
+ list.add(br_array[i].toString());
+ }
+ return list;
+ }
+
+
+ public List fileToLines(File file) {
+ String filePath = file.getPath();
+ List lines = null;
+ try {
+ lines = Files.readAllLines(Paths.get(filePath));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return lines;
+ }
+
+ public String fileToString(File file){
+ List lineList = fileToLines(file);
+ StringBuilder sb = new StringBuilder();
+ for (String line : lineList){
+ sb.append(line).append("\n");
+ }
+ return sb.toString();
+ }
+
+
+ public Map mapMinus(Map fromA, Map substractB) {
+
+ Map difference = new HashMap<>();
+ for (Map.Entry entry : fromA.entrySet()) {
+ String key = entry.getKey();
+
+ if (fromA.containsKey(key) && !substractB.containsKey(key)) {
+ SyncFile file = fromA.get(key);
+ difference.put(key, file);
+ }
+
+ }
+ return difference;
+ }
+
+
+ public StringBuilder stringListToSb(List list) {
+ StringBuilder sb = new StringBuilder();
+
+ for (String line : list) {
+ sb.append(line + "\n");
+ }
+ return sb;
+ }
+
+ /**
+ * Write sb to file at path .
+ *
+ * @param path String
+ * @param sb StringBuilder
+ */
+ public void writeSbToPath(String path, StringBuilder sb) {
+ writeSbToFile(new File(path), sb);
+ }
+
+ public void writeSbToFile(File file, StringBuilder sb) {
+ try {
+ BufferedWriter bw = new BufferedWriter(new FileWriter(file));
+ bw.write(sb.toString());
+ bw.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ /**
+ * Write List of String to file at path .
+ *
+ * @param path String
+ * @param list StringBuilder
+ */
+ public void writeStringListToFile(String path, List list) {
+ File file = new File(path);
+ File parent = new File(file.getParent());
+ if (!parent.exists()) {
+
+ x.execute(new String[]{"mkdir",
+ "-p",
+ parent.getPath()});
+ }
+
+
+ try {
+ BufferedWriter bw = new BufferedWriter(new FileWriter(new File(path)));
+ StringBuilder sb = stringListToSb(list);
+ bw.write(sb.toString());
+ bw.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public String stateFilePath(String path) {
+ return "/tmp/ensync/state" + path.replace("/", "-");
+ }
+}
diff --git a/src/main/java/com/olexyn/ensync/artifacts/SyncDirectory.java b/src/main/java/com/olexyn/ensync/artifacts/SyncDirectory.java
new file mode 100644
index 0000000..acb38c8
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/artifacts/SyncDirectory.java
@@ -0,0 +1,362 @@
+package com.olexyn.ensync.artifacts;
+
+import com.olexyn.ensync.Execute;
+import com.olexyn.ensync.Tools;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A SyncDirectory is an occurrence of a particular directory somewhere across the filesystems.
+ */
+public class SyncDirectory {
+
+ private String flowState;
+ private SyncDirectory thisSD = this;
+
+
+ private SyncMap syncMap;
+ public String path = null;
+
+ public Map listCreated = new HashMap<>();
+ public Map listDeleted = new HashMap<>();
+ public Map listModified = new HashMap<>();
+
+
+ Tools tools = new Tools();
+ Execute x = new Execute();
+
+ /**
+ * Create a SyncDirectory from realPath.
+ *
+ * @see SyncMap
+ */
+ public SyncDirectory(String path, SyncMap syncMap) {
+
+ this.path = path;
+ this.syncMap = syncMap;
+
+ }
+
+
+ /**
+ * Get the current state by using the `find` command.
+ */
+ public Map readFreshState() {
+ //NOTE that the SFile().lastModifiedOld is not set here, so it is 0 by default.
+ Map filemap = new HashMap<>();
+
+ Execute.TwoBr find = x.execute(new String[]{"find",
+ path});
+
+ List pathList = tools.brToListString(find.output);
+
+ for (String filePath : pathList) {
+ SyncFile file = new SyncFile(this, filePath);
+
+ filemap.put(filePath, file);
+ }
+
+
+ return filemap;
+
+
+ }
+
+
+ /**
+ * READ the contents of StateFile to Map.
+ */
+ public Map readStateFile() {
+ Map filemap = new HashMap<>();
+ List lines = tools.fileToLines(new File(tools.stateFilePath(path)));
+
+ for (String line : lines) {
+ // this is a predefined format: "modification-time path"
+ String modTimeString = line.split(" ")[0];
+ long modTime = Long.parseLong(modTimeString);
+
+ String sFilePath = line.replace(modTimeString + " ", "");
+ SyncFile sfile = new SyncFile(this, sFilePath);
+
+ sfile.setTimeModifiedFromStateFile(modTime);
+
+ filemap.put(sFilePath, sfile);
+ }
+
+ return filemap;
+
+ }
+
+
+ /**
+ * Compare the OLD and NEW pools.
+ * List is cleared and created each time.
+ */
+ public Map makeListCreated() {
+
+ Map fromA = readFreshState();
+ Map substractB = readStateFile();
+
+ return tools.mapMinus(fromA, substractB);
+ }
+
+
+ /**
+ * Compare the OLD and NEW pools.
+ * List is cleared and created each time.
+ */
+ public Map makeListDeleted() {
+
+ Map fromA = readStateFile();
+ Map substractB = readFreshState();
+
+ Map listDeleted = tools.mapMinus(fromA, substractB);
+
+ Map swap = new HashMap<>();
+
+
+ for (var entry : listDeleted.entrySet()) {
+
+ String key = entry.getKey();
+ String parentKey = entry.getValue().getParent();
+
+ if (listDeleted.containsKey(parentKey) || swap.containsKey(parentKey)) {
+ swap.put(key, listDeleted.get(key));
+ }
+ }
+
+ return tools.mapMinus(listDeleted, swap);
+ }
+
+
+ /**
+ * Compare the OLD and NEW pools.
+ * List is cleared and created each time.
+ */
+ public Map makeListModified() {
+
+ Map listModified = new HashMap<>();
+
+ Map stateFileMap = readStateFile();
+
+ for (var freshFileEntry : readFreshState().entrySet()) {
+
+ String freshFileKey = freshFileEntry.getKey();
+ SyncFile freshFile = freshFileEntry.getValue();
+
+ if (freshFile.isDirectory()) { continue;} // no need to modify Directories, the Filesystem will do that, if a File changed.
+
+ // If KEY exists in OLD , thus FILE was NOT created.
+ boolean oldFileExists = stateFileMap.containsKey(freshFileKey);
+ boolean fileIsFresher = freshFile.getTimeModified() > freshFile.getTimeModifiedFromStateFile();
+
+ if (oldFileExists && fileIsFresher) {
+ listModified.put(freshFileKey, freshFile);
+ }
+ }
+ return listModified;
+ }
+
+
+ /**
+ * QUERY state of the filesystem at realPath.
+ * WRITE the state of the filesystem to file.
+ */
+ public void writeStateFile(String path) {
+ List outputList = new ArrayList<>();
+
+
+ Execute.TwoBr find = x.execute(new String[]{"find",
+ path});
+
+ List pathList = tools.brToListString(find.output);
+
+
+ for (String filePath : pathList) {
+ long lastModified = new File(filePath).lastModified();
+ outputList.add("" + lastModified + " " + filePath);
+ }
+
+ tools.writeStringListToFile(tools.stateFilePath(path), outputList);
+ }
+
+
+ private class Info {
+
+ private String thisFilePath;
+ private String otherFilePath;
+ private String otherParentPath;
+ private File otherParentFile;
+
+
+ private Info(SyncDirectory thisSD, SyncFile sFile, SyncDirectory otherSD) {
+ // Example:
+ // syncDirectory /foo
+ // otherSyncDirectory /bar
+ // createdFile /foo/hello/created-file.gif
+ // relativePath /hello/created-file.gif
+ String relativePath = sFile.getPath().replace(thisSD.path, "");
+ this.thisFilePath = sFile.getPath();
+ this.otherFilePath = otherSD.path + relativePath;
+ File otherFile = new File(otherFilePath);
+
+ this.otherParentPath = otherFile.getParent();
+ this.otherParentFile = new File(otherParentPath);
+
+
+ }
+ }
+
+
+ public void doCreate() {
+
+ for (var entry : listCreated.entrySet()) {
+ SyncFile createdFile = entry.getValue();
+
+ for (var otherEntry : syncMap.syncDirectories.entrySet()) {
+ SyncDirectory otherSD = otherEntry.getValue();
+
+ if (this.equals(otherSD)) { continue; }
+
+ Info info = new Info(this, createdFile, otherSD);
+
+ writeFile(info, this, createdFile, otherSD);
+ }
+ }
+ }
+
+
+ /**
+ *
+ */
+ public void doDelete() {
+
+ for (var entry : listDeleted.entrySet()) {
+ SyncFile deletedFile = entry.getValue();
+
+ for (var otherEntry : syncMap.syncDirectories.entrySet()) {
+ SyncDirectory otherSD = otherEntry.getValue();
+
+ if (this.equals(otherSD)) { continue; }
+
+ Info info = new Info(thisSD, deletedFile, otherSD);
+ deleteFile(info, thisSD, deletedFile, otherSD);
+
+
+ }
+ }
+ }
+
+ private void deleteFile(Info info, SyncDirectory thisSD, SyncFile thisFile, SyncDirectory otherSD) {
+
+ SyncFile otherFile = new SyncFile(otherSD, otherSD.path + thisFile.relativePath);
+
+ if (!otherFile.exists()) { return;}
+
+ // if the otherFile was created with ensync it will have the == TimeModified.
+ long thisFileTimeModified = thisFile.getTimeModified();
+ long otherFileTimeModified = otherFile.getTimeModified();
+
+ if (thisFile.getTimeModified() >= otherFile.getTimeModified()) {
+ List cmd = List.of("rm", "-r", info.otherFilePath);
+ x.execute(cmd);
+ }
+ }
+
+
+ public void doModify() {
+
+ for (var entry : listModified.entrySet()) {
+ SyncFile modifiedFile = entry.getValue();
+
+ for (var otherEntry : syncMap.syncDirectories.entrySet()) {
+ SyncDirectory otherSD = otherEntry.getValue();
+
+ if (this.equals(otherSD)) { continue; }
+
+ Info info = new Info(this, modifiedFile, otherSD);
+
+ writeFile(info, this, modifiedFile, otherSD);
+ }
+ }
+ }
+
+
+ private void writeFile(Info info, SyncDirectory thisSD, SyncFile thisFile, SyncDirectory otherSD) {
+
+ SyncFile otherFile = new SyncFile(otherSD, otherSD.path + thisFile.relativePath);
+
+
+ if (otherFile.exists() && thisFile.getTimeModified() < otherFile.getTimeModified()) { return;}
+
+
+ if (thisFile.isDirectory() && !otherFile.exists()) {
+ List cmd = List.of("mkdir", "-p", info.otherFilePath);
+ x.execute(cmd);
+ return;
+ }
+
+ if (thisFile.isFile()) {
+
+
+ if (!info.otherParentFile.exists()) {
+ makeParentChain(otherFile, thisFile);
+ // List cmd = List.of("mkdir", "-p", info.otherParentPath);
+ //x.execute(cmd);
+ }
+
+ List cmd = List.of("cp", "-p", info.thisFilePath, info.otherFilePath);
+ x.execute(cmd);
+ copyModifDate(thisFile.getParentFile(), otherFile.getParentFile());
+ }
+ }
+
+
+ private void makeParentChain(File otherFile, File thisFile) {
+ try {
+ File otherParent = new File(otherFile.getParent());
+ File thisParent = new File(thisFile.getParent());
+
+ if (!otherParent.exists()) {
+ makeParentChain(otherParent, thisParent);
+ makeParentChain(otherFile, thisFile);
+
+ } else if (thisFile.isDirectory()) {
+
+ List cmd = List.of("mkdir", otherFile.getPath());
+ x.execute(cmd);
+
+
+ cmd = List.of("stat", "--format", "%y", thisFile.getPath());
+
+
+ String mDate = x.execute(cmd).output.readLine();
+
+
+ cmd = List.of("touch", "-m", "--date=" + mDate, otherFile.getPath());
+ String error = x.execute(cmd).error.readLine();
+ int br = 0;
+
+
+ }
+ } catch (Exception ignored) {}
+ }
+
+
+ private void copyModifDate(File fromFile, File toFile) {
+ try {
+ List cmd = List.of("stat", "--format", "%y", fromFile.getPath());
+ String mDate = x.execute(cmd).output.readLine();
+
+ cmd = List.of("touch", "-m", "--date=" + mDate, toFile.getPath());
+ x.execute(cmd);
+ } catch (Exception ignored) {}
+ }
+
+}
+
+
diff --git a/src/main/java/com/olexyn/ensync/artifacts/SyncFile.java b/src/main/java/com/olexyn/ensync/artifacts/SyncFile.java
new file mode 100644
index 0000000..224fc7c
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/artifacts/SyncFile.java
@@ -0,0 +1,63 @@
+package com.olexyn.ensync.artifacts;
+
+import java.io.File;
+import java.util.Map;
+
+public class SyncFile extends File {
+
+
+ // Very IMPORTANT field. Allows to store lastModified as it is stored in the StateFile.
+ public long timeModifiedFromStateFile = 0;
+
+ public String relativePath;
+ private SyncDirectory sd;
+
+
+
+ public SyncFile(SyncDirectory sd , String pathname) {
+
+ super(pathname);
+ this.sd = sd;
+ relativePath = this.getPath().replace(sd.path, "");
+ }
+
+
+
+
+
+ public void setTimeModifiedFromStateFile(long value){
+ timeModifiedFromStateFile = value;
+ }
+
+
+ public long getTimeModifiedFromStateFile(){
+ SyncFile record = sd.readStateFile().get(this.getPath());
+
+
+ return record == null ? 0 : record.timeModifiedFromStateFile;
+ }
+
+
+ /**
+ * If File exists on Disk get the TimeModified from there.
+ * Else try to read it from StateFile.
+ * Else return 0 ( = oldest possible time - a value of 0 can be seen as equal to "never existed").
+ * EXAMPLES:
+ * If a File was deleted, then the time will be taken from statefile.
+ * If a File never existed, it will have time = 0, and thus will always be overwritten.
+ */
+ public long getTimeModified(){
+ if (this.exists()){
+ return lastModified();
+ }
+
+ if (sd.readStateFile().get(this.getPath())!=null){
+ return getTimeModifiedFromStateFile();
+ }
+
+
+ return 0;
+ }
+
+
+}
diff --git a/src/main/java/com/olexyn/ensync/artifacts/SyncMap.java b/src/main/java/com/olexyn/ensync/artifacts/SyncMap.java
new file mode 100644
index 0000000..1e36c8d
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/artifacts/SyncMap.java
@@ -0,0 +1,45 @@
+package com.olexyn.ensync.artifacts;
+
+
+import com.olexyn.ensync.Tools;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A SyncMap is the set of such SyncDirectories. The purpose of the SyncMap is to help syncronize the SyncDirectories.
+ */
+public class SyncMap {
+
+ public String name;
+ public Map syncDirectories = new HashMap<>();
+
+ Tools tools = new Tools();
+
+ /**
+ * @see SyncMap
+ */
+ public SyncMap(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Creates a new Syncdirectory.
+ * Adds the created SyncDirectory to this SyncMap.
+ *
+ * @param realPath the path from which the SyncDirectory is created.
+ * @see SyncDirectory
+ */
+ public void addDirectory(String realPath) {
+ if (new File(realPath).isDirectory()) {
+ syncDirectories.put(realPath, new SyncDirectory(realPath, this));
+ }
+ }
+
+ public void removeDirectory(String realPath) {
+ syncDirectories.remove(realPath);
+ }
+
+
+}
diff --git a/src/main/java/com/olexyn/ensync/saved_directories.xml b/src/main/java/com/olexyn/ensync/saved_directories.xml
new file mode 100644
index 0000000..a2f56b5
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/saved_directories.xml
@@ -0,0 +1,11 @@
+
+ SyncMap1
+
+ SyncDirectory1
+ /foo/dir
+
+
+ SyncDirectory2
+ /bar/dir
+
+
diff --git a/src/main/java/com/olexyn/ensync/shell/pipe.sh b/src/main/java/com/olexyn/ensync/shell/pipe.sh
new file mode 100755
index 0000000..dc6f96c
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/shell/pipe.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+a=$1
+b=$2
+$a | $b
+
+# this is a pipe
\ No newline at end of file
diff --git a/src/main/java/com/olexyn/ensync/shell/pipe2.sh b/src/main/java/com/olexyn/ensync/shell/pipe2.sh
new file mode 100755
index 0000000..826e21f
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/shell/pipe2.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+a=$1
+b=$2
+c=$3
+$a | $b | $c
+
+# this is a double pipe
\ No newline at end of file
diff --git a/src/main/java/com/olexyn/ensync/shell/toFile.sh b/src/main/java/com/olexyn/ensync/shell/toFile.sh
new file mode 100755
index 0000000..8585b3d
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/shell/toFile.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+a=$1
+b=$2
+c=$3
+
+$a $b > $c
\ No newline at end of file
diff --git a/src/main/java/com/olexyn/ensync/ui/Bridge.java b/src/main/java/com/olexyn/ensync/ui/Bridge.java
new file mode 100644
index 0000000..9bfc0f9
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/ui/Bridge.java
@@ -0,0 +1,54 @@
+package com.olexyn.ensync.ui;
+
+
+import com.olexyn.ensync.artifacts.SyncMap;
+
+
+import java.io.File;
+
+ import static com.olexyn.ensync.Main.MAP_OF_SYNCMAPS;
+
+/**
+ * Connect the Controller and the Flow
+ */
+public class Bridge {
+
+
+ void newCollection(String collectionName) {
+
+ synchronized (MAP_OF_SYNCMAPS) {
+ MAP_OF_SYNCMAPS.put(collectionName, new SyncMap(collectionName));
+ }
+ }
+
+
+ void removeCollection(String collectionName) {
+ synchronized (MAP_OF_SYNCMAPS) {
+ MAP_OF_SYNCMAPS.remove(collectionName);
+ }
+ }
+
+
+ void addDirectory(String collectionName, File diretory) {
+ synchronized (MAP_OF_SYNCMAPS) {
+ MAP_OF_SYNCMAPS.get(collectionName).addDirectory(diretory.getAbsolutePath());
+ }
+ //TODO pause syning when adding
+ }
+
+
+ /**
+ * This works, because a directory, which here is an unique ablsolute path,
+ * is only supposed to present once across entire SYNC.
+ */
+ void removeDirectory(String directoryAbsolutePath) {
+ //TODO fix ConcurrentModificationException. This will possibly resolve further errors.
+ synchronized (MAP_OF_SYNCMAPS) {
+ for (var syncMap : MAP_OF_SYNCMAPS.entrySet()) {
+ syncMap.getValue().removeDirectory(directoryAbsolutePath);
+ }
+ }
+
+
+ }
+}
diff --git a/src/main/java/com/olexyn/ensync/ui/Controller.java b/src/main/java/com/olexyn/ensync/ui/Controller.java
new file mode 100644
index 0000000..55e8504
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/ui/Controller.java
@@ -0,0 +1,237 @@
+package com.olexyn.ensync.ui;
+
+
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import javafx.scene.text.Text;
+import javafx.stage.DirectoryChooser;
+import javafx.stage.Window;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ResourceBundle;
+
+/***
+ * Controller class for JavaFX. Contains the application logic.
+ */
+public class Controller implements Initializable {
+
+
+ final static int COLUMN_COUNT = 5; // How many columns should the GridPane have? Adjust if necessary.
+ final static Text SPACE = new Text(""); // Placeholder
+ int collection_count = 0;
+ Bridge bridge = new Bridge();
+
+ @FXML
+ protected GridPane gridPane;
+
+ @Override
+ public void initialize(URL url, ResourceBundle resourceBundle) {
+
+ Text end = new Text("");
+ end.setId("end");
+
+ Button newCollectionButton = new Button("New Collection");
+ newCollectionButton.setId("newCollectionButton");
+ newCollectionButton.setOnAction(event -> { this.newCollection();});
+
+ gridPane.add(end, 0, 0);
+
+ List nodeList = new ArrayList<>(gridPane.getChildren());
+
+ List payload = Arrays.asList(new Text(""), new Text(""), new Text(""), new Text(""), newCollectionButton);
+
+
+ insertPayload(nodeList, payload, "end", 0);
+ redraw(gridPane, nodeList);
+
+
+ }
+
+
+ private void newCollection() {
+
+
+
+ String collectionName = "name" + collection_count++;
+
+ bridge.newCollection(collectionName);
+
+
+ TextField collectionStateTextField = new TextField();
+ collectionStateTextField.setText("STATE");
+ collectionStateTextField.setStyle("-fx-text-fill: green");
+ collectionStateTextField.setDisable(true);
+ collectionStateTextField.setId("collectionStateTextField-" + collectionName);
+
+ Button removeCollectionButton = new Button("Remove Collection");
+ removeCollectionButton.setId("removeCollectionButton-" + collectionName);
+ removeCollectionButton.setOnAction(event -> { this.removeCollection(collectionName);});
+
+ TextField addDirectoryTextField = new TextField();
+ addDirectoryTextField.setId("addDirectoryTextField-" + collectionName);
+
+ Button addDirectoryButton = new Button("Add Directory");
+ addDirectoryButton.setId("addDirectoryButton-" + collectionName);
+ addDirectoryButton.setOnAction(event -> { this.addDirectory(collectionName);});
+
+
+ List nodeList = new ArrayList<>(gridPane.getChildren());
+
+ List payload = new ArrayList<>();
+ payload.addAll(Arrays.asList(new Text(""), new Text(""), collectionStateTextField, new Text(""), removeCollectionButton));
+ payload.addAll(Arrays.asList(addDirectoryTextField, new Text(""), new Text(""), new Text(""), addDirectoryButton));
+
+ insertPayload(nodeList, payload, "newCollectionButton", -4);
+ redraw(gridPane, nodeList);
+ }
+
+
+
+
+ /**
+ * For the order & number of Nodes see ui-design.png .
+ * Remove the "Remove-Collection-Line" and the consecutive lines until and including the "Add-Directory-Line".
+ * The !=null expression protects from Text("") placeholders, who have id==null.
+ */
+ private void removeCollection(String collectionName) {
+
+ bridge.removeCollection(collectionName);
+
+ List nodeList = new ArrayList<>(gridPane.getChildren());
+
+ here:
+ for (Node node : nodeList) {
+
+ if (node.getId() != null && node.getId().equals("removeCollectionButton-" + collectionName)) {
+ int i = nodeList.indexOf(node) - 4;
+ while (i < nodeList.size()) {
+ nodeList.remove(i);
+
+ if (nodeList.get(i).getId() != null && nodeList.get(i).getId().equals("addDirectoryButton-" + collectionName)) {
+ nodeList.remove(i);
+ break here;
+ }
+ }
+
+ }
+ }
+
+ redraw(gridPane, nodeList);
+ }
+
+
+ /**
+ *
+ */
+ private void addDirectory(String collectionName) {
+ // TODO throw error if other collection already contains absollutepath
+ Window stage = gridPane.getScene().getWindow();
+
+ final DirectoryChooser directoryChooser = new DirectoryChooser();
+ directoryChooser.setTitle("Select Directory.");
+ directoryChooser.setInitialDirectory(new File(System.getProperty("user.home")));
+
+ File directory = directoryChooser.showDialog(stage);
+
+ if (directory != null) {
+
+ bridge.addDirectory(collectionName, directory);
+
+ TextField directoryPathTextField = new TextField();
+ directoryPathTextField.setText(directory.getAbsolutePath());
+ directoryPathTextField.setDisable(true);
+ directoryPathTextField.setId("directoryPathTextField-" + directory.getAbsolutePath());
+
+
+
+ TextField directoryStateTextField = new TextField();
+ directoryStateTextField.setText("STATE");
+ directoryStateTextField.setStyle("-fx-text-fill: green");
+ directoryStateTextField.setDisable(true);
+ directoryStateTextField.setId("directoryStateTextField-" + directory.getAbsolutePath());
+
+ Button removeDirectoryButton = new Button("Remove");
+ removeDirectoryButton.setId("removeDirectoryButton-" + directory.getAbsolutePath());
+ removeDirectoryButton.setOnAction(event -> { this.removeDirectory(directory.getAbsolutePath());});
+
+
+ List nodeList = new ArrayList<>(gridPane.getChildren());
+ List payload = Arrays.asList(directoryPathTextField, new Text(""), directoryStateTextField, new Text(""), removeDirectoryButton);
+ insertPayload(nodeList, payload, "addDirectoryTextField-" + collectionName, 0);
+ redraw(gridPane, nodeList);
+ }
+
+
+ }
+
+
+ /**
+ * Find the Node with @param id.
+ * Insert the contents of the @param payload starting from the last.
+ * This pushes the Node with @param id forward.
+ */
+ private void insertPayload(List nodeList, List payload, String id, int offset) {
+ for (Node node : nodeList) {
+
+ if (node.getId() != null && node.getId().equals(id)) {
+ int i = nodeList.indexOf(node) + offset;
+
+ for (int j = payload.size() - 1; j >= 0; j--) {
+ nodeList.add(i, payload.get(j));
+ }
+ break;
+ }
+ }
+ }
+
+
+ /**
+ * Clear the gridPane and redraw it with contents of nodeList.
+ */
+ private void redraw(GridPane gridPane, List nodeList) {
+ gridPane.getChildren().clear();
+
+ int col = 0, row = 0;
+
+ for (Node node : nodeList) {
+ if (nodeList.indexOf(node) % COLUMN_COUNT == 0) {
+ row++;
+ col = 0;
+ }
+ gridPane.add(node, col, row);
+ col++;
+ }
+ }
+
+
+ private void removeDirectory(String directoryAbsolutePath) {
+
+ bridge.removeDirectory(directoryAbsolutePath);
+
+ List nodeList = new ArrayList<>(gridPane.getChildren());
+
+ for (Node node : nodeList) {
+
+ if (node.getId() != null && node.getId().equals("removeDirectoryButton-" +directoryAbsolutePath)) {
+ int i = nodeList.indexOf(node) - 4;
+ for (int j = 0; j < 5; j++) {
+ nodeList.remove(i);
+ }
+ break;
+ }
+ }
+ redraw(gridPane, nodeList);
+ }
+
+
+}
+
+
diff --git a/src/main/java/com/olexyn/ensync/ui/UI.java b/src/main/java/com/olexyn/ensync/ui/UI.java
new file mode 100644
index 0000000..9df73d6
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/ui/UI.java
@@ -0,0 +1,37 @@
+package com.olexyn.ensync.ui;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+
+public class UI extends Application implements Runnable {
+
+
+
+
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ Parent root = FXMLLoader.load(getClass().getResource("layout.fxml"));
+ Scene scene = new Scene(root, 500, 500);
+
+
+
+
+
+ primaryStage.setTitle("EnSync");
+ primaryStage.setScene(scene);
+ primaryStage.show();
+ }
+
+
+
+
+ @Override
+ public void run() {
+ UI.launch();
+ }
+}
diff --git a/src/main/java/com/olexyn/ensync/ui/layout.fxml b/src/main/java/com/olexyn/ensync/ui/layout.fxml
new file mode 100644
index 0000000..614e320
--- /dev/null
+++ b/src/main/java/com/olexyn/ensync/ui/layout.fxml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/olexyn/ensync/AppTest.java b/src/test/java/com/olexyn/ensync/AppTest.java
new file mode 100644
index 0000000..b1b28ec
--- /dev/null
+++ b/src/test/java/com/olexyn/ensync/AppTest.java
@@ -0,0 +1,20 @@
+package com.olexyn.ensync;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test for simple App.
+ */
+public class AppTest
+{
+ /**
+ * Rigorous Test :-)
+ */
+ @Test
+ public void shouldAnswerWithTrue()
+ {
+ assertTrue( true );
+ }
+}
diff --git a/src/test/java/com/olexyn/ensync/files/FileTest.java b/src/test/java/com/olexyn/ensync/files/FileTest.java
new file mode 100644
index 0000000..fd2aafd
--- /dev/null
+++ b/src/test/java/com/olexyn/ensync/files/FileTest.java
@@ -0,0 +1,148 @@
+package com.olexyn.ensync.files;
+
+import com.olexyn.ensync.Execute;
+import com.olexyn.ensync.Tools;
+import org.junit.Assert;
+
+import java.io.File;
+
+public class FileTest {
+
+ Execute x = new Execute();
+ Tools tools = new Tools();
+
+ private static final String PATH = System.getProperty("user.dir");
+
+ private static final String fileAPath = "asdf";
+ private static final String fileBPath = "asff";
+
+ private final File a = new File(fileAPath);
+ private final File b = new File(fileBPath);
+
+ private void createFile(File file){
+
+ }
+
+ private void updateFile(File file){
+
+ }
+
+
+ private void deleteFile(File file){
+
+ }
+
+
+
+
+
+ public void deleteA(){
+
+ Assert.assertFalse(a.exists());
+ Assert.assertFalse(b.exists());
+
+
+
+ }
+
+ /**
+ * Simulates user activity on disk.
+ */
+ void createFiles() throws InterruptedException {
+ StringBuilder sbA = new StringBuilder("a");
+ StringBuilder sbB = new StringBuilder("b");
+
+ // dv (deleted-void)
+ // TODO
+
+ // dd
+ tools.writeSbToPath(PATH+"/a/dd", sbA);
+ Thread.sleep(10);
+ tools.writeSbToPath(PATH+"/b/dd", sbB);
+ Thread.sleep(10);Thread.sleep(10);
+ x.execute(new String[]{"rm", PATH+"/a/dd"});
+ Thread.sleep(10);
+ x.execute(new String[]{"rm", PATH+"/b/dd"});
+ Thread.sleep(10);
+
+ // dc
+ tools.writeSbToPath(PATH+"/a/dc", sbA);
+ Thread.sleep(10);
+ x.execute(new String[]{"rm", PATH+"/a/dc"});
+ Thread.sleep(10);
+ tools.writeSbToPath(PATH+"/b/dc", sbB);
+ Thread.sleep(10);
+
+ // dm
+ tools.writeSbToPath(PATH+"/a/dm", sbA);
+ Thread.sleep(10);
+ x.execute(new String[]{"rm", PATH+"/a/dm"});
+ Thread.sleep(10);
+ tools.writeSbToPath(PATH+"/b/dm", sbB);
+ Thread.sleep(10);
+
+ // dv (deleted-void)
+ // TODO
+
+ // cd
+ // TODO
+
+ // cc
+ // TODO
+
+ // cm
+ // TODO
+
+ // cv (created-void)
+ // TODO
+
+ // md
+ // TODO
+
+ // mc
+ // TODO
+
+ // mm
+ // TODO
+
+ }
+
+
+ /**
+ * Checks if end-state is as desired.
+ * @throws Exception otherwise.
+ */
+ void fileTest() throws Exception {
+
+
+
+
+
+
+
+ // Files where the second (= the newer) file was deleted. Thus both files should not exist in the end-state.
+ String[] arrayToDelete = {"/a/dd", "/b/dd" , "/a/cd", "/b/cd", "/a/md", "/b/md"};
+ for (String path : arrayToDelete){
+ if (new TestableFile(path).exists()) throw new Exception();
+ }
+
+ // Files where the second (= the newer) file was created or modified. Thus both files should contain "b" in the end-state.
+ String[] arrayToB = {"/a/dc", "/b/dc" , "/a/dm", "/b/dm", "/a/cc", "/b/cc"};
+ for (String path : arrayToB){
+ if (!new TestableFile(path).hasContent("b")) throw new Exception();
+ }
+
+
+
+
+ }
+
+
+ // Assertion Exception
+
+
+
+
+
+
+}
diff --git a/src/test/java/com/olexyn/ensync/files/TestableFile.java b/src/test/java/com/olexyn/ensync/files/TestableFile.java
new file mode 100644
index 0000000..77db06a
--- /dev/null
+++ b/src/test/java/com/olexyn/ensync/files/TestableFile.java
@@ -0,0 +1,21 @@
+package com.olexyn.ensync.files;
+
+import com.olexyn.ensync.Tools;
+
+import java.io.File;
+
+public class TestableFile extends File {
+
+ Tools tools = new Tools();
+
+
+ public TestableFile(String pathname) {
+ super(pathname);
+ }
+
+ public boolean hasContent(String s){
+
+ String line = tools.fileToLines(this).get(0);
+ return line.equals(s);
+ }
+}
diff --git a/src/test/java/com/olexyn/ensync/scenario.md b/src/test/java/com/olexyn/ensync/scenario.md
new file mode 100644
index 0000000..2c3450c
--- /dev/null
+++ b/src/test/java/com/olexyn/ensync/scenario.md
@@ -0,0 +1,26 @@
+### Testing Scenario
+Test two configs:
+1. FileOps happen while System is down.
+1. FileOps happen while System is running.
+
+
+
+
+| Symbol | Description|
+---|---
+`a` | File `a` in directory `A`
+`b` | File `b` in directory `B`
+`d(x)` | File `x` is deleted.
+`c(x)` | File `x` is created.
+`m(x)` | File `x` is modified.
+
+
+
+
+| `Given` | | `When` | | `Then` | |
+---|---|---|---|---|---
+| `A` | `B`| `A` | `B`|`A` | `B`|
+| `a` | | `d(a)` | | | |
+| `a` | `b` | `d(a)` | `d(b)` | | |
+
+
diff --git a/src/test/java/com/olexyn/ensync/test-config.uxf b/src/test/java/com/olexyn/ensync/test-config.uxf
new file mode 100644
index 0000000..7367943
--- /dev/null
+++ b/src/test/java/com/olexyn/ensync/test-config.uxf
@@ -0,0 +1,1585 @@
+
+
+ 10
+
+ UMLObject
+
+ 690
+ 1930
+ 150
+ 40
+
+ otherDirectory
+
+
+
+ UMLObject
+
+ 690
+ 1890
+ 150
+ 40
+
+ this.Directory
+
+
+
+ UMLObject
+
+ 780
+ 1830
+ 90
+ 40
+
+ Delete
+bg=red
+group=1
+
+
+
+ UMLObject
+
+ 870
+ 1830
+ 90
+ 40
+
+ Create
+bg=green
+group=1
+
+
+
+ UMLObject
+
+ 960
+ 1830
+ 90
+ 40
+
+ Modify
+bg=yellow
+group=1
+
+
+
+ UMLState
+
+ 850
+ 1890
+ 70
+ 40
+
+ File
+bg=red
+group=2
+
+
+
+ UMLObject
+
+ 690
+ 1830
+ 90
+ 40
+
+ Unchanged
+bg=white
+group=1
+
+
+
+ UMLState
+
+ 930
+ 1930
+ 60
+ 40
+
+ File
+group=2
+
+
+
+ UMLState
+
+ 850
+ 1930
+ 70
+ 40
+
+ File
+bg=red
+group=2
+
+
+
+ UMLState
+
+ 920
+ 1890
+ 70
+ 40
+
+ File
+bg=red
+group=2
+
+
+
+ UMLState
+
+ 1210
+ 1890
+ 70
+ 40
+
+ File
+bg=green
+group=3
+
+
+
+ UMLState
+
+ 1210
+ 1930
+ 70
+ 40
+
+ File
+group=3
+
+
+
+ UMLState
+
+ 1140
+ 1930
+ 70
+ 40
+
+ File
+bg=green
+group=3
+
+
+
+ UMLState
+
+ 1140
+ 1890
+ 70
+ 40
+
+ File
+bg=green
+group=3
+
+
+
+ UMLState
+
+ 1500
+ 1890
+ 70
+ 40
+
+ File
+bg=yellow
+group=4
+
+
+
+ UMLState
+
+ 1500
+ 1930
+ 70
+ 40
+
+ File
+group=4
+
+
+
+ UMLState
+
+ 1430
+ 1930
+ 70
+ 40
+
+ File
+bg=yellow
+group=4
+
+
+
+ UMLState
+
+ 1430
+ 1890
+ 70
+ 40
+
+ File
+bg=yellow
+group=4
+
+
+
+ UMLState
+
+ 990
+ 1890
+ 70
+ 40
+
+ File
+bg=red
+group=2
+
+
+
+ UMLState
+
+ 990
+ 1930
+ 70
+ 40
+
+ File
+bg=green
+group=2
+
+
+
+ UMLState
+
+ 1060
+ 1890
+ 70
+ 40
+
+ File
+bg=red
+group=2
+
+
+
+ UMLState
+
+ 1060
+ 1930
+ 70
+ 40
+
+ File
+bg=yellow
+group=2
+
+
+
+ UMLState
+
+ 1350
+ 1890
+ 70
+ 40
+
+ File
+bg=green
+group=3
+
+
+
+ UMLState
+
+ 1350
+ 1930
+ 70
+ 40
+
+ File
+bg=yellow
+group=3
+
+
+
+ UMLState
+
+ 850
+ 1990
+ 70
+ 40
+
+ do
+nothing
+
+
+
+ UMLState
+
+ 920
+ 1990
+ 790
+ 40
+
+ cp if newer
+ try: time deletet = last time present in StateFile, else time deleted = 0 (~never existed)
+halign=left
+
+
+
+ UMLState
+
+ 1280
+ 1930
+ 70
+ 40
+
+ File
+bg=red
+group=3
+
+
+
+ UMLState
+
+ 1280
+ 1890
+ 70
+ 40
+
+ File
+bg=green
+group=3
+
+
+
+ UMLState
+
+ 1570
+ 1890
+ 70
+ 40
+
+ File
+bg=yellow
+group=4
+
+
+
+ UMLState
+
+ 1640
+ 1930
+ 70
+ 40
+
+ File
+bg=green
+group=4
+
+
+
+ UMLState
+
+ 1640
+ 1890
+ 70
+ 40
+
+ File
+bg=yellow
+group=4
+
+
+
+ UMLState
+
+ 1570
+ 1930
+ 70
+ 40
+
+ File
+bg=red
+group=4
+
+
+
+ UMLObject
+
+ 850
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1140
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1210
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 920
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 680
+ 2310
+ 150
+ 40
+
+ otherDirectory
+
+
+
+ UMLObject
+
+ 680
+ 2220
+ 150
+ 40
+
+ this.Directory
+
+
+
+ UMLState
+
+ 1140
+ 2220
+ 70
+ 40
+
+ File
+bg=red
+
+
+
+ UMLState
+
+ 1050
+ 2320
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLState
+
+ 890
+ 2220
+ 70
+ 40
+
+ File
+
+
+
+ UMLObject
+
+ 1290
+ 2220
+ 80
+ 140
+
+ result
+
+
+
+ UMLObject
+
+ 970
+ 2220
+ 70
+ 140
+
+ time
+of
+last
+lool
+
+
+
+ UMLState
+
+ 1380
+ 2320
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLState
+
+ 1380
+ 2220
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLObject
+
+ 690
+ 2590
+ 150
+ 40
+
+ otherDirectory
+
+
+
+ UMLObject
+
+ 690
+ 2500
+ 150
+ 40
+
+ this.Directory
+
+
+
+ UMLState
+
+ 1270
+ 2500
+ 70
+ 40
+
+ File
+bg=red
+
+
+
+ UMLState
+
+ 980
+ 2600
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLState
+
+ 900
+ 2500
+ 70
+ 40
+
+ File
+
+
+
+ UMLObject
+
+ 1430
+ 2500
+ 80
+ 140
+
+ result
+
+
+
+ UMLObject
+
+ 1070
+ 2500
+ 70
+ 140
+
+ time
+of
+last
+lool
+
+
+
+ UMLState
+
+ 1520
+ 2500
+ 70
+ 40
+
+ File
+bg=red
+
+
+
+ UMLState
+
+ 1520
+ 2600
+ 70
+ 40
+
+ File
+bg=red
+
+
+
+ UMLObject
+
+ 680
+ 2150
+ 720
+ 30
+
+ Deleted Files are tracked by their last existance in a StateFile.
+
+
+
+ UMLObject
+
+ 990
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1060
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLState
+
+ 1160
+ 2500
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLState
+
+ 1160
+ 2600
+ 70
+ 40
+
+ File
+bg=green
+
+
+
+ UMLObject
+
+ 1360
+ 2500
+ 70
+ 140
+
+ current
+loop
+
+
+
+ UMLObject
+
+ 1220
+ 2220
+ 70
+ 140
+
+ current
+loop
+
+
+
+ Relation
+
+ 910
+ 2190
+ 280
+ 50
+
+ lt=-
+ 10.0;30.0;10.0;10.0;260.0;10.0;260.0;30.0
+
+
+ Relation
+
+ 1180
+ 2530
+ 140
+ 160
+
+ lt=<-
+comparison >=
+ 10.0;110.0;10.0;140.0;120.0;140.0;120.0;10.0
+
+
+ UMLObject
+
+ 1280
+ 2060
+ 70
+ 40
+
+ RED
+bg=gray
+
+
+
+ UMLObject
+
+ 1350
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1430
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1500
+ 2060
+ 70
+ 40
+
+ YES
+bg=green
+
+
+
+ UMLObject
+
+ 1570
+ 2060
+ 70
+ 40
+
+ RED
+bg=gray
+
+
+
+ UMLObject
+
+ 1640
+ 2060
+ 70
+ 40
+
+ RED
+bg=gray
+
+
+
+ Relation
+
+ 910
+ 2250
+ 200
+ 160
+
+ lt=<-
+comparison >=
+ 10.0;10.0;10.0;140.0;180.0;140.0;180.0;110.0
+
+
+ UMLState
+
+ 990
+ 590
+ 40
+ 40
+
+ A
+bg=red
+
+
+
+ UMLState
+
+ 1030
+ 590
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1030
+ 630
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1030
+ 550
+ 40
+ 40
+
+ B
+bg=red
+
+
+
+ UMLState
+
+ 1070
+ 550
+ 40
+ 40
+
+
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 750
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 1030
+ 790
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1030
+ 830
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1030
+ 750
+ 40
+ 40
+
+ B
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 950
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 1030
+ 990
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1030
+ 1030
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1030
+ 950
+ 40
+ 40
+
+ B
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 630
+ 40
+ 40
+
+ A
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 550
+ 40
+ 40
+
+ A
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 510
+ 40
+ 40
+
+ A
+bg=red
+
+
+
+ UMLState
+
+ 1070
+ 510
+ 40
+ 40
+
+
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 830
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 990
+ 790
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 990
+ 710
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 990
+ 1030
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 990
+ 990
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 990
+ 910
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 750
+ 40
+ 40
+
+
+bg=red
+
+
+
+ UMLState
+
+ 1070
+ 950
+ 40
+ 40
+
+
+bg=red
+
+
+
+ UMLState
+
+ 1070
+ 590
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1070
+ 790
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1070
+ 990
+ 40
+ 40
+
+ B
+bg=green
+
+
+
+ UMLState
+
+ 1070
+ 630
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 830
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 1030
+ 40
+ 40
+
+ B
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 910
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 710
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLNote
+
+ 1100
+ 150
+ 140
+ 70
+
+ first a file is deleted
+
+
+
+ Relation
+
+ 960
+ 210
+ 160
+ 280
+
+ lt=.
+
+ 50.0;260.0;10.0;120.0;140.0;10.0
+
+
+ UMLState
+
+ 1030
+ 510
+ 40
+ 40
+
+ B
+bg=gray
+
+
+
+ UMLState
+
+ 990
+ 470
+ 40
+ 40
+
+ A
+bg=red
+
+
+
+ UMLState
+
+ 1070
+ 470
+ 40
+ 40
+
+
+bg=red
+
+
+
+ UMLState
+
+ 990
+ 670
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 1070
+ 670
+ 40
+ 40
+
+ A
+bg=green
+
+
+
+ UMLState
+
+ 1030
+ 710
+ 40
+ 40
+
+ B
+bg=gray
+
+
+
+ UMLState
+
+ 990
+ 870
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 1070
+ 870
+ 40
+ 40
+
+ A
+bg=yellow
+
+
+
+ UMLState
+
+ 1030
+ 910
+ 40
+ 40
+
+ B
+bg=gray
+
+
+
+ Relation
+
+ 1100
+ 490
+ 230
+ 100
+
+ lt=.
+
+ 10.0;80.0;210.0;10.0
+
+
+ UMLNote
+
+ 1040
+ 0
+ 140
+ 70
+
+ there is no second file
+
+
+
+ Relation
+
+ 1100
+ 510
+ 320
+ 120
+
+ lt=.
+
+ 10.0;100.0;300.0;10.0
+
+
+ UMLNote
+
+ 1370
+ 50
+ 410
+ 130
+
+ TEST-TABLE:
+/here a is the file that is modified first/
+/and b is the file that is modified second/
+*start*
+file name content
+*end*
+file name content
+
+
+
+ UMLNote
+
+ 1310
+ 390
+ 80
+ 110
+
+ *start*
+a dd a
+b dd b
+*end*
+a -
+b -
+
+
+
+ UMLNote
+
+ 1400
+ 410
+ 80
+ 110
+
+ *start*
+a dc a
+b dc b
+*end*
+a dc b
+b dc b
+
+
+
+ UMLNote
+
+ 1490
+ 430
+ 80
+ 110
+
+ *start*
+a dm a
+b dm b
+*end*
+a dm b
+b dm b
+
+
+
+ Relation
+
+ 1100
+ 530
+ 410
+ 140
+
+ lt=.
+
+ 10.0;120.0;390.0;10.0
+
+
+ UMLNote
+
+ 1540
+ 620
+ 80
+ 110
+
+ *start*
+a cd a
+b cd b
+*end*
+a -
+b -
+
+
+
+ Relation
+
+ 1100
+ 720
+ 460
+ 70
+
+ lt=.
+
+ 10.0;50.0;440.0;10.0
+
+
+ UMLNote
+
+ 1620
+ 860
+ 80
+ 110
+
+ *start*
+a md a
+b md b
+*end*
+a -
+b -
+
+
+
+ Relation
+
+ 1100
+ 950
+ 540
+ 40
+
+ lt=.
+
+ 10.0;20.0;520.0;20.0
+
+
+ UMLNote
+
+ 1630
+ 720
+ 80
+ 110
+
+ *start*
+a cc a
+b cc b
+*end*
+a cc b
+b cc b
+
+
+
+ Relation
+
+ 1100
+ 800
+ 550
+ 50
+
+ lt=.
+
+ 10.0;10.0;530.0;30.0
+
+