Verbessertes Module Version Handling

Einführung

Der Magnolia Module-Mechanismus erlaubt es Entwicklern, den Lebenszyklus von Magnolia Modulen zu verwalten. Jedes Magnolia Module definiert in seinem Module-Deskriptor XML File verschiedene Module-Klassen, wovon eine der sogenannte VersionHandler ist. (Siehe Module descriptor elements). Im Module-Descriptor registriere ich also eine Klasse, welche das ModuleVersionHandler Interface implementiert. Magnolia stellt hier bereits Basisklassen zur Verfügung, welche sich z.B. um die Unterscheidung von zwischen Install- oder Update-Modus kümmern.

In diesem kurzen Post möchte ich zeigen, wie man die Magnolia Klassen erweitern kann, um möglichst effizient verschiedene Use Cases der Module-Entwicklung abzudecken.

Problemstellung

Die Magnolia Klassen AbstractModuleVersionHandler und DefaultModuleVersionHandler stellen eine ziemlich saubere Basis für die Behandlung von Delta Updates zur Verfügung. Da wir aber in unseren Projekten Magnolia-Konfigurationen fast ausschliesslich dynamisch via Delta Tasks verwalten, hat uns immer wieder die Möglichkeit gefehlt, solche Tasks sowohl beim Installieren als auch beim Update auszuführen. Dazu kommt das Bedürfnis, während der Entwicklung Startup-Tasks auszuführen, welche auf einer normalen Umgebung nur als Update Task laufen.

Lösungsansatz

Aufgrund dieser wiederkehrenden Bedürfnisse habe ich eine VersionHandler Klasse implementiert, welche sich dieser Problematik annimmt. Die Klasse verwendet die Möglichkeiten von Umgebungsspezifischen Konfigurationen, welche in einem der letzten Posts beschrieben sind. Entsprechend sind folgende sprechenden Methoden vorhanden, um Tasks zu registrieren:

  • getEarlyInstallTasks()
  • getInstallOnlyTasks()
  • getInstallAndUpdateTasks()
  • getUpdateOnlyTasks()
  • getModuleStartupTasks()
  • getSnapshotStartupTasks()
  • getLocalDevelopmentStartupTasks()

Der daraus resultierende Lösungsansatz für erweitertes VersionHandling sieht wie folgt aus:

EnhancedModuleVersionHandler.java

import info.magnolia.module.DefaultModuleVersionHandler;
import info.magnolia.module.InstallContext;
import info.magnolia.module.ModuleManager;
import info.magnolia.module.delta.Task;
import info.magnolia.module.model.Version;
import info.magnolia.objectfactory.Components;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


public abstract class EnhancedModuleVersionHandler extends DefaultModuleVersionHandler {

    public EnhancedModuleVersionHandler() {
    }

    /**
     * This Method should only be used to execute tasks at the very beginning of module installation.
     *
     * @param installContext the current InstallContext
     * @return a list of tasks to be executed at the very beginning of module installation.
     */

    @Override
    protected final List<Task> getBasicInstallTasks(InstallContext installContext) {
        final List<Task> installTasks = new LinkedList<Task>();
        Version forVersion = getVersionFromInstallContext(installContext);

        installTasks.addAll(safeList(getEarlyInstallTasks(installContext, forVersion)));
        installTasks.addAll(safeList(super.getBasicInstallTasks(installContext)));

        return installTasks;
    }

    @Override
    protected final List<Task> getExtraInstallTasks(InstallContext installContext) { //when module node does not exist
        List<Task> installTasks = new LinkedList<Task>();
        Version forVersion = getVersionFromInstallContext(installContext);

        installTasks.addAll(safeList(super.getExtraInstallTasks(installContext)));

        installTasks.addAll(safeList(getInstallAndUpdateTasks(installContext, forVersion)));
        installTasks.addAll(safeList(getInstallOnlyTasks(installContext, forVersion)));

        return installTasks;
    }

    @Override
    protected final List<Task> getDefaultUpdateTasks(Version forVersion) { //on every module update
        List<Task> installTasks = new LinkedList<Task>();
        InstallContext installContext = getCurrentInstallContext();

        installTasks.addAll(safeList(super.getDefaultUpdateTasks(forVersion)));

        installTasks.addAll(safeList(getInstallAndUpdateTasks(installContext, forVersion)));
        installTasks.addAll(safeList(getUpdateOnlyTasks(installContext, forVersion)));

        return installTasks;
    }

    @Override
    protected final List<Task> getStartupTasks(InstallContext installContext) {
        List<Task> installTasks = new LinkedList<Task>();
        Version forVersion = getVersionFromInstallContext(installContext);

        installTasks.addAll(safeList(getModuleStartupTasks(installContext, forVersion)));

        if (isSnapshot(forVersion)) {
            installTasks.addAll(safeList(getSnapshotStartupTasks(installContext, forVersion)));
        }

        if (EnvironmentUtil.isLocalDevelopmentEnvironment()) {
            installTasks.addAll(safeList(getLocalDevelopmentStartupTasks(installContext, forVersion)));
        }

        return installTasks;
    }

    /**
     * Tasks to be executed at the very beginning of a Module installation, before the basic install tasks. (May be null)
     * <p/>
     * Can be used to register nodetypes before {@link info.magnolia.module.delta.ModuleBootstrapTask} is executed.
     */

    protected abstract List<? extends Task> getEarlyInstallTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed on Module Install (May be null)
     * Will be executed after {@link #getInstallAndUpdateTasks(InstallContext, Version)}
     */

    protected abstract List<? extends Task> getInstallOnlyTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed on Module Install and EVERY Update Delta (May be null)
     * --> This means that if you reset the module version to 0, these tasks will be executed for every Update step from 0 to the current version.
     */

    protected abstract List<? extends Task> getInstallAndUpdateTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed EVERY Update Delta (May be null)
     * --> This means that if you reset the module version to 0, these tasks will be executed for every Update step from 0 to the current version.
     * Will be executed after {@link #getInstallAndUpdateTasks(InstallContext, Version)}
     */

    protected abstract List<? extends Task> getUpdateOnlyTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed when the module starts up (May be null)
     */

    protected abstract List<? extends Task> getModuleStartupTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed when the module starts up with a SNAPSHOT version. (May be null)
     */

    protected abstract List<? extends Task> getSnapshotStartupTasks(InstallContext installContext, Version forVersion);

    /**
     * Tasks to be executed when the module starts up on local development. (May be null)
     *
     * @see EnvironmentUtil#isLocalDevelopmentEnvironment()
     */

    protected abstract List<? extends Task> getLocalDevelopmentStartupTasks(InstallContext installContext, Version forVersion);

    protected final Version getVersionFromInstallContext(InstallContext installContext) {
        return installContext.getCurrentModuleDefinition().getVersion();
    }

    protected final InstallContext getCurrentInstallContext() {
        return Components.getComponent(ModuleManager.class).getInstallContext();
    }

    protected final boolean isSnapshot(Version version) {
        return "SNAPSHOT".equalsIgnoreCase(version.getClassifier());
    }

    protected final <T> List<T> safeList(List<T> unsafeList) {
        return unsafeList != null ? unsafeList : new ArrayList<T>();
    }
}

Anwendungsfall

Am häufigsten verwende ich die Variante getInstallAndUpdateTasks(). Mein Modul soll immer den aktuellsten Stand im Repository anlegen, egal ob es gerade installiert oder aktualisiert wird. Allfällige Content-Migrationen oder andere Version-Updates organisiere ich weiterhin mit der register(Delta delta) Methode.

In seltenen Fällen sollen Konfigurationen nur einmalig angelegt werden, wozu dann getInstallOnlyTasks() verwendet wird. Dies kann beispielsweise verwendet werden, um Konfigurationen nur einmalig als Standardwerte anzulegen, um diese nachher manuell zu verwalten.

Ebenfalls sehr hilfreich sind Tasks, welche nur unter gewissen Umständen ausgeführt werden. Hier verwende ich Methoden wie getSnapshotStartupTasks() oder getLocalDevelopmentStartupTasks(), welche vor allem während der Entwicklung von Modulen sehr hilfreich sein können.

Schlussendlich kann VersionHandling so aussehen:

MyModuleVersionHandler.java

import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.DeltaBuilder;
import info.magnolia.module.delta.Task;
import info.magnolia.module.model.Version;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class MyModuleVersionHandler extends EnhancedModuleVersionHandler {

    public MyModuleVersionHandler() {
        register(DeltaBuilder.update("2.0.0", "Migration Tasks for version 2.0.0")
                .addTask(new HeavyContentMigrationTask())
        );
    }

    @Override
    protected List<? extends Task> getEarlyInstallTasks(InstallContext installContext, Version forVersion) {
        return null;
    }

    @Override
    protected List<? extends Task> getInstallOnlyTasks(InstallContext installContext, Version forVersion) {
        return Arrays.asList(
                new FancyInitialConfigurationTask()
        );
    }

    @Override
    protected List<? extends Task> getInstallAndUpdateTasks(InstallContext installContext, Version forVersion) {
        return Arrays.asList(
                new MakeThingsGoodTask(),
                new PolishAdminInterfaceTask(),
                new AddConfigXYZTask()
        );
    }

    @Override
    protected List<? extends Task> getUpdateOnlyTasks(InstallContext installContext, Version forVersion) {
        return Arrays.asList(
                new EmptyImagingWorkspaceTask()
        );
    }

    @Override
    protected List<? extends Task> getModuleStartupTasks(InstallContext installContext, Version forVersion) {
        return null;
    }

    @Override
    protected List<? extends Task> getSnapshotStartupTasks(InstallContext installContext, Version forVersion) {
        List<Task> startupTasks = new LinkedList<Task>();

        //execute all general update tasks on snapshot for certain environments
        final boolean anyEnvironment = EnvironmentUtil.isAnyEnvironment(
                "dev-author",
                "dev-public-1",
                "dev-public-2"
        );

        if (anyEnvironment) {
            startupTasks.addAll(getInstallAndUpdateTasks(installContext, forVersion));
            startupTasks.addAll(getUpdateOnlyTasks(installContext, forVersion));
        }

        return startupTasks;
    }

    @Override
    protected List<? extends Task> getLocalDevelopmentStartupTasks(InstallContext installContext, Version forVersion) {
        List<Task> startupTasks = new LinkedList<Task>();

        //execute all general update tasks on local development
        startupTasks.addAll(getInstallAndUpdateTasks(installContext, forVersion));
        startupTasks.addAll(getUpdateOnlyTasks(installContext, forVersion));

        //add additional tasks
        startupTasks.addAll(Arrays.asList(
                new DisableSoftLockingTask(),
                new BootstrapTestCategoriesTask(),
                new DisableDefaultSubscriberTask()
        ));

        return startupTasks;
    }
}

Fazit

Aufgrund der guten Basis, welche Magnolia out-of-the-box liefert, kann mit wenig Aufwand eine schöne Lösung für optimiertes ModuleVersionHandling gefunden werden. Besonders die Möglichkeit, während der lokalen Entwicklung regelmässig die Konfigurationen aktuell zu halten, ermöglicht effizientes Arbeiten in grösseren Entwicklungsteams. Jeder Entwickler hat nach jedem Neustart automatisch die aktuellsten Konfigurationen in seiner Entwicklungsinstanz eingespielt.

Feedback und Anregungen sind wie immer herzlich willkommen!

Ein Gedanke zu “Verbessertes Module Version Handling

  1. Pingback: Umgebungsspezifische Konfigurationen in Magnolia CMS – Magnolia

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>