Skip to content

Commit 533fc5a

Browse files
hohwillecthies-capgeminijan-vcapgemini
authored
#1166 automatic project import for intellij #1508: fix xml merge when file is empty (#1649)
Co-authored-by: cthies <[email protected]> Co-authored-by: jan-vcapgemini <[email protected]> Co-authored-by: jan-vcapgemini <[email protected]>
1 parent 5d72ee4 commit 533fc5a

File tree

17 files changed

+366
-22
lines changed

17 files changed

+366
-22
lines changed

CHANGELOG.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE
44

55
== 2025.12.001
66

7+
* https://github.com/devonfw/IDEasy/issues/1166[#1166]: Automatic project import for IntelliJ
8+
* https://github.com/devonfw/IDEasy/issues/1508[#1508]: xml merger fails when merging empty file
9+
710
Release with new features and bugfixes:
811

912
* https://github.com/devonfw/IDEasy/issues/39[#39]: Implement ToolCommandlet for pip
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.devonfw.tools.ide.environment;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import com.devonfw.tools.ide.context.IdeContext;
7+
8+
/**
9+
* Subclass of {@link EnvironmentVariablesMap} that resolves variables recursively and allows new variables to be added to the resolver.
10+
*/
11+
public class ExtensibleEnvironmentVariables extends EnvironmentVariablesMap {
12+
13+
private final Map<String, String> variables;
14+
15+
/**
16+
* The constructor.
17+
*
18+
* @param parent the parent {@link EnvironmentVariables} to inherit from.
19+
* @param context the context to use.
20+
*/
21+
public ExtensibleEnvironmentVariables(AbstractEnvironmentVariables parent, IdeContext context) {
22+
super(parent, context);
23+
this.variables = new HashMap<>();
24+
}
25+
26+
/**
27+
* @param name the name of the variable to set.
28+
* @param value the value of the variable to set.
29+
*/
30+
public void setValue(String name, String value) {
31+
this.variables.put(name, value);
32+
}
33+
34+
@Override
35+
protected String getValue(String name, boolean ignoreDefaultValue) {
36+
String value = this.variables.get(name);
37+
if (value != null) {
38+
return value;
39+
}
40+
return super.getValue(name, ignoreDefaultValue);
41+
}
42+
43+
@Override
44+
protected Map<String, String> getVariables() {
45+
return this.variables;
46+
}
47+
48+
@Override
49+
public EnvironmentVariablesType getType() {
50+
return EnvironmentVariablesType.TOOL;
51+
}
52+
}

cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ default void copy(Path source, Path target) {
154154
/**
155155
* @param source the source {@link Path file or folder} to copy.
156156
* @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
157-
* and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
158-
* {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
157+
* and copy that to {@code target} in case that is an existing folder. Instead, it will always be simple and stupid and just copy from {@code source} to
158+
* {@code target}. Therefore, the result is always clear and easy to predict and understand. Also, you can easily rename a file to copy. While
159159
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
160160
* the same content of {@code source} in {@code target}.
161161
* @param mode the {@link FileCopyMode}.
@@ -168,8 +168,8 @@ default void copy(Path source, Path target, FileCopyMode mode) {
168168
/**
169169
* @param source the source {@link Path file or folder} to copy.
170170
* @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
171-
* and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
172-
* {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
171+
* and copy that to {@code target} in case that is an existing folder. Instead, it will always be simple and stupid and just copy from {@code source} to
172+
* {@code target}. Therefore, the result is always clear and easy to predict and understand. Also, you can easily rename a file to copy. While
173173
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
174174
* the same content of {@code source} in {@code target}.
175175
* @param mode the {@link FileCopyMode}.
@@ -331,6 +331,20 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac
331331
*/
332332
Path findFirst(Path dir, Predicate<Path> filter, boolean recursive);
333333

334+
/**
335+
* Example usage:
336+
* <pre>
337+
* findAncestor(ideHome.resolve("workspaces/test/foo/bar"), ideHome.resolve("workspaces"), 1); // will return ideHome.resolve("workspaces/test")
338+
* </pre>
339+
*
340+
* @param path the {@link Path} to the file or directory to find the ancestor from.
341+
* @param baseDir the {@link Path} to the base-directory is supposed to be a direct or indirect {@link Path#getParent() parent} of {@code path}.
342+
* @param subfolderCount the number of sub-folders of {@code baseDir} to retain from {@code path}.
343+
* @return the {@link Path} pointing to {@code subfolderCount} sub-folders from {@code baseDir} that is still equal or a {@link Path#getParent() parent} of
344+
* {@code directory} or {@code null} if no such {@link Path} exists.
345+
*/
346+
Path findAncestor(Path path, Path baseDir, int subfolderCount);
347+
334348
/**
335349
* @param dir the {@link Path} to the directory where to list the children.
336350
* @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if {@code true} is returned).
@@ -510,7 +524,7 @@ default void writeProperties(Properties properties, Path file) {
510524
/**
511525
* @param properties the {@link Properties} to save.
512526
* @param file the {@link Path} to the file where to save the properties.
513-
* @param createParentDir if {@code true}, the parent directory will created if it does not already exist, {@code false} otherwise (fail if parent does
527+
* @param createParentDir if {@code true}, the parent directory will be created if it does not already exist, {@code false} otherwise (fail if parent does
514528
* not exist).
515529
*/
516530
void writeProperties(Properties properties, Path file, boolean createParentDir);
@@ -586,4 +600,10 @@ default Path getBinParentPath(Path binPath) {
586600
}
587601
return binPath;
588602
}
603+
604+
/**
605+
* @param file the {@link Path} the potential file.
606+
* @return if the given {@code file} exists and is not empty.
607+
*/
608+
boolean isNonEmptyFile(Path file);
589609
}

cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,42 @@ private Path findFirstRecursive(Path dir, Predicate<Path> filter, boolean recurs
10251025
return null;
10261026
}
10271027

1028+
@Override
1029+
public Path findAncestor(Path path, Path baseDir, int subfolderCount) {
1030+
1031+
if ((path == null) || (baseDir == null)) {
1032+
this.context.debug("Path should not be null for findAncestor.");
1033+
return null;
1034+
}
1035+
if (subfolderCount <= 0) {
1036+
throw new IllegalArgumentException("Subfolder count: " + subfolderCount);
1037+
}
1038+
// 1. option relativize
1039+
// 2. recursive getParent
1040+
// 3. loop getParent???
1041+
// 4. getName + getNameCount
1042+
path = path.toAbsolutePath().normalize();
1043+
baseDir = baseDir.toAbsolutePath().normalize();
1044+
int directoryNameCount = path.getNameCount();
1045+
int baseDirNameCount = baseDir.getNameCount();
1046+
int delta = directoryNameCount - baseDirNameCount - subfolderCount;
1047+
if (delta < 0) {
1048+
return null;
1049+
}
1050+
// ensure directory is a sub-folder of baseDir
1051+
for (int i = 0; i < baseDirNameCount; i++) {
1052+
if (!path.getName(i).toString().equals(baseDir.getName(i).toString())) {
1053+
return null;
1054+
}
1055+
}
1056+
Path result = path;
1057+
while (delta > 0) {
1058+
result = result.getParent();
1059+
delta--;
1060+
}
1061+
return result;
1062+
}
1063+
10281064
@Override
10291065
public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
10301066

@@ -1060,6 +1096,15 @@ public boolean isEmptyDir(Path dir) {
10601096
return listChildren(dir, f -> true).isEmpty();
10611097
}
10621098

1099+
@Override
1100+
public boolean isNonEmptyFile(Path file) {
1101+
1102+
if (Files.isRegularFile(file)) {
1103+
return (getFileSize(file) > 0);
1104+
}
1105+
return false;
1106+
}
1107+
10631108
private long getFileSize(Path file) {
10641109

10651110
try {

cli/src/main/java/com/devonfw/tools/ide/merge/xml/XmlMergeSupport.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ static String escapeSingleQuotes(String value) {
163163
*/
164164
static QName getQualifiedName(Element element) {
165165

166+
if (element == null) {
167+
return null;
168+
}
166169
String namespaceURI = element.getNamespaceURI();
167170
String localName = element.getLocalName();
168171
if (localName == null) {

cli/src/main/java/com/devonfw/tools/ide/merge/xml/XmlMerger.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,26 @@ protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, P
112112
public Document merge(XmlMergeDocument templateDocument, XmlMergeDocument workspaceDocument, boolean workspaceFileExists) {
113113

114114
Document resultDocument;
115-
Path source = templateDocument.getPath();
116-
Path template = workspaceDocument.getPath();
115+
Path template = templateDocument.getPath();
116+
Path source = workspaceDocument.getPath();
117117
this.context.debug("Merging {} into {} ...", template, source);
118118
Element templateRoot = templateDocument.getRoot();
119119
QName templateQName = XmlMergeSupport.getQualifiedName(templateRoot);
120+
Document document = workspaceDocument.getDocument();
120121
Element workspaceRoot = workspaceDocument.getRoot();
122+
if (workspaceRoot == null) {
123+
workspaceRoot = (Element) document.importNode(templateRoot, false);
124+
NamedNodeMap attributes = workspaceRoot.getAttributes();
125+
int length = attributes.getLength();
126+
for (int i = 0; i < length; i++) {
127+
Attr attribute = (Attr) attributes.item(i);
128+
if (XmlMergeSupport.hasMergeNamespace(attribute)) {
129+
workspaceRoot.removeAttributeNode(attribute);
130+
}
131+
}
132+
workspaceRoot.removeAttributeNS(XmlMergeSupport.MERGE_NS_URI, "xmlns:xsi");
133+
document.appendChild(workspaceRoot);
134+
}
121135
QName workspaceQName = XmlMergeSupport.getQualifiedName(workspaceRoot);
122136
if (templateQName.equals(workspaceQName)) {
123137
XmlMergeStrategy strategy = XmlMergeSupport.getMergeStrategy(templateRoot);
@@ -139,7 +153,7 @@ public Document merge(XmlMergeDocument templateDocument, XmlMergeDocument worksp
139153
}
140154
ElementMatcher elementMatcher = new ElementMatcher(this.context, templateDocument.getPath(), workspaceDocument.getPath());
141155
strategy.merge(templateRoot, workspaceRoot, elementMatcher);
142-
resultDocument = workspaceDocument.getDocument();
156+
resultDocument = document;
143157
} else {
144158
this.context.error("Cannot merge XML template {} with root {} into XML file {} with root {} as roots do not match.", templateDocument.getPath(),
145159
templateQName, workspaceDocument.getPath(), workspaceQName);
@@ -177,12 +191,17 @@ public XmlMergeDocument loadAndResolve(Path file, EnvironmentVariables variables
177191
*/
178192
public XmlMergeDocument load(Path file) {
179193

180-
try (InputStream in = Files.newInputStream(file)) {
181-
Document document = DOCUMENT_BUILDER.parse(in);
182-
return new XmlMergeDocument(document, file);
183-
} catch (Exception e) {
184-
throw new IllegalStateException("Failed to load XML from: " + file, e);
194+
Document document;
195+
if (this.context.getFileAccess().isNonEmptyFile(file)) {
196+
try (InputStream in = Files.newInputStream(file)) {
197+
document = DOCUMENT_BUILDER.parse(in);
198+
} catch (Exception e) {
199+
throw new IllegalStateException("Failed to load XML from: " + file, e);
200+
}
201+
} else {
202+
document = DOCUMENT_BUILDER.newDocument();
185203
}
204+
return new XmlMergeDocument(document, file);
186205
}
187206

188207
/**

cli/src/main/java/com/devonfw/tools/ide/tool/gradle/Gradle.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
*/
1818
public class Gradle extends LocalToolCommandlet {
1919

20-
private static final String BUILD_GRADLE = "build.gradle";
21-
private static final String BUILD_GRADLE_KTS = "build.gradle.kts";
20+
/** build.gradle file name */
21+
public static final String BUILD_GRADLE = "build.gradle";
22+
23+
/** build.gradle.kts file name */
24+
public static final String BUILD_GRADLE_KTS = "build.gradle.kts";
2225
private static final String GRADLE_WRAPPER_FILENAME = "gradlew";
2326

2427
/**

cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
package com.devonfw.tools.ide.tool.intellij;
22

33
import java.nio.file.Files;
4+
import java.nio.file.Path;
5+
import java.util.Map;
6+
import java.util.Map.Entry;
47
import java.util.Set;
58

9+
import org.w3c.dom.Document;
10+
11+
import com.devonfw.tools.ide.cli.CliException;
12+
import com.devonfw.tools.ide.commandlet.CommandletManager;
613
import com.devonfw.tools.ide.common.Tag;
714
import com.devonfw.tools.ide.context.IdeContext;
15+
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
16+
import com.devonfw.tools.ide.environment.EnvironmentVariables;
17+
import com.devonfw.tools.ide.environment.ExtensibleEnvironmentVariables;
18+
import com.devonfw.tools.ide.merge.xml.XmlMergeDocument;
19+
import com.devonfw.tools.ide.merge.xml.XmlMerger;
820
import com.devonfw.tools.ide.process.EnvironmentContext;
21+
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
922
import com.devonfw.tools.ide.tool.ToolInstallation;
23+
import com.devonfw.tools.ide.tool.gradle.Gradle;
1024
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
1125
import com.devonfw.tools.ide.tool.ide.IdeaBasedIdeToolCommandlet;
26+
import com.devonfw.tools.ide.tool.mvn.Mvn;
1227

1328
/**
1429
* {@link IdeToolCommandlet} for <a href="https://www.jetbrains.com/idea/">IntelliJ</a>.
@@ -21,6 +36,14 @@ public class Intellij extends IdeaBasedIdeToolCommandlet {
2136

2237
private static final String IDEA_BASH_SCRIPT = IDEA + ".sh";
2338

39+
private static final String FOLDER_IDEA_CONFIG = ".idea";
40+
private static final String TEMPLATE_LOCATION = "intellij/workspace/repository/" + FOLDER_IDEA_CONFIG;
41+
private static final String GRADLE_XML = "gradle.xml";
42+
private static final String MISC_XML = "misc.xml";
43+
private static final String IDEA_PROPERTIES = "idea.properties";
44+
45+
private static final Map<Class<? extends LocalToolCommandlet>, String> BUILD_TOOL_TO_IJ_TEMPLATE = Map.of(Mvn.class, MISC_XML, Gradle.class, GRADLE_XML);
46+
2447
/**
2548
* The constructor.
2649
*
@@ -49,9 +72,58 @@ protected String getBinaryName() {
4972

5073
@Override
5174
public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
52-
5375
super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
54-
environmentContext.withEnvVar("IDEA_PROPERTIES", this.context.getWorkspacePath().resolve("idea.properties").toString());
76+
environmentContext.withEnvVar("IDEA_PROPERTIES", this.context.getWorkspacePath().resolve(IDEA_PROPERTIES).toString());
77+
}
78+
79+
private EnvironmentVariables getIntellijEnvironmentVariables(Path projectPath) {
80+
ExtensibleEnvironmentVariables environmentVariables = new ExtensibleEnvironmentVariables(
81+
(AbstractEnvironmentVariables) this.context.getVariables().getParent(), this.context);
82+
83+
environmentVariables.setValue("PROJECT_PATH", projectPath.toString().replace('\\', '/'));
84+
return environmentVariables.resolved();
85+
}
86+
87+
private void mergeConfig(Path repositoryPath, String configFilePath) {
88+
Path templatePath = this.context.getSettingsPath().resolve(TEMPLATE_LOCATION);
89+
Path templateFile = templatePath.resolve(configFilePath);
90+
if (!Files.exists(templateFile)) {
91+
throw new CliException(
92+
"Cannot import project into workspace: template file not found at " + templateFile + "\n"
93+
+ "Please do an upstream merge of your settings git repository.");
94+
}
95+
Path workspacesPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_WORKSPACES);
96+
Path workspacePath = this.context.getFileAccess().findAncestor(repositoryPath, workspacesPath, 1);
97+
if (workspacePath == null) {
98+
throw new CliException(
99+
"Cannot import project into workspace: could not find workspace from " + repositoryPath);
100+
}
101+
XmlMerger xmlMerger = new XmlMerger(this.context);
102+
EnvironmentVariables environmentVariables = getIntellijEnvironmentVariables(workspacePath.relativize(repositoryPath));
103+
Path workspaceFile = workspacePath.resolve(FOLDER_IDEA_CONFIG).resolve(configFilePath);
104+
105+
XmlMergeDocument workspaceDocument = xmlMerger.load(workspaceFile);
106+
XmlMergeDocument templateDocument = xmlMerger.loadAndResolve(templateFile, environmentVariables);
107+
108+
Document mergedDocument = xmlMerger.merge(templateDocument, workspaceDocument, false);
109+
110+
xmlMerger.save(mergedDocument, workspaceFile);
111+
}
112+
113+
@Override
114+
public void importRepository(Path repositoryPath) {
115+
CommandletManager commandletManager = this.context.getCommandletManager();
116+
for (Entry<Class<? extends LocalToolCommandlet>, String> entry : BUILD_TOOL_TO_IJ_TEMPLATE.entrySet()) {
117+
LocalToolCommandlet buildTool = commandletManager.getCommandlet(entry.getKey());
118+
Path buildDescriptor = buildTool.findBuildDescriptor(repositoryPath);
119+
if (buildDescriptor != null) {
120+
String templateFilename = entry.getValue();
121+
this.context.debug("Found build descriptor {} so merging template {}", buildDescriptor, templateFilename);
122+
mergeConfig(repositoryPath, templateFilename);
123+
return;
124+
}
125+
}
126+
this.context.warning("No supported build descriptor was found for project import in {}", repositoryPath);
55127
}
56128

57129
}

0 commit comments

Comments
 (0)