diff --git a/integration-tests/src/test/resources/integration-test/openmrs-distro.properties b/integration-tests/src/test/resources/integration-test/openmrs-distro.properties index 97a28026f..4e2108806 100644 --- a/integration-tests/src/test/resources/integration-test/openmrs-distro.properties +++ b/integration-tests/src/test/resources/integration-test/openmrs-distro.properties @@ -13,3 +13,4 @@ spa.frontendModules.@openmrs/esm-login-app=3.3.1 spa.frontendModules.@openmrs/esm-patient-chart-app=3.1.0 spa.apiUrl=notopenmrs spa.configUrls=foo +content.hiv=1.0.0 diff --git a/maven-plugin/src/main/java/org/openmrs/maven/plugins/BuildDistro.java b/maven-plugin/src/main/java/org/openmrs/maven/plugins/BuildDistro.java index e609621b3..dc7144259 100644 --- a/maven-plugin/src/main/java/org/openmrs/maven/plugins/BuildDistro.java +++ b/maven-plugin/src/main/java/org/openmrs/maven/plugins/BuildDistro.java @@ -8,17 +8,14 @@ import org.apache.commons.lang.StringUtils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; -import org.eclipse.jgit.api.errors.GitAPIException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.eclipse.jgit.api.CloneCommand; -import org.eclipse.jgit.api.Git; import org.openmrs.maven.plugins.model.Artifact; import org.openmrs.maven.plugins.model.DistroProperties; +import org.openmrs.maven.plugins.model.Project; import org.openmrs.maven.plugins.model.Server; import org.openmrs.maven.plugins.model.Version; import org.openmrs.maven.plugins.utility.DistroHelper; -import org.openmrs.maven.plugins.model.Project; import org.openmrs.maven.plugins.utility.SDKConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -195,6 +192,7 @@ public void executeTask() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException("The distro you specified, '" + distro + "' could not be retrieved"); } + distroHelper.parseContentProperties(distroProperties); String distroName = buildDistro(buildDirectory, distroArtifact, distroProperties); wizard.showMessage( @@ -510,7 +508,7 @@ private void copyDbDump(File targetDirectory, InputStream stream) throws MojoExe } try (FileWriter writer = new FileWriter(dbDump); - BufferedInputStream bis = new BufferedInputStream(stream)) { + BufferedInputStream bis = new BufferedInputStream(stream)) { writer.write(DUMP_PREFIX); int c; diff --git a/maven-plugin/src/main/java/org/openmrs/maven/plugins/Setup.java b/maven-plugin/src/main/java/org/openmrs/maven/plugins/Setup.java index 00b23e95a..3df0cc825 100644 --- a/maven-plugin/src/main/java/org/openmrs/maven/plugins/Setup.java +++ b/maven-plugin/src/main/java/org/openmrs/maven/plugins/Setup.java @@ -1,5 +1,24 @@ package org.openmrs.maven.plugins; +import net.lingala.zip4j.core.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.lang.StringUtils; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.openmrs.maven.plugins.model.Artifact; +import org.openmrs.maven.plugins.model.DistroProperties; +import org.openmrs.maven.plugins.model.Server; +import org.openmrs.maven.plugins.model.Version; +import org.openmrs.maven.plugins.utility.DBConnector; +import org.openmrs.maven.plugins.utility.DistroHelper; +import org.openmrs.maven.plugins.utility.SDKConstants; +import org.openmrs.maven.plugins.utility.ServerHelper; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -20,25 +39,6 @@ import java.util.List; import java.util.Objects; -import net.lingala.zip4j.core.ZipFile; -import net.lingala.zip4j.exception.ZipException; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.output.NullOutputStream; -import org.apache.commons.lang.StringUtils; -import org.apache.ibatis.jdbc.ScriptRunner; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.openmrs.maven.plugins.model.Artifact; -import org.openmrs.maven.plugins.model.DistroProperties; -import org.openmrs.maven.plugins.model.Server; -import org.openmrs.maven.plugins.model.Version; -import org.openmrs.maven.plugins.utility.DBConnector; -import org.openmrs.maven.plugins.utility.DistroHelper; -import org.openmrs.maven.plugins.utility.SDKConstants; -import org.openmrs.maven.plugins.utility.ServerHelper; - /** * Set up a new instance of OpenMRS server. It can be used for setting up a platform or a distribution. It prompts for any missing, but required parameters. @@ -276,6 +276,7 @@ public void setup(Server server, DistroProperties distroProperties) throws MojoE // `setServerVersionsFromDistroProperties`, and `server.setValuesFromDistroPropertiesModules`. distroHelper.savePropertiesToServer(distroProperties, server); setServerVersionsFromDistroProperties(server, distroProperties); + distroHelper.parseContentProperties(distroProperties); moduleInstaller.installModulesForDistro(server, distroProperties, distroHelper); setConfigFolder(server, distroProperties); if (spaInstaller != null) { diff --git a/pom.xml b/pom.xml index da561c158..bd1ef4474 100644 --- a/pom.xml +++ b/pom.xml @@ -403,6 +403,12 @@ 0.10.2 + + org.semver4j + semver4j + 5.3.0 + + org.jline diff --git a/sdk-commons/pom.xml b/sdk-commons/pom.xml index 52a066ad0..7020304ca 100644 --- a/sdk-commons/pom.xml +++ b/sdk-commons/pom.xml @@ -34,6 +34,11 @@ java-semver + + org.semver4j + semver4j + + org.apache.httpcomponents diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/Artifact.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/Artifact.java index 175e9d28c..fc8255553 100644 --- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/Artifact.java +++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/Artifact.java @@ -24,6 +24,7 @@ public class Artifact { public static final String GROUP_MODULE = "org.openmrs.module"; public static final String GROUP_OWA = "org.openmrs.owa"; public static final String GROUP_WEB = "org.openmrs.web"; + public static final String GROUP_CONTENT = "org.openmrs.content"; public static final String GROUP_OPENMRS = "org.openmrs"; public static final String GROUP_H2 = "com.h2database"; public static final String GROUP_DISTRO = "org.openmrs.distro"; diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/BaseSdkProperties.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/BaseSdkProperties.java index a843e1337..e6ed4ee9c 100644 --- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/BaseSdkProperties.java +++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/BaseSdkProperties.java @@ -16,14 +16,15 @@ public abstract class BaseSdkProperties { public static final String PROPERTY_DISTRO_ARTIFACT_ID = "distro.artifactId"; public static final String PROPERTY_DISTRO_GROUP_ID = "distro.groupId"; - protected static final String ARTIFACT_ID = "artifactId"; - protected static final String TYPE = "type"; - protected static final String GROUP_ID = "groupId"; + public static final String ARTIFACT_ID = "artifactId"; + public static final String TYPE = "type"; + public static final String GROUP_ID = "groupId"; protected static final String TYPE_OMOD = "omod"; protected static final String TYPE_WAR = "war"; protected static final String TYPE_JAR = "jar"; protected static final String NAME = "name"; protected static final String VERSION = "version"; + protected static final String TYPE_CONTENT = "content"; protected static final String TYPE_DISTRO = "distro"; protected static final String TYPE_OWA = "owa"; protected static final String TYPE_SPA = "spa"; @@ -179,34 +180,36 @@ protected String checkIfOverwritten(String key, String param) { } else { switch (param) { case ARTIFACT_ID: - return extractArtifactId(key); + return extractArtifactId(key); case GROUP_ID: - switch (getArtifactType(key)) { - case TYPE_WAR: //for openmrs.war use org.openmrs.web groupId - return Artifact.GROUP_WEB; - case TYPE_OMOD: - return Artifact.GROUP_MODULE; - case TYPE_DISTRO: - case TYPE_CONFIG: - return properties.getProperty(PROPERTY_DISTRO_GROUP_ID, Artifact.GROUP_DISTRO); - default: - return ""; - } - + switch (getArtifactType(key)) { + case TYPE_WAR: //for openmrs.war use org.openmrs.web groupId + return Artifact.GROUP_WEB; + case TYPE_OMOD: + return Artifact.GROUP_MODULE; + case TYPE_DISTRO: + case TYPE_CONFIG: + return properties.getProperty(PROPERTY_DISTRO_GROUP_ID, Artifact.GROUP_DISTRO); + case TYPE_CONTENT: + return Artifact.GROUP_CONTENT; + default: + return ""; + } case TYPE: - switch (getArtifactType(key)) { - case TYPE_OMOD: - case TYPE_DISTRO: - return TYPE_JAR; - case TYPE_WAR: - return TYPE_WAR; - case TYPE_CONFIG: - return TYPE_ZIP; - default: - return ""; - } + switch (getArtifactType(key)) { + case TYPE_OMOD: + case TYPE_DISTRO: + return TYPE_JAR; + case TYPE_WAR: + return TYPE_WAR; + case TYPE_CONFIG: + case TYPE_CONTENT: + return TYPE_ZIP; + default: + return ""; + } default: - return ""; + return ""; } } } diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/DistroProperties.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/DistroProperties.java index 09f1e1e53..32d2e8d9d 100644 --- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/DistroProperties.java +++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/model/DistroProperties.java @@ -1,8 +1,5 @@ package org.openmrs.maven.plugins.model; -import static org.openmrs.maven.plugins.utility.PropertiesUtils.loadPropertiesFromFile; -import static org.openmrs.maven.plugins.utility.PropertiesUtils.loadPropertiesFromResource; - import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.maven.plugin.MojoExecutionException; @@ -21,6 +18,9 @@ import java.util.Properties; import java.util.Set; +import static org.openmrs.maven.plugins.utility.PropertiesUtils.loadPropertiesFromFile; +import static org.openmrs.maven.plugins.utility.PropertiesUtils.loadPropertiesFromResource; + /** * */ @@ -291,4 +291,37 @@ private boolean hasPlaceholder(Object object){ int index = asString.indexOf("{"); return index != -1 && asString.substring(index).contains("}"); } + + public boolean contains(String propertyName) { + return properties.containsKey(propertyName); + } + + /** + * Adds a dependency with the specified version to the properties. + * + * @param dependency The name of the dependency. + * @param version The version of the dependency. + */ + public void add(String dependency, String version) { + if (StringUtils.isBlank(dependency) || StringUtils.isBlank(version)) { + log.error("Dependency name or version cannot be blank"); + return; + } + + if (dependency.startsWith("omod.") || dependency.startsWith("owa.") || dependency.startsWith("war") + || dependency.startsWith("spa.frontendModule")) { + properties.setProperty(dependency, version); + log.info("Added dependency: {} with version: {}", dependency, version); + } + } + + public String get(String contentDependencyKey) { + return properties.getProperty(contentDependencyKey); + } + + @Override + public String checkIfOverwritten(String key, String param) { + return super.checkIfOverwritten(key, param); + } + } diff --git a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DefaultWizard.java b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DefaultWizard.java index dcc4a648c..ca872e5db 100644 --- a/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DefaultWizard.java +++ b/sdk-commons/src/main/java/org/openmrs/maven/plugins/utility/DefaultWizard.java @@ -781,8 +781,7 @@ private Map getDistroVersionsOptionsMap(Set versions, Ve * @param optionTemplate The template for generating option keys in the map. * @return A LinkedHashMap containing the generated options map. */ - private Map getO3VersionsOptionsMap(VersionsHelper versionsHelper, - String optionTemplate) { + private Map getO3VersionsOptionsMap(VersionsHelper versionsHelper, String optionTemplate) { Map 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 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);