From 394a5c42bfd5fc68164321eb72b537bf2e1d78e9 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 22 Nov 2024 17:20:04 +0100 Subject: [PATCH] MMKB-5027: Adapts magnolia update mechanism --- .gitignore | 3 + Readme.md | 32 ++++++ pom.xml | 98 +++++++++++++++++ .../updates/IntranetUpdateModuleConfig.java | 11 ++ .../IntranetUpdateModuleVersionHandler.java | 102 ++++++++++++++++++ .../at/ucs/magnolia/updates/ModuleUpdate.java | 42 ++++++++ .../BootstrapUpdateYamlsWithProperties.java | 40 +++++++ .../magnolia/updates/util/LoggingTask.java | 20 ++++ .../util/MagnoliaPropertyResolver.java | 30 ++++++ .../magnolia/updates/util/TaskWrapper.java | 39 +++++++ .../updates/util/UcsBootstrapUtil.java | 59 ++++++++++ .../updates/util/VersionComparator.java | 23 ++++ .../magnolia/updates/util/VersionUtil.java | 41 +++++++ 13 files changed, 540 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 pom.xml create mode 100644 src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleConfig.java create mode 100644 src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleVersionHandler.java create mode 100644 src/main/java/at/ucs/magnolia/updates/ModuleUpdate.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/BootstrapUpdateYamlsWithProperties.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/LoggingTask.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/MagnoliaPropertyResolver.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/TaskWrapper.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/UcsBootstrapUtil.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/VersionComparator.java create mode 100644 src/main/java/at/ucs/magnolia/updates/util/VersionUtil.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45867f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +target/ +.nvimlog diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..cd48339 --- /dev/null +++ b/Readme.md @@ -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 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 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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f503288 --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + 21 + 6.2.46 + 1.18.34 + + at.ucs.magnolia + ucs-intranet-magnolia-updates + 1.0.0 + ucs-intranet-magnolia-updates + http://maven.apache.org + jar + + + + + ucs.repo + http://192.168.1.229:8080/nexus/content/repositories/thirdparty/ + + + + + + + info.magnolia.bundle + magnolia-bundle-parent + ${magnoliaBundleVersion} + pom + import + + + + + + + info.magnolia + magnolia-core + + + junit + junit + 3.8.1 + test + + + javax.inject + javax.inject + + + org.projectlombok + lombok + ${version.lombok} + provided + + + org.apache.commons + commons-lang3 + 3.17.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + ${javaVersion} + ${javaVersion} + + + org.projectlombok + lombok + ${version.lombok} + + + + + + + + + magnolia.public + https://nexus.magnolia-cms.com/content/groups/public + + true + + + + vaadin-addons + https://maven.vaadin.com/vaadin-addons + + + diff --git a/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleConfig.java b/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleConfig.java new file mode 100644 index 0000000..f87ea0d --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleConfig.java @@ -0,0 +1,11 @@ +package at.ucs.magnolia.updates; + +import info.magnolia.module.delta.Task; + +import java.util.List; + +public interface IntranetUpdateModuleConfig { + List getInitialUpdateTasks(); + String getYamlUpdateDir(); + String getUpdateTaskPackage(); +} diff --git a/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleVersionHandler.java b/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleVersionHandler.java new file mode 100644 index 0000000..ffdf9ee --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/IntranetUpdateModuleVersionHandler.java @@ -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 getExtraInstallTasks(InstallContext installContext) { + return getModuleConfig().getInitialUpdateTasks(); + } + + @Override + public List getDeltas(InstallContext installContext, Version from) { + List deltaList = new ArrayList<>(); + + if (from == null) { + deltaList.add(getInstall(installContext)); + } + + deltaList.addAll(getUpdateDeltas(installContext, from)); + return deltaList; + } + + protected List 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 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 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/at/ucs/magnolia/updates/ModuleUpdate.java b/src/main/java/at/ucs/magnolia/updates/ModuleUpdate.java new file mode 100644 index 0000000..291f4b3 --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/ModuleUpdate.java @@ -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 getUpdateTasks() { + return Collections.emptyList(); + } + + public List getUpdateTasksAfterBootstrap() { + return Collections.emptyList(); + } + + public List getAllTasks() { + List 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; + } +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/BootstrapUpdateYamlsWithProperties.java b/src/main/java/at/ucs/magnolia/updates/util/BootstrapUpdateYamlsWithProperties.java new file mode 100644 index 0000000..213b09b --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/BootstrapUpdateYamlsWithProperties.java @@ -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); + } + +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/LoggingTask.java b/src/main/java/at/ucs/magnolia/updates/util/LoggingTask.java new file mode 100644 index 0000000..5c5dea6 --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/LoggingTask.java @@ -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()); + } +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/MagnoliaPropertyResolver.java b/src/main/java/at/ucs/magnolia/updates/util/MagnoliaPropertyResolver.java new file mode 100644 index 0000000..8df2dab --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/MagnoliaPropertyResolver.java @@ -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)); + } + +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/TaskWrapper.java b/src/main/java/at/ucs/magnolia/updates/util/TaskWrapper.java new file mode 100644 index 0000000..88dd59e --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/TaskWrapper.java @@ -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()); + } + } + } +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/UcsBootstrapUtil.java b/src/main/java/at/ucs/magnolia/updates/util/UcsBootstrapUtil.java new file mode 100644 index 0000000..e7f3768 --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/UcsBootstrapUtil.java @@ -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 list = new ArrayList<>(Arrays.asList(resourceNames)); + if (list.contains(null)) { + throw new IllegalArgumentException("Resource names contain a entry that cannot be processed."); + } + + Collections.sort(list, new StringLengthComparator()); + + for (Iterator 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); + } + +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/VersionComparator.java b/src/main/java/at/ucs/magnolia/updates/util/VersionComparator.java new file mode 100644 index 0000000..5c4288f --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/VersionComparator.java @@ -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 { + + @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); + } + +} diff --git a/src/main/java/at/ucs/magnolia/updates/util/VersionUtil.java b/src/main/java/at/ucs/magnolia/updates/util/VersionUtil.java new file mode 100644 index 0000000..05a40fa --- /dev/null +++ b/src/main/java/at/ucs/magnolia/updates/util/VersionUtil.java @@ -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); + } + } + +}