MMKB-5027: Adapts magnolia update mechanism

This commit is contained in:
Alexander Graf 2024-11-22 17:20:04 +01:00
commit 394a5c42bf
13 changed files with 540 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
target/
.nvimlog

32
Readme.md Normal file
View File

@ -0,0 +1,32 @@
# Synopsis
Maven Module um Magnolia Update Tasks auszuführen. Diese Update Tasks sind in ModuleUpdates gebundelt.
Magnolia Bundles welche diese Funktionalität implementieren wollen, müssen IntranetUpdateModuleVersionHandler implementieren
und als Version Handler setzten.
In den meisten Fällen reicht es hierfür IntranetUpdateModuleVersionHandler#getModuleConfig() zu implementieren und ein Object
von Typ IntranetUpdateModuleConfig zurückzugeben.
In IntranetUpdateModuleConfig können 3 Konfigurationen gesetzt werden:
* List<Task> getInitialUpdateTasks()
* Eine Liste von Tasks die bei einem ersten Startup ausgeführt werden sollen
* String getYamlUpdateDir()
* Filepath in von dem Bootstrap Yamls ausgelesen werden sollen
* String getUpdateTaskPackage()
* Java Package aus den Implementierungen von ModuleUpdate ausgelesen werden sollen
## Module Update
Module Updates funktionieren hierbei genauso wie SimpleUpdates. Es gibt 3 verschiedene Arten von Tasks.
* Generelle Tasks die ausgeführt werden sollen
* Yaml Bootstrap Files
* Tasks die nachdem Bootstrap ausgeführt werden sollen
Jedes Module muss eine Version gesetzt haben und muss mit einem Timestamp (ddMMyyyyHHmm) starten.
## Allgemeine Funktionsweise
1. Tasks aus List<Task> getInitialUpdateTasks() werden ausgeführt, falls das Module initial installiert wird
2. ModuleUpdates werden über Reflection aus dem angegebenen Path initiiert und ausgeführt
1. Sollte ein Task aus dem ModuleUpdate erfolgreich durchgeführt werden, dann wird ein Eintrag in JCR unter der Module Config in einem Version Node gespeichert
2. Falls diese Version in diesem Task schon vorhanden ist, wird dieser Task ignoriert

98
pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<properties>
<javaVersion>21</javaVersion>
<magnoliaBundleVersion>6.2.46</magnoliaBundleVersion>
<version.lombok>1.18.34</version.lombok>
</properties>
<groupId>at.ucs.magnolia</groupId>
<artifactId>ucs-intranet-magnolia-updates</artifactId>
<version>1.0.0</version>
<name>ucs-intranet-magnolia-updates</name>
<url>http://maven.apache.org</url>
<packaging>jar</packaging>
<!-- deploy new versions to ucs nexus repository -->
<distributionManagement>
<repository>
<id>ucs.repo</id>
<url>http://192.168.1.229:8080/nexus/content/repositories/thirdparty/</url>
</repository>
</distributionManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>info.magnolia.bundle</groupId>
<artifactId>magnolia-bundle-parent</artifactId>
<version>${magnoliaBundleVersion}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>info.magnolia</groupId>
<artifactId>magnolia-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${version.lombok}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>${javaVersion}</source>
<target>${javaVersion}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${version.lombok}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>magnolia.public</id>
<url>https://nexus.magnolia-cms.com/content/groups/public</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>vaadin-addons</id>
<url>https://maven.vaadin.com/vaadin-addons</url>
</repository>
</repositories>
</project>

View File

@ -0,0 +1,11 @@
package at.ucs.magnolia.updates;
import info.magnolia.module.delta.Task;
import java.util.List;
public interface IntranetUpdateModuleConfig {
List<Task> getInitialUpdateTasks();
String getYamlUpdateDir();
String getUpdateTaskPackage();
}

View File

@ -0,0 +1,102 @@
package at.ucs.magnolia.updates;
import at.ucs.magnolia.updates.util.TaskWrapper;
import at.ucs.magnolia.updates.util.VersionComparator;
import at.ucs.magnolia.updates.util.VersionUtil;
import info.magnolia.module.DefaultModuleVersionHandler;
import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.Delta;
import info.magnolia.module.delta.DeltaBuilder;
import info.magnolia.module.delta.Task;
import info.magnolia.module.model.Version;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import javax.jcr.RepositoryException;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;
@Slf4j
public abstract class IntranetUpdateModuleVersionHandler extends DefaultModuleVersionHandler {
protected abstract IntranetUpdateModuleConfig getModuleConfig();
@Override
protected List<Task> getExtraInstallTasks(InstallContext installContext) {
return getModuleConfig().getInitialUpdateTasks();
}
@Override
public List<Delta> getDeltas(InstallContext installContext, Version from) {
List<Delta> deltaList = new ArrayList<>();
if (from == null) {
deltaList.add(getInstall(installContext));
}
deltaList.addAll(getUpdateDeltas(installContext, from));
return deltaList;
}
protected List<Delta> getUpdateDeltas(InstallContext installContext, Version from) {
try {
return new Reflections(
getModuleConfig().getUpdateTaskPackage(),
new SubTypesScanner()
).getSubTypesOf(ModuleUpdate.class).stream()
.map(this::initiateModuleUpdate)
.filter(Objects::nonNull)
.sorted(new VersionComparator())
.map(updateModule -> buildDelta(installContext, updateModule))
.toList();
} catch (Exception e) {
log.error("Could not register update tasks", e);
}
return Collections.emptyList();
}
private ModuleUpdate initiateModuleUpdate(Class<? extends ModuleUpdate> moduleUpdateClass) {
try {
ModuleUpdate moduleUpdate = moduleUpdateClass.getConstructor().newInstance();
moduleUpdate.setYamlPath(getModuleConfig().getYamlUpdateDir());
return moduleUpdate;
} catch (RuntimeException | NoSuchMethodException | InstantiationException |
IllegalAccessException |
InvocationTargetException e) {
log.error(e.getLocalizedMessage(), e);
}
return null;
}
private Predicate<TaskWrapper> installNecessary(InstallContext installContext) {
return (wrapper) -> {
try {
return !VersionUtil.containsVersion(installContext, wrapper.getVersion());
} catch (RepositoryException e) {
return false;
}
};
}
private Delta buildDelta(InstallContext installContext, ModuleUpdate moduleUpdate) {
final String version = moduleUpdate.getVersion();
List<Task> wrappedTasks = IntStream.range(0, moduleUpdate.getAllTasks().size())
.mapToObj(i -> {
final String wrappedVersion = MessageFormat.format("{0}_{1}", version, i);
final Task task = moduleUpdate.getAllTasks().get(i);
return new TaskWrapper(task, wrappedVersion);
})
.filter(installNecessary(installContext))
.map(Task.class::cast)
.toList();
// Fake sem version
// We don't actually need it
return DeltaBuilder.update(Version.parseVersion(0, 4, 2), "")
.addTasks(wrappedTasks);
}
}

View File

@ -0,0 +1,42 @@
package at.ucs.magnolia.updates;
import at.ucs.magnolia.updates.util.BootstrapUpdateYamlsWithProperties;
import at.ucs.magnolia.updates.util.LoggingTask;
import info.magnolia.module.delta.Task;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Getter
@Setter
@RequiredArgsConstructor
public abstract class ModuleUpdate {
private final String version;
private String yamlPath;
public List<Task> getUpdateTasks() {
return Collections.emptyList();
}
public List<Task> getUpdateTasksAfterBootstrap() {
return Collections.emptyList();
}
public List<Task> getAllTasks() {
List<Task> tasks = new ArrayList<>();
tasks.add(new LoggingTask("updating to version: " + version, ""));
tasks.addAll(getUpdateTasks());
tasks.add(new LoggingTask("Importing bootsrap Yamls for: " + version, ""));
tasks.add(new BootstrapUpdateYamlsWithProperties(
"Import bootstrap YAML folder",
MessageFormat.format("Imports bootstrap YAML files from {0}{1}", yamlPath, version), version, yamlPath));
tasks.addAll(getUpdateTasksAfterBootstrap());
return tasks;
}
}

View File

@ -0,0 +1,40 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.cms.util.ClasspathResourcesUtil;
import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.BootstrapResourcesTask;
import javax.jcr.RepositoryException;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Arrays;
/**
* This task bootstraps all yamls of update-yamls in a specific version and replaces magnolia property placeholder.
*/
public class BootstrapUpdateYamlsWithProperties extends BootstrapResourcesTask {
private final String version;
private final String yamlPath;
public BootstrapUpdateYamlsWithProperties(String name, String description, String version, String yamlPath) {
super(name, description);
this.version = version;
this.yamlPath = yamlPath;
}
@Override
protected void bootstrap(InstallContext installContext, int importUUIDBehavior) throws IOException, RepositoryException {
// In the original Task super.bootstrap is called, but since we want to call our own BootstrapUtil
// we have to use code called in the super class explicitly
String[] resourcesToBootstrap = ClasspathResourcesUtil.findResources(name -> acceptResource(installContext, name));
resourcesToBootstrap = filterResourcesToBootstrap(resourcesToBootstrap);
UcsBootstrapUtil.bootstrap(resourcesToBootstrap, importUUIDBehavior);
}
private String[] filterResourcesToBootstrap(String[] resourcesToBootstrap) {
return Arrays.stream(resourcesToBootstrap)
.filter(s -> s.startsWith(MessageFormat.format("/{0}/{1}/", yamlPath, version)))
.toArray(String[]::new);
}
}

View File

@ -0,0 +1,20 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.AbstractTask;
import info.magnolia.module.delta.TaskExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingTask extends AbstractTask {
private static final Logger log = LoggerFactory.getLogger(LoggingTask.class.getName());
public LoggingTask(String taskName, String taskDescription) {
super(taskName, taskDescription);
}
@Override
public void execute(InstallContext installContext) throws TaskExecutionException {
log.info(getName());
}
}

View File

@ -0,0 +1,30 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.init.PropertySource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.text.StringSubstitutor;
import java.io.IOException;
import java.io.InputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.text.StringSubstitutor.*;
public class MagnoliaPropertyResolver {
private MagnoliaPropertyResolver() {
}
public static InputStream resolve(PropertySource properties, InputStream in) {
try {
return IOUtils.toInputStream(replaceMagnoliaPlaceholders(properties, in), UTF_8);
} catch (IOException e) {
return null;
}
}
private static String replaceMagnoliaPlaceholders(PropertySource properties, InputStream in) throws IOException {
StringSubstitutor substitutor = new StringSubstitutor(properties::getProperty, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
return substitutor.replace(IOUtils.toString(in, UTF_8));
}
}

View File

@ -0,0 +1,39 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.module.InstallContext;
import info.magnolia.module.InstallStatus;
import info.magnolia.module.delta.AbstractTask;
import info.magnolia.module.delta.Task;
import info.magnolia.module.delta.TaskExecutionException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
@Slf4j
public class TaskWrapper extends AbstractTask {
private final Task task;
@Getter
private final String version;
public TaskWrapper(Task task, String version) {
super(task.getName(), task.getDescription());
this.task = task;
this.version = version;
}
@Override
public void execute(InstallContext installContext) throws TaskExecutionException {
task.execute(installContext);
if (installContext.getStatus() != InstallStatus.installFailed) {
try {
VersionUtil.addVersion(installContext, getVersion(), task.getName());
} catch (RepositoryException e) {
log.info(e.getMessage());
}
}
}
}

View File

@ -0,0 +1,59 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.cms.util.StringLengthComparator;
import info.magnolia.importexport.BootstrapUtil;
import info.magnolia.init.MagnoliaConfigurationProperties;
import info.magnolia.objectfactory.Components;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
* This class is exactly working like the original BootstrapUtil class of Magnolia,
* with the only difference, that it substitutes Magnolia Property placeholders
* with their actual value, since this is not implemented for bootstrap yamls by Magnolia
*/
public class UcsBootstrapUtil {
private static final Logger log = LoggerFactory.getLogger(BootstrapUtil.class);
public static void bootstrap(String[] resourceNames, int importUUIDBehavior) throws IOException, RepositoryException {
// sort by length --> import parent node firstsubPath
List<String> list = new ArrayList<>(Arrays.asList(resourceNames));
if (list.contains(null)) {
throw new IllegalArgumentException("Resource names contain a <null> entry that cannot be processed.");
}
Collections.sort(list, new StringLengthComparator());
for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
bootstrap(iter.next(), null, importUUIDBehavior);
}
}
public static void bootstrap(String resourceName, String subPath, int importUUIDBehavior) throws IOException, RepositoryException {
final InputStream stream = BootstrapUtil.class.getResourceAsStream(resourceName);
if (stream == null) {
throw new IOException("Can't find resource to bootstrap at " + resourceName);
}
MagnoliaConfigurationProperties magnoliaConfiguration = Components.getComponent(MagnoliaConfigurationProperties.class);
InputStream resolvedStream = MagnoliaPropertyResolver.resolve(magnoliaConfiguration, stream);
// Verify if the node already exists and execute jcr import command
bootstrap(resourceName, subPath, resolvedStream, importUUIDBehavior);
}
public static void bootstrap(String resourceName, String subPath, InputStream stream, int importUUIDBehavior) throws RepositoryException {
BootstrapUtil.bootstrap(resourceName, subPath, stream, importUUIDBehavior);
}
public static void export(Node content, File directory) throws IOException, RepositoryException {
BootstrapUtil.export(content, directory);
}
}

View File

@ -0,0 +1,23 @@
package at.ucs.magnolia.updates.util;
import at.ucs.magnolia.updates.ModuleUpdate;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
@Slf4j
public class VersionComparator implements Comparator<ModuleUpdate> {
@Override
public int compare(ModuleUpdate o1, ModuleUpdate o2) {
var o1Version = o1.getVersion();
var o2Version = o2.getVersion();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("ddMMyyyyHHmm");
LocalDateTime first = LocalDateTime.from(dateTimeFormatter.parse(o1Version.substring(0, 12)));
LocalDateTime second = LocalDateTime.from(dateTimeFormatter.parse(o2Version.substring(0, 12)));
return first.compareTo(second);
}
}

View File

@ -0,0 +1,41 @@
package at.ucs.magnolia.updates.util;
import info.magnolia.jcr.util.NodeTypes;
import info.magnolia.jcr.util.NodeUtil;
import info.magnolia.module.InstallContext;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
public class VersionUtil {
private static Node getVersionsNode(InstallContext installContext) throws RepositoryException {
// make sure we have the /modules node
if (!installContext.hasModulesNode()) {
final Session session = installContext.getConfigJCRSession();
session.getRootNode().addNode("modules", NodeTypes.Content.NAME);
}
final Node moduleNode = installContext.getOrCreateCurrentModuleNode();
if (!moduleNode.hasNode("config/versions")) {
NodeUtil.createPath(moduleNode, "config/versions", "mgnl:content");
}
return moduleNode.getNode("config/versions");
}
public static boolean containsVersion(InstallContext installContext, String version) throws RepositoryException {
Node versionNode = getVersionsNode(installContext);
return versionNode != null && versionNode.hasProperty(version);
}
public static void addVersion(InstallContext installContext, String version, String name) throws RepositoryException {
Node versionsNode = getVersionsNode(installContext);
if (versionsNode != null) {
versionsNode.setProperty(version, name);
}
}
}