diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8674590 --- /dev/null +++ b/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + com.olexyn.ensync + ensync + 0.1 + + ensync + http://www.example.com + + + UTF-8 + 1.11 + 1.11 + + + + + junit + junit + 4.11 + test + + + + javafx + javafx.base + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.base.jar + + + + javafx + javafx.fxml + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.fxml.jar + + + + javafx + javafx.controls + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.controls.jar + + + + javafx + javafx.graphics + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.graphics.jar + + + + javafx + javafx.media + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.media.jar + + + + javafx + javafx.swing + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.swing.jar + + + + javafx + javafx.web + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx.web.jar + + + + javafx + javafx.swt + 11.0.2 + system + /home/user/app/javafx-sdk-11.0.2/lib/javafx-swt.jar + + + + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/src/main/java/com/olexyn/ensync/App.java b/src/main/java/com/olexyn/ensync/App.java new file mode 100644 index 0000000..c659c27 --- /dev/null +++ b/src/main/java/com/olexyn/ensync/App.java @@ -0,0 +1,13 @@ +package com.olexyn.ensync; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/src/main/java/com/olexyn/ensync/Execute.java b/src/main/java/com/olexyn/ensync/Execute.java new file mode 100644 index 0000000..e92f362 --- /dev/null +++ b/src/main/java/com/olexyn/ensync/Execute.java @@ -0,0 +1,54 @@ +package com.olexyn.ensync; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.List; + +public class Execute { + + + /** + * @param cmd an array representing a shell command + * @return TwoBr class, containing two BufferedReaders, + * output and error + * @see output BufferedReader, corresponds to STDOUT + * error BufferedReader, corresponds to STDERR + */ + public TwoBr execute(String cmd[]) { + TwoBr twobr = new TwoBr(); + try { + Process process = Runtime.getRuntime().exec(cmd); + process.waitFor(); + twobr.output = new BufferedReader(new InputStreamReader(process.getInputStream())); + twobr.error = new BufferedReader(new InputStreamReader(process.getErrorStream())); + } catch (Exception e) { + e.printStackTrace(); + } + return twobr; + } + + + public TwoBr execute(List cmd) { + + String[] cmdArr = new String[cmd.size()]; + for (int i = 0; i < cmd.size(); i++) { + cmdArr[i] = cmd.get(i); + } + + return execute(cmdArr); + } + + public void executeBatch(List 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 + +