diff --git a/src/main/java/blue/contract/packager/DependencyProcessor.java b/src/main/java/blue/contract/packager/DependencyProcessor.java index 86b58fe..c5af9b8 100644 --- a/src/main/java/blue/contract/packager/DependencyProcessor.java +++ b/src/main/java/blue/contract/packager/DependencyProcessor.java @@ -13,7 +13,7 @@ class DependencyProcessor { private final DependencyGraph graph; - private final Map processedPackages; + private final Map> processedPackages; // Map> private final TopologicalSorter topologicalSorter; private final NodePreprocessor nodePreprocessor; private final BluePackageInitializer bluePackageInitializer; @@ -30,16 +30,26 @@ public DependencyProcessor(DependencyGraph graph) { public List process() { List processingOrder = graph.getProcessingOrder(); - for (String dirName : processingOrder) { - processDirectory(dirName); + for (String typeAndVersion : processingOrder) { + String[] parts = typeAndVersion.split("="); + processDirectory(parts[0], parts[1]); } - return new ArrayList<>(processedPackages.values()); + return processedPackages.values().stream() + .flatMap(versionMap -> versionMap.values().stream()) + .toList(); } - private void processDirectory(String dirName) { - DirectoryNode dir = graph.getDirectories().get(dirName); - BluePackage bluePackage = bluePackageInitializer.initialize(dirName, dir.getDependency(), processedPackages); - processedPackages.put(dirName, bluePackage); + private void processDirectory(String typeName, String version) { + DirectoryNode dir = graph.getDirectories().get(typeName).get(version); + BluePackage bluePackage = bluePackageInitializer.initialize( + typeName, + version, + dir.getDependency(), + processedPackages + ); + processedPackages + .computeIfAbsent(typeName, k -> new HashMap<>()) + .put(version, bluePackage); Map nodes = new HashMap<>(dir.getNodes()); Set processed = new HashSet<>(); @@ -47,28 +57,36 @@ private void processDirectory(String dirName) { try { processingOrder = topologicalSorter.sort(nodes); } catch (IllegalStateException e) { - throw new IllegalStateException("Cyclic dependency detected in directory '" + dirName + "': " + e.getMessage()); + throw new IllegalStateException( + String.format("Cyclic dependency detected in directory '%s' version '%s': %s", + typeName, version, e.getMessage()) + ); } for (String nodeName : processingOrder) { - processNode(nodeName, nodes, processed, dirName); + processNode(nodeName, nodes, processed, typeName, version); } } - private void processNode(String nodeName, Map nodes, Set processed, String dirName) { + private void processNode(String nodeName, Map nodes, Set processed, + String typeName, String version) { if (processed.contains(nodeName)) { return; } - Node node = nodes.values().stream().filter(n -> n.getName().equals(nodeName)).findFirst().orElseThrow(); - Node preprocessedNode = nodePreprocessor.preprocess(node, dirName, processedPackages); + Node node = nodes.values().stream() + .filter(n -> n.getName().equals(nodeName)) + .findFirst() + .orElseThrow(); + + Node preprocessedNode = nodePreprocessor.preprocess(node, typeName, version, processedPackages); nodes.put(nodeName, preprocessedNode); - BluePackage bluePackage = processedPackages.get(dirName); + BluePackage bluePackage = processedPackages.get(typeName).get(version); bluePackage.addPreprocessedNode(nodeName, preprocessedNode); - packageMappingsUpdater.update(dirName, nodeName, preprocessedNode, processedPackages); + packageMappingsUpdater.update(typeName, version, nodeName, preprocessedNode, processedPackages); processed.add(nodeName); } } \ No newline at end of file diff --git a/src/main/java/blue/contract/packager/graphbuilder/ClasspathDependencyGraphBuilder.java b/src/main/java/blue/contract/packager/graphbuilder/ClasspathDependencyGraphBuilder.java index 97da42a..2d60e7d 100644 --- a/src/main/java/blue/contract/packager/graphbuilder/ClasspathDependencyGraphBuilder.java +++ b/src/main/java/blue/contract/packager/graphbuilder/ClasspathDependencyGraphBuilder.java @@ -1,6 +1,7 @@ package blue.contract.packager.graphbuilder; import blue.contract.packager.model.DependencyGraph; +import blue.contract.packager.model.TypeDependency; import blue.language.model.Node; import java.io.IOException; @@ -34,16 +35,34 @@ public DependencyGraph buildDependencyGraph(String rootDir) throws IOException { String[] directories = content.split("\n"); for (String dir : directories) { - String dependency = readDependency(rootDir + "/" + dir + "/_extends.txt"); - graph.addDirectory(dir, dependency); - processFiles(rootDir, dir, graph); + String typePath = rootDir + "/" + dir; + String[] versions = getVersions(typePath); + + for (String version : versions) { + TypeDependency dependency = readDependency(typePath + "/" + version + "/" + EXTENDS_FILE_NAME); + graph.addDirectory(dir, version, dependency); + processFiles(rootDir, dir, version, graph); + } } } return graph; } - private String readDependency(String path) throws IOException { + private String[] getVersions(String typePath) throws IOException { + URL typeUrl = classLoader.getResource(typePath); + if (typeUrl == null) { + throw new IOException("Type directory not found: " + typePath); + } + + try (InputStream is = typeUrl.openStream()) { + Scanner scanner = new Scanner(is).useDelimiter("\\A"); + String content = scanner.hasNext() ? scanner.next() : ""; + return content.split("\n"); + } + } + + private TypeDependency readDependency(String path) throws IOException { try (InputStream is = classLoader.getResourceAsStream(path)) { if (is == null) { throw new IOException("Dependency file not found: " + path); @@ -53,12 +72,12 @@ private String readDependency(String path) throws IOException { if (content.isEmpty() || content.lines().count() != 1) { throw new IOException("Invalid dependency file format. Expected one line in: " + path); } - return content; + return TypeDependency.parse(content); } } - private void processFiles(String rootDir, String dirName, DependencyGraph graph) throws IOException { - String dirPath = rootDir + "/" + dirName + "/"; + private void processFiles(String rootDir, String dirName, String version, DependencyGraph graph) throws IOException { + String dirPath = rootDir + "/" + dirName + "/" + version + "/"; URL dirUrl = classLoader.getResource(dirPath); if (dirUrl == null) { return; @@ -75,7 +94,7 @@ private void processFiles(String rootDir, String dirName, DependencyGraph graph) try (InputStream fileIs = classLoader.getResourceAsStream(filePath)) { if (fileIs != null) { Node contract = YAML_MAPPER.readValue(fileIs, Node.class); - graph.addNode(dirName, contract); + graph.addNode(dirName, version, contract); } } } diff --git a/src/main/java/blue/contract/packager/graphbuilder/FileSystemDependencyGraphBuilder.java b/src/main/java/blue/contract/packager/graphbuilder/FileSystemDependencyGraphBuilder.java index dccb095..066b3d9 100644 --- a/src/main/java/blue/contract/packager/graphbuilder/FileSystemDependencyGraphBuilder.java +++ b/src/main/java/blue/contract/packager/graphbuilder/FileSystemDependencyGraphBuilder.java @@ -1,6 +1,7 @@ package blue.contract.packager.graphbuilder; import blue.contract.packager.model.DependencyGraph; +import blue.contract.packager.model.TypeDependency; import blue.language.model.Node; import java.io.IOException; @@ -27,13 +28,11 @@ public DependencyGraph buildDependencyGraph(String rootDir) throws IOException { throw new IOException("Root directory not found: " + fullPath); } - try (DirectoryStream stream = Files.newDirectoryStream(fullPath)) { - for (Path path : stream) { - if (Files.isDirectory(path)) { - String dirName = path.getFileName().toString(); - String dependency = readDependency(path.resolve(EXTENDS_FILE_NAME)); - graph.addDirectory(dirName, dependency); - processFiles(path, dirName, graph); + try (DirectoryStream typeStream = Files.newDirectoryStream(fullPath)) { + for (Path typePath : typeStream) { + if (Files.isDirectory(typePath)) { + String typeName = typePath.getFileName().toString(); + processTypeDirectory(typePath, typeName, graph); } } } @@ -41,7 +40,20 @@ public DependencyGraph buildDependencyGraph(String rootDir) throws IOException { return graph; } - private String readDependency(Path path) throws IOException { + private void processTypeDirectory(Path typePath, String typeName, DependencyGraph graph) throws IOException { + try (DirectoryStream versionStream = Files.newDirectoryStream(typePath)) { + for (Path versionPath : versionStream) { + if (Files.isDirectory(versionPath)) { + String version = versionPath.getFileName().toString(); + TypeDependency dependency = readDependency(versionPath.resolve(EXTENDS_FILE_NAME)); + graph.addDirectory(typeName, version, dependency); + processFiles(versionPath, typeName, version, graph); + } + } + } + } + + private TypeDependency readDependency(Path path) throws IOException { if (!Files.exists(path)) { throw new IOException("Dependency file not found: " + path); } @@ -49,16 +61,19 @@ private String readDependency(Path path) throws IOException { if (content.isEmpty() || content.lines().count() != 1) { throw new IOException("Invalid dependency file format. Expected one line in: " + path); } - return content; + try { + return TypeDependency.parse(content); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid dependency format in file: " + path + ". " + e.getMessage()); + } } - private void processFiles(Path dirPath, String dirName, DependencyGraph graph) throws IOException { - try (DirectoryStream stream = Files.newDirectoryStream(dirPath, "*.blue")) { + private void processFiles(Path versionPath, String typeName, String version, DependencyGraph graph) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(versionPath, "*.blue")) { for (Path file : stream) { Node contract = YAML_MAPPER.readValue(Files.readString(file), Node.class); - graph.addNode(dirName, contract); + graph.addNode(typeName, version, contract); } } } - } \ No newline at end of file diff --git a/src/main/java/blue/contract/packager/graphbuilder/SequentialDependencyGraphBuilder.java b/src/main/java/blue/contract/packager/graphbuilder/SequentialDependencyGraphBuilder.java index 37173fe..eba3b93 100644 --- a/src/main/java/blue/contract/packager/graphbuilder/SequentialDependencyGraphBuilder.java +++ b/src/main/java/blue/contract/packager/graphbuilder/SequentialDependencyGraphBuilder.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; public class SequentialDependencyGraphBuilder implements DependencyGraphBuilder { private final List builders; @@ -27,17 +28,29 @@ public DependencyGraph buildDependencyGraph(String rootDir) throws IOException { } private void mergeGraphs(DependencyGraph target, DependencyGraph source) { - for (String dirName : source.getDirectories().keySet()) { - DirectoryNode sourceDir = source.getDirectories().get(dirName); - DirectoryNode targetDir = target.getDirectories().get(dirName); - - if (targetDir != null) { - throw new IllegalStateException("Directory name collision detected: " + dirName); - } - - target.addDirectory(dirName, sourceDir.getDependency()); - for (Node node : sourceDir.getNodes().values()) { - target.addNode(dirName, node); + for (Map.Entry> typeEntry : source.getDirectories().entrySet()) { + String typeName = typeEntry.getKey(); + Map sourceVersions = typeEntry.getValue(); + + for (Map.Entry versionEntry : sourceVersions.entrySet()) { + String version = versionEntry.getKey(); + DirectoryNode sourceDir = versionEntry.getValue(); + + // Check if this type+version combination already exists in the target + Map targetVersions = target.getDirectories().get(typeName); + if (targetVersions != null && targetVersions.containsKey(version)) { + throw new IllegalStateException( + String.format("Directory collision detected: %s version %s", typeName, version) + ); + } + + // Add the directory with its dependency + target.addDirectory(typeName, version, sourceDir.getDependency()); + + // Add all nodes from the source directory + for (Node node : sourceDir.getNodes().values()) { + target.addNode(typeName, version, node); + } } } } diff --git a/src/main/java/blue/contract/packager/model/BluePackage.java b/src/main/java/blue/contract/packager/model/BluePackage.java index ba4bda7..0703802 100644 --- a/src/main/java/blue/contract/packager/model/BluePackage.java +++ b/src/main/java/blue/contract/packager/model/BluePackage.java @@ -7,22 +7,27 @@ public class BluePackage { private final String directoryName; + private final String version; private final Node packageContent; private Map mappings; private Map preprocessedNodes; - public BluePackage(String directoryName, Node packageContent) { + public BluePackage(String directoryName, String version, Node packageContent) { this.directoryName = directoryName; + this.version = version; this.packageContent = packageContent; this.mappings = new HashMap<>(); this.preprocessedNodes = new HashMap<>(); - } public String getDirectoryName() { return directoryName; } + public String getVersion() { + return version; + } + public Node getPackageContent() { return packageContent; } diff --git a/src/main/java/blue/contract/packager/model/DependencyGraph.java b/src/main/java/blue/contract/packager/model/DependencyGraph.java index cbf5c24..c9ff9e9 100644 --- a/src/main/java/blue/contract/packager/model/DependencyGraph.java +++ b/src/main/java/blue/contract/packager/model/DependencyGraph.java @@ -7,20 +7,20 @@ import static blue.contract.packager.BluePackageExporter.ROOT_DEPENDENCY; public class DependencyGraph { - private Map directories; + private Map> directories; // Map> public DependencyGraph() { this.directories = new HashMap<>(); } - public void addDirectory(String name, String dependency) { - DirectoryNode node = new DirectoryNode(name); + public void addDirectory(String name, String version, TypeDependency dependency) { + DirectoryNode node = new DirectoryNode(name, version); node.setDependency(dependency); - directories.put(name, node); + directories.computeIfAbsent(name, k -> new HashMap<>()).put(version, node); } - public void addNode(String dirName, Node node) { - directories.get(dirName).addNode(node); + public void addNode(String dirName, String version, Node node) { + directories.get(dirName).get(version).addNode(node); } public List getProcessingOrder() { @@ -28,44 +28,48 @@ public List getProcessingOrder() { Set visited = new HashSet<>(); Set recursionStack = new HashSet<>(); - for (String dirName : directories.keySet()) { - if (hasCyclicDependency(dirName, visited, recursionStack, order)) { - throw new IllegalStateException("Cyclic dependency detected in directory structure: " + - String.join(" -> ", recursionStack) + " -> " + dirName); + for (Map.Entry> entry : directories.entrySet()) { + String dirName = entry.getKey(); + for (String version : entry.getValue().keySet()) { + if (hasCyclicDependency(dirName, version, visited, recursionStack, order)) { + throw new IllegalStateException("Cyclic dependency detected in directory structure: " + + String.join(" -> ", recursionStack) + " -> " + dirName + "=" + version); + } } } return order; } - private boolean hasCyclicDependency(String dirName, Set visited, Set recursionStack, List order) { - if (recursionStack.contains(dirName)) { + private boolean hasCyclicDependency(String dirName, String version, Set visited, Set recursionStack, List order) { + String key = dirName + "=" + version; + if (recursionStack.contains(key)) { return true; } - if (visited.contains(dirName)) { + if (visited.contains(key)) { return false; } - visited.add(dirName); - recursionStack.add(dirName); + visited.add(key); + recursionStack.add(key); - DirectoryNode node = directories.get(dirName); - String dep = node.getDependency(); - if (!dep.equals(ROOT_DEPENDENCY) && hasCyclicDependency(dep, visited, recursionStack, order)) { + DirectoryNode node = directories.get(dirName).get(version); + TypeDependency dep = node.getDependency(); + if (!dep.isRoot() && hasCyclicDependency(dep.getTypeName(), dep.getVersion(), visited, recursionStack, order)) { return true; } - recursionStack.remove(dirName); - order.add(dirName); + recursionStack.remove(key); + order.add(key); return false; } - public Map getDirectories() { + public Map> getDirectories() { return directories; } - public DependencyGraph setDirectories(Map directories) { + public DependencyGraph setDirectories(Map> directories) { this.directories = directories; return this; } diff --git a/src/main/java/blue/contract/packager/model/DirectoryNode.java b/src/main/java/blue/contract/packager/model/DirectoryNode.java index d48769e..33be4de 100644 --- a/src/main/java/blue/contract/packager/model/DirectoryNode.java +++ b/src/main/java/blue/contract/packager/model/DirectoryNode.java @@ -7,12 +7,14 @@ public class DirectoryNode { private String name; - private String dependency; + private String version; + private TypeDependency dependency; private Map nodes; - public DirectoryNode(String name) { + public DirectoryNode(String name, String version) { this.name = name; - this.dependency = ""; + this.version = version; + this.dependency = TypeDependency.ROOT; this.nodes = new HashMap<>(); } @@ -25,11 +27,11 @@ public DirectoryNode setName(String name) { return this; } - public String getDependency() { + public TypeDependency getDependency() { return dependency; } - public DirectoryNode setDependency(String dependency) { + public DirectoryNode setDependency(TypeDependency dependency) { this.dependency = dependency; return this; } @@ -43,6 +45,15 @@ public DirectoryNode setNodes(Map nodes) { return this; } + public String getVersion() { + return version; + } + + public DirectoryNode setVersion(String version) { + this.version = version; + return this; + } + public void addNode(Node node) { String nodeName = node.getName(); if (nodes.containsKey(nodeName)) { diff --git a/src/main/java/blue/contract/packager/model/TypeDependency.java b/src/main/java/blue/contract/packager/model/TypeDependency.java new file mode 100644 index 0000000..df3b04a --- /dev/null +++ b/src/main/java/blue/contract/packager/model/TypeDependency.java @@ -0,0 +1,55 @@ +package blue.contract.packager.model; + +import java.util.*; + +import static blue.contract.packager.BluePackageExporter.ROOT_DEPENDENCY; + +public class TypeDependency { + private final String typeName; + private final String version; + + public static final TypeDependency ROOT = new TypeDependency(ROOT_DEPENDENCY, null); + + public TypeDependency(String typeName, String version) { + this.typeName = typeName; + this.version = version; + } + + public String getTypeName() { + return typeName; + } + + public String getVersion() { + return version; + } + + public static TypeDependency parse(String dependencyString) { + if (dependencyString.equals(ROOT_DEPENDENCY)) { + return ROOT; + } + + String[] parts = dependencyString.split("="); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid dependency format. Expected 'type=version' or 'ROOT', got: " + dependencyString); + } + + return new TypeDependency(parts[0].trim(), parts[1].trim()); + } + + public boolean isRoot() { + return this.equals(ROOT); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypeDependency that = (TypeDependency) o; + return Objects.equals(typeName, that.typeName) && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(typeName, version); + } +} diff --git a/src/main/java/blue/contract/packager/utils/BluePackageInitializer.java b/src/main/java/blue/contract/packager/utils/BluePackageInitializer.java index 34f0a00..b983b7b 100644 --- a/src/main/java/blue/contract/packager/utils/BluePackageInitializer.java +++ b/src/main/java/blue/contract/packager/utils/BluePackageInitializer.java @@ -1,6 +1,7 @@ package blue.contract.packager.utils; import blue.contract.packager.model.BluePackage; +import blue.contract.packager.model.TypeDependency; import blue.language.model.Node; import blue.language.utils.BlueIdCalculator; @@ -11,24 +12,33 @@ import static blue.contract.packager.BluePackageExporter.*; public class BluePackageInitializer { - public BluePackage initialize(String dirName, String dependency, Map processedPackages) { + public BluePackage initialize(String dirName, String version, TypeDependency dependency, + Map> processedPackages) { List packageItems = new ArrayList<>(); - String blueId = dependency.equals(ROOT_DEPENDENCY) ? BOOTSTRAP_BLUE_ID : calculateDependencyBlueId(dependency, processedPackages); + String blueId = dependency.isRoot() + ? BOOTSTRAP_BLUE_ID + : calculateDependencyBlueId(dependency, processedPackages); packageItems.add(new Node().blueId(blueId)); Node typeNode = new Node().type(new Node().blueId(REPLACE_INLINE_TYPES_WITH_BLUE_ID_TRANSFORMER_BLUE_ID)); packageItems.add(typeNode); Node packageContent = new Node().items(packageItems); - return new BluePackage(dirName, packageContent); + return new BluePackage(dirName, version, packageContent); } - private String calculateDependencyBlueId(String dependency, Map processedPackages) { - BluePackage dependencyPackage = processedPackages.get(dependency); + private String calculateDependencyBlueId(TypeDependency dependency, + Map> processedPackages) { + BluePackage dependencyPackage = processedPackages + .getOrDefault(dependency.getTypeName(), Map.of()) + .get(dependency.getVersion()); if (dependencyPackage == null) { - throw new IllegalStateException("Dependency package not found: " + dependency); + throw new IllegalStateException( + String.format("Dependency package not found: %s=%s", + dependency.getTypeName(), dependency.getVersion()) + ); } Node dependencyContent = dependencyPackage.getPackageContent(); diff --git a/src/main/java/blue/contract/packager/utils/NodePreprocessor.java b/src/main/java/blue/contract/packager/utils/NodePreprocessor.java index d5c8a39..31c7dcb 100644 --- a/src/main/java/blue/contract/packager/utils/NodePreprocessor.java +++ b/src/main/java/blue/contract/packager/utils/NodePreprocessor.java @@ -17,14 +17,16 @@ import static blue.language.utils.UncheckedObjectMapper.YAML_MAPPER; public class NodePreprocessor { - public Node preprocess(Node node, String dirName, Map processedPackages) { - List packageContents = new ArrayList<>(processedPackages.values().stream() - .map(BluePackage::getPackageContent) - .collect(Collectors.toList())); + public Node preprocess(Node node, String dirName, String version, + Map> processedPackages) { + List packageContents = processedPackages.values().stream() + .flatMap(versionMap -> versionMap.values().stream()) + .map(BluePackage::getPackageContent) + .collect(Collectors.toList()); NodeProvider nodeProvider = NodeProviderWrapper.wrap(new BasicNodeProvider(packageContents)); Preprocessor preprocessor = new Preprocessor(nodeProvider); - Node blueNode = processedPackages.get(dirName).getPackageContent(); + Node blueNode = processedPackages.get(dirName).get(version).getPackageContent(); return preprocessor.preprocess(node, blueNode); } } \ No newline at end of file diff --git a/src/main/java/blue/contract/packager/utils/PackageMappingsUpdater.java b/src/main/java/blue/contract/packager/utils/PackageMappingsUpdater.java index 32b0047..ffe23b3 100644 --- a/src/main/java/blue/contract/packager/utils/PackageMappingsUpdater.java +++ b/src/main/java/blue/contract/packager/utils/PackageMappingsUpdater.java @@ -8,9 +8,10 @@ import java.util.Map; public class PackageMappingsUpdater { - public void update(String dirName, String nodeName, Node preprocessedNode, Map processedPackages) { + public void update(String dirName, String version, String nodeName, Node preprocessedNode, + Map> processedPackages) { String blueId = BlueIdCalculator.calculateBlueId(preprocessedNode); - BluePackage bluePackage = processedPackages.get(dirName); + BluePackage bluePackage = processedPackages.get(dirName).get(version); Node packageContent = bluePackage.getPackageContent(); Node packageMappingsNode = packageContent.getItems().get(1); @@ -18,14 +19,14 @@ public void update(String dirName, String nodeName, Node preprocessedNode, Map()); } - Node mappingsNode = packageMappingsNode.getProperties().computeIfAbsent("mappings", k -> new Node()); + Node mappingsNode = packageMappingsNode.getProperties() + .computeIfAbsent("mappings", k -> new Node()); if (mappingsNode.getProperties() == null) { mappingsNode.properties(new HashMap<>()); } mappingsNode.getProperties().put(nodeName, new Node().value(blueId)); - bluePackage.getMappings().put(nodeName, blueId); } } \ No newline at end of file