IdeProfileSync.java
package network.ike.plugin.ws;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.model.v4.MavenStaxReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/**
* Write the active {@code with-*} profile list into
* {@code .mvn/maven.config} so IntelliJ activates them on import.
*
* <p><b>Why.</b> Workspace aggregator POMs declare profiles like
* {@code <profile><id>with-tinkar-core</id>} activated by an
* {@code <exists>${project.basedir}/tinkar-core/pom.xml</exists>}
* file-presence rule. The Maven CLI honors these reliably; IntelliJ's
* Maven importer does not, leaving the user to tick boxes by hand on
* every import. A {@code -P} arg in {@code .mvn/maven.config} is read
* by both the CLI and the IntelliJ importer at parse time, so the
* profiles activate without any IDE clicking.
*
* <p><b>Owned-block format.</b> The plugin manages exactly the lines
* between two markers, leaving everything else verbatim:
*
* <pre>{@code
* -T 1C
* -pl
* !.teamcity
* # >>> ws:ide-sync managed >>>
* -Pwith-tinkar-core,with-komet-bom
* # <<< ws:ide-sync managed <<<
* }</pre>
*
* <p>If no {@code with-*} profiles are present-and-active, the block is
* removed entirely — leaving an empty file is fine. The {@code -P} arg
* is a single comma-joined token; spaces around the value would split
* into separate args under Maven 3.9+ line-based parsing.
*
* <p>Idempotent: running twice produces identical content.
*
* <p>See {@code IKE-Network/ike-issues#276}.
*/
final class IdeProfileSync {
static final String BEGIN_MARKER = "# >>> ws:ide-sync managed >>>";
static final String END_MARKER = "# <<< ws:ide-sync managed <<<";
private static final String PROFILE_PREFIX = "with-";
private IdeProfileSync() {}
/**
* Sync the profile list for the workspace at {@code workspaceRoot}.
* No-op when the workspace POM declares no {@code with-*} profiles.
*
* @param workspaceRoot the workspace root directory (containing
* {@code pom.xml} and {@code workspace.yaml})
* @param log plugin log for status messages
*/
static void run(File workspaceRoot, Log log) {
Path pomPath = workspaceRoot.toPath().resolve("pom.xml");
if (!Files.isRegularFile(pomPath)) {
log.debug("ide-sync: no pom.xml at " + workspaceRoot
+ " — skipping");
return;
}
List<String> activeProfiles;
try {
activeProfiles = computeActiveProfiles(workspaceRoot, pomPath);
} catch (IOException e) {
log.warn("ide-sync: cannot parse pom.xml — " + e.getMessage());
return;
}
Path mavenConfig = workspaceRoot.toPath()
.resolve(".mvn").resolve("maven.config");
try {
String existing = Files.isRegularFile(mavenConfig)
? Files.readString(mavenConfig, StandardCharsets.UTF_8)
: "";
String updated = rewriteOwnedBlock(existing, activeProfiles);
if (updated.equals(existing)) {
log.debug("ide-sync: .mvn/maven.config already up to date");
return;
}
Files.createDirectories(mavenConfig.getParent());
Files.writeString(mavenConfig, updated, StandardCharsets.UTF_8);
if (activeProfiles.isEmpty()) {
log.info(" ide-sync: cleared .mvn/maven.config -P block");
} else {
log.info(" ide-sync: .mvn/maven.config -P "
+ String.join(",", activeProfiles));
}
} catch (IOException e) {
log.warn("ide-sync: cannot update .mvn/maven.config — "
+ e.getMessage());
}
}
/**
* Enumerate {@code <profile><id>with-X</id></profile>} entries in the
* workspace POM whose {@code <activation><file><exists>} target is
* present on disk. Returned in alphabetical order so the output is
* stable regardless of POM declaration order.
*/
private static List<String> computeActiveProfiles(File workspaceRoot,
Path pomPath)
throws IOException {
Model model;
try {
String content = Files.readString(pomPath, StandardCharsets.UTF_8);
model = new MavenStaxReader().read(new StringReader(content));
} catch (Exception e) {
throw new IOException("Cannot parse " + pomPath + ": "
+ e.getMessage(), e);
}
List<Profile> profiles = model.getProfiles();
if (profiles == null || profiles.isEmpty()) {
return List.of();
}
List<String> active = new ArrayList<>();
for (Profile p : profiles) {
String id = p.getId();
if (id == null || !id.startsWith(PROFILE_PREFIX)) {
continue;
}
Optional<String> existsPath = activationFileExists(p);
if (existsPath.isEmpty()) {
continue;
}
String resolved = existsPath.get()
.replace("${project.basedir}", workspaceRoot.toString());
if (Files.exists(Path.of(resolved))) {
active.add(id);
}
}
active.sort(Comparator.naturalOrder());
return active;
}
private static Optional<String> activationFileExists(Profile p) {
if (p.getActivation() == null
|| p.getActivation().getFile() == null) {
return Optional.empty();
}
String exists = p.getActivation().getFile().getExists();
return (exists == null || exists.isBlank())
? Optional.empty()
: Optional.of(exists);
}
/**
* Replace (or remove) the owned block in the existing
* {@code maven.config} content, preserving everything outside the
* markers verbatim.
*
* <p>If {@code activeProfiles} is empty, the entire block (including
* markers) is removed. Otherwise the block is rewritten with the
* current profile list.
*
* @param existing current file content (may be empty)
* @param activeProfiles sorted list of profile ids to activate
* @return new file content, suitable to write back as-is
*/
static String rewriteOwnedBlock(String existing,
List<String> activeProfiles) {
String[] lines = existing.split("\n", -1);
StringBuilder out = new StringBuilder();
boolean inBlock = false;
boolean blockEmitted = false;
for (String line : lines) {
if (!inBlock && line.equals(BEGIN_MARKER)) {
inBlock = true;
if (!activeProfiles.isEmpty()) {
appendBlock(out, activeProfiles);
blockEmitted = true;
}
continue;
}
if (inBlock) {
if (line.equals(END_MARKER)) {
inBlock = false;
}
continue;
}
out.append(line).append('\n');
}
// Trailing newline from split is an empty token at end — strip the
// final \n we appended for it, then re-normalize.
if (out.length() > 0 && out.charAt(out.length() - 1) == '\n') {
out.setLength(out.length() - 1);
}
if (!blockEmitted && !activeProfiles.isEmpty()) {
if (out.length() > 0 && out.charAt(out.length() - 1) != '\n') {
out.append('\n');
}
appendBlock(out, activeProfiles);
// appendBlock leaves no trailing newline; add one for POSIX
out.append('\n');
} else if (out.length() > 0
&& out.charAt(out.length() - 1) != '\n') {
out.append('\n');
}
return out.toString();
}
private static void appendBlock(StringBuilder out,
List<String> activeProfiles) {
if (out.length() > 0 && out.charAt(out.length() - 1) != '\n') {
out.append('\n');
}
out.append(BEGIN_MARKER).append('\n');
// Maven 4 treats unknown profiles as a fatal error. When IntelliJ
// (or any reactor invocation scoped with -pl <subproject>) parses a
// subproject in isolation, the with-* profiles only declared in
// the workspace aggregator POM are absent — and the build aborts
// before importing. The Maven 4 "?profileId" prefix marks the
// activation as optional: present-and-active if the profile
// exists, soft-warning if it doesn't. See ike-issues#299.
out.append("-P");
for (int i = 0; i < activeProfiles.size(); i++) {
if (i > 0) out.append(',');
out.append('?').append(activeProfiles.get(i));
}
out.append('\n');
out.append(END_MARKER);
}
}