optionsMap = new LinkedHashMap<>();
{
diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DistroHelper.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DistroHelper.java
index 1e53baac5..756af6039 100644
--- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DistroHelper.java
+++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DistroHelper.java
@@ -8,24 +8,52 @@
import org.apache.maven.project.MavenProject;
import org.openmrs.maven.plugins.model.Artifact;
import org.openmrs.maven.plugins.model.DistroProperties;
+import org.openmrs.maven.plugins.model.PackageJson;
import org.openmrs.maven.plugins.model.Server;
import org.openmrs.maven.plugins.model.UpgradeDifferential;
import org.openmrs.maven.plugins.model.Version;
+import org.semver4j.Semver;
+import org.semver4j.SemverException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.twdata.maven.mojoexecutor.MojoExecutor;
+import org.twdata.maven.mojoexecutor.MojoExecutor.Element;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.util.*;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
+import static org.openmrs.maven.plugins.model.BaseSdkProperties.ARTIFACT_ID;
+import static org.openmrs.maven.plugins.model.BaseSdkProperties.GROUP_ID;
import static org.openmrs.maven.plugins.model.BaseSdkProperties.PROPERTY_DISTRO_ARTIFACT_ID;
import static org.openmrs.maven.plugins.model.BaseSdkProperties.PROPERTY_DISTRO_GROUP_ID;
-import static org.twdata.maven.mojoexecutor.MojoExecutor.*;
+import static org.openmrs.maven.plugins.model.BaseSdkProperties.TYPE;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.artifactId;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.element;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.goal;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.groupId;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.name;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.plugin;
+import static org.twdata.maven.mojoexecutor.MojoExecutor.version;
public class DistroHelper {
+ private static final String CONTENT_PROPERTIES = "content.properties";
+
+ private static final String CONTENT_PREFIX = "content.";
+
+ private static final Logger log = LoggerFactory.getLogger(DistroHelper.class);
/**
* The project currently being build.
*/
@@ -49,7 +77,7 @@ public class DistroHelper {
final VersionsHelper versionHelper;
public DistroHelper(MavenProject mavenProject, MavenSession mavenSession, BuildPluginManager pluginManager,
- Wizard wizard, VersionsHelper versionHelper) {
+ Wizard wizard, VersionsHelper versionHelper) {
this.mavenProject = mavenProject;
this.mavenSession = mavenSession;
this.pluginManager = pluginManager;
@@ -553,4 +581,220 @@ public DistroProperties resolveParentArtifact(Artifact parentArtifact, Server se
return resolveParentArtifact(parentArtifact, server.getServerDirectory(), distroProperties, appShellVersion);
}
+ /**
+ * Parses and processes content properties from content packages defined in the given {@code DistroProperties} object.
+ *
+ * This method creates a temporary directory to download and process content package ZIP files specified
+ * in the {@code distroProperties} file. The method delegates the download and processing of content packages
+ * to the {@code downloadContentPackages} method, ensuring that the content packages are correctly handled
+ * and validated.
+ *
+ * After processing, the temporary directory used for storing the downloaded ZIP files is deleted,
+ * even if an error occurs during processing. If the temporary directory cannot be deleted, a warning is logged.
+ *
+ * @param distroProperties The {@code DistroProperties} object containing key-value pairs specifying
+ * content packages and other properties needed to build a distribution.
+ *
+ * @throws MojoExecutionException If there is an error during the processing of content packages,
+ * such as issues with creating the temporary directory, downloading
+ * the content packages, or IO errors during file operations.
+ */
+ public void parseContentProperties(DistroProperties distroProperties) throws MojoExecutionException {
+ File tempDirectory = null;
+ try {
+ tempDirectory = Files.createTempDirectory("content-packages").toFile();
+ downloadContentPackages(tempDirectory, distroProperties);
+
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to process content packages", e);
+ } finally {
+ if (tempDirectory != null && tempDirectory.exists()) {
+ try {
+ FileUtils.deleteDirectory(tempDirectory);
+ } catch (IOException e) {
+ log.warn("Failed to delete temporary directory: {}", tempDirectory.getAbsolutePath(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Downloads and processes content packages specified in the given distro properties.
+ *
+ * This method filters out properties starting with a specific prefix (defined by {@code CONTENT_PREFIX})
+ * from the {@code distroProperties} file, identifies the corresponding versions, and downloads the
+ * associated ZIP files from the Maven repository. It then processes each downloaded ZIP file to locate
+ * and parse a {@code content.properties} file, ensuring that the content package is valid and meets
+ * the expected requirements.
+ *
+ * If a {@code groupId} is overridden for a particular content package, the method uses the overridden
+ * value when fetching the package from Maven. The ZIP files are temporarily stored and processed to extract
+ * the {@code content.properties} file, which is then validated and compared against the dependencies specified
+ * in the {@code distro.properties} file.
+ *
+ * @param contentPackageZipFile The directory where content package ZIP files will be temporarily stored.
+ * @param distroProperties The {@code DistroProperties} object containing key-value pairs that specify
+ * content packages and other properties needed to build a distribution.
+ *
+ * @throws MojoExecutionException If there is an error during the download or processing of the content packages,
+ * such as missing or invalid {@code content.properties} files, or any IO issues.
+ */
+ public void downloadContentPackages(File contentPackageZipFile, DistroProperties distroProperties)
+ throws MojoExecutionException {
+ Properties contentProperties = new Properties();
+
+ for (Object key : distroProperties.getAllKeys()) {
+ String keyOb = key.toString();
+ if (!keyOb.startsWith(CONTENT_PREFIX)) {
+ continue;
+ }
+
+ Artifact artifact = new Artifact(distroProperties.checkIfOverwritten(keyOb.replace(CONTENT_PREFIX, ""), ARTIFACT_ID), distroProperties.getParam(keyOb),
+ distroProperties.checkIfOverwritten(keyOb, GROUP_ID), distroProperties.checkIfOverwritten(keyOb, TYPE));
+
+ String version = distroProperties.get(keyOb);
+ String zipFileName = keyOb.replace(CONTENT_PREFIX, "") + "-" + version + ".zip";
+ File zipFile = downloadDistro(contentPackageZipFile, artifact, zipFileName);
+
+ if (zipFile == null) {
+ log.warn("ZIP file not found for content package: {}", keyOb);
+ continue;
+ }
+
+ try (ZipFile zip = new ZipFile(zipFile)) {
+ boolean foundContentProperties = false;
+ Enumeration extends ZipEntry> entries = zip.entries();
+
+ while (entries.hasMoreElements()) {
+ ZipEntry zipEntry = entries.nextElement();
+ if (zipEntry.getName().equals(CONTENT_PROPERTIES)) {
+ foundContentProperties = true;
+
+ try (InputStream inputStream = zip.getInputStream(zipEntry)) {
+ contentProperties.load(inputStream);
+ log.info("content.properties file found in {} and parsed successfully.",
+ contentPackageZipFile.getName());
+
+ if (contentProperties.getProperty("name") == null
+ || contentProperties.getProperty("version") == null) {
+ throw new MojoExecutionException(
+ "Content package name or version not specified in content.properties in "
+ + contentPackageZipFile.getName());
+ }
+
+ processContentProperties(contentProperties, distroProperties, contentPackageZipFile.getName());
+ }
+ break;
+ }
+ }
+
+ if (!foundContentProperties) {
+ throw new MojoExecutionException(
+ "No content.properties file found in ZIP file: " + contentPackageZipFile.getName());
+ }
+
+ }
+ catch (IOException e) {
+ throw new MojoExecutionException("Error reading content.properties from ZIP file: "
+ + contentPackageZipFile.getName() + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Processes the {@code content.properties} file of a content package and validates the dependencies
+ * against the {@code DistroProperties} provided. This method ensures that the dependencies defined
+ * in the {@code content.properties} file are either present in the {@code distroProperties} file with
+ * a version that matches the specified version range, or it finds the latest matching version if not
+ * already specified in {@code distroProperties}.
+ *
+ * The method iterates over each dependency listed in the {@code content.properties} file, focusing on
+ * dependencies that start with specific prefixes such as {@code omod.}, {@code owa.}, {@code war},
+ * {@code spa.frontendModule}, or {@code content.}. For each dependency, the method performs the following:
+ *
+ * - If the dependency is not present in {@code distroProperties}, it attempts to find the latest version
+ * matching the specified version range and adds it to {@code distroProperties}.
+ * - If the dependency is present, it checks whether the version specified in {@code distroProperties}
+ * falls within the version range specified in {@code content.properties}. If it does not, an error is thrown.
+ *
+ *
+ * @param contentProperties The {@code Properties} object representing the {@code content.properties} file
+ * of a content package.
+ * @param distroProperties The {@code DistroProperties} object containing key-value pairs specifying
+ * the current distribution's dependencies and their versions.
+ * @param zipFileName The name of the ZIP file containing the {@code content.properties} file being processed.
+ * Used in error messages to provide context.
+ *
+ * @throws MojoExecutionException If no matching version is found for a dependency not defined in
+ * {@code distroProperties}, or if the version specified in {@code distroProperties}
+ * does not match the version range in {@code content.properties}.
+ */
+ protected void processContentProperties(Properties contentProperties, DistroProperties distroProperties, String zipFileName) throws MojoExecutionException {
+ for (String dependency : contentProperties.stringPropertyNames()) {
+ if (dependency.startsWith("omod.") || dependency.startsWith("owa.") || dependency.startsWith("war")
+ || dependency.startsWith("spa.frontendModule") || dependency.startsWith("content.")) {
+ String versionRange = contentProperties.getProperty(dependency);
+ String distroVersion = distroProperties.get(dependency);
+
+ if (distroVersion == null) {
+ String latestVersion = findLatestMatchingVersion(dependency, versionRange);
+ if (latestVersion == null) {
+ throw new MojoExecutionException(
+ "No matching version found for dependency " + dependency + " in " + zipFileName);
+ }
+ distroProperties.add(dependency, latestVersion);
+ } else {
+ checkVersionInRange(dependency, versionRange, distroVersion, contentProperties.getProperty("name"));
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the version from distro.properties satisfies the range specified in content.properties.
+ * Throws an exception if there is a mismatch.
+ *
+ * @param contentDependencyKey The key of the content dependency.
+ * @param contentDependencyVersionRange The version range specified in content.properties.
+ * @param distroPropertyVersion The version specified in distro.properties.
+ * @param contentPackageName The name of the content package.
+ * @throws MojoExecutionException If the version does not fall within the specified range or if the
+ * range format is invalid.
+ */
+ private static void checkVersionInRange(String contentDependencyKey, String contentDependencyVersionRange, String distroPropertyVersion, String contentPackageName) throws MojoExecutionException {
+ Semver semverVersion = new Semver(distroPropertyVersion);
+
+ try {
+ boolean inRange = semverVersion.satisfies(contentDependencyVersionRange.trim());
+ if (!inRange) {
+ throw new MojoExecutionException("Incompatible version for " + contentDependencyKey + " in content package "
+ + contentPackageName + ". Specified range: " + contentDependencyVersionRange
+ + ", found in distribution: " + distroPropertyVersion);
+ }
+ } catch (SemverException e) {
+ throw new MojoExecutionException("Invalid version range format for " + contentDependencyKey
+ + " in content package " + contentPackageName + ": " + contentDependencyVersionRange, e);
+ }
+ }
+
+ public String findLatestMatchingVersion(String dependency, String versionRange) {
+ if (dependency.startsWith("omod") || dependency.startsWith("owa") || dependency.startsWith("content.") || dependency.startsWith("war.")) {
+ return versionHelper.getLatestReleasedVersion(new Artifact(dependency, "latest"));
+ } else if (dependency.startsWith("spa.frontendModule")) {
+ PackageJson packageJson = createPackageJson(dependency);
+ return getResolvedVersionFromNpmRegistry(packageJson, versionRange);
+ }
+ throw new IllegalArgumentException("Unsupported dependency type: " + dependency);
+ }
+
+ private PackageJson createPackageJson(String dependency) {
+ PackageJson packageJson = new PackageJson();
+ packageJson.setName(dependency.substring("spa.frontendModules.".length()));
+ return packageJson;
+ }
+
+ private String getResolvedVersionFromNpmRegistry(PackageJson packageJson, String versionRange) {
+ NpmVersionHelper npmVersionHelper = new NpmVersionHelper();
+ return npmVersionHelper.getResolvedVersionFromNpmRegistry(packageJson, versionRange);
+ }
}
diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/NpmVersionHelper.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/NpmVersionHelper.java
new file mode 100644
index 000000000..f6f993aa8
--- /dev/null
+++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/NpmVersionHelper.java
@@ -0,0 +1,76 @@
+package org.openmrs.maven.plugins.utility;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.openmrs.maven.plugins.model.PackageJson;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+
+public class NpmVersionHelper {
+
+ private static final Logger log = LoggerFactory.getLogger(NpmVersionHelper.class);
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Retrieves the resolved version of an NPM package based on the supplied semver range.
+ *
+ * This method runs the `npm pack --dry-run --json @` command to get the exact
+ * version of the package that satisfies the specified semver range.
+ *
+ * @param packageJson The PackageJson object containing the name of the package.
+ * @param versionRange The semver range to resolve the version against.
+ * @return The resolved version of the package that satisfies the semver range.
+ * @throws RuntimeException if the command fails or the resolved version cannot be determined.
+ */
+ public String getResolvedVersionFromNpmRegistry(PackageJson packageJson, String versionRange) {
+ try {
+ String packageName = packageJson.getName();
+ JsonNode jsonArray = getPackageMetadata(versionRange, packageName);
+ if (jsonArray.isEmpty()) {
+ throw new RuntimeException("No versions found for the specified range: " + versionRange);
+ }
+
+ JsonNode jsonObject = jsonArray.get(0);
+ return jsonObject.get("version").asText();
+ }
+ catch (IOException | InterruptedException e) {
+ log.error(e.getMessage(), e);
+ throw new RuntimeException("Error retrieving resolved version from NPM", e);
+ }
+ }
+
+ private static JsonNode getPackageMetadata(String versionRange, String packageName) throws IOException, InterruptedException {
+ if (packageName == null || packageName.isEmpty()) {
+ throw new IllegalArgumentException("Package name cannot be null or empty");
+ }
+
+ ProcessBuilder processBuilder = new ProcessBuilder()
+ .command("npm", "pack", "--dry-run", "--json", packageName + "@" + versionRange).redirectErrorStream(true)
+ .inheritIO();
+ Process process = processBuilder.start();
+
+ // Read the command output
+ StringBuilder outputBuilder = new StringBuilder();
+ char[] buffer = new char[4096];
+ try (Reader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
+ int read;
+ while ((read = reader.read(buffer)) >= 0) {
+ outputBuilder.append(buffer, 0, read);
+ }
+ }
+
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ throw new RuntimeException(
+ "npm pack --dry-run --json command failed with exit code " + exitCode + ". Output: " + outputBuilder);
+ }
+
+ return objectMapper.readTree(outputBuilder.toString());
+ }
+}
diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/PropertiesUtils.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/PropertiesUtils.java
index 9bf8955e7..60264dadc 100644
--- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/PropertiesUtils.java
+++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/PropertiesUtils.java
@@ -1,5 +1,21 @@
package org.openmrs.maven.plugins.utility;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.openmrs.maven.plugins.model.Artifact;
+import org.openmrs.maven.plugins.model.Server;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -17,23 +33,6 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.commons.io.IOUtils;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.openmrs.maven.plugins.model.Artifact;
-import org.openmrs.maven.plugins.model.Server;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.xml.sax.SAXException;
-
public class PropertiesUtils {
private static final Logger log = LoggerFactory.getLogger(PropertiesUtils.class);