AECU simplifies content migrations by executing migration scripts during package installation. It is built on top of Groovy Console.
Features:
- GUI to run scripts and see history of runs
- Run mode support
- Fallback scripts in case of errors
- Extension of Groovy Console bindings
- Service API
- Health Checks
The tool was presented at adaptTo() conference in Berlin
Table of contents
- Requirements
- Installation
- File and Folder Structure
- Execution of Migration Scripts
- History of Past Runs
- Extension to Groovy Console
- JMX Interface
- Health Checks
- API Documentation
- License
- Changelog
- Developers
AECU requires Java 8 and AEM 6.3 or above. Groovy Console can be installed manually if bundle install is not used.
You can download the package from Maven Central or our releases section. The aecu.ui.apps package will install the AECU software. It requires that you installed Groovy Console before.
<dependency>
<groupId>de.valtech.aecu</groupId>
<artifactId>aecu.ui.apps</artifactId>
<version>LATEST</version>
<type>zip</type>
</dependency>
To simplify installation we provide a bundle package that already includes the Groovy Console. This makes sure there are no compatibility issues. The package is also available on Maven Central or our releases section.
<dependency>
<groupId>de.valtech.aecu</groupId>
<artifactId>aecu.bundle</artifactId>
<version>LATEST</version>
<type>zip</type>
</dependency>
All migration scripts need to be located in /etc/groovyconsole/scripts/aecu. There you can create an unlimited number of folders and files. E.g. organize your files by project or deployment. The content of the scripts is plain Groovy code that can be run via Groovy Console.
There are just a few naming conventions:
- Run modes: folders can contain run modes to limit the execution to a specific target environment. E.g. some scripts are for author only or for your local dev environment.
- Always selector: if a script name ends with ".always.groovy" then it will be executed by install hook on each package installation. There will be no more check if this script was already executed before.
- Fallback selector: if a script name ends with ".fallback.groovy" then it will be executed only if the corresponding script failed with an exception. E.g. if there is "script.groovy" and "script.fallback.groovy" then the fallback script only gets executed if "script.groovy" fails.
This is the preferred method to execute your scripts. It allows to run them without any user interaction. Just package them with a content package and do a regular deployment.
You can add the install hook by adding de.valtech.aecu.core.installhook.AecuInstallHook as a hook to your package properties. The AECU package and Groovy Console need to be installed beforehand.
<plugin>
<groupId>com.day.jcr.vault</groupId>
<artifactId>content-package-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<filterSource>src/main/content/META-INF/vault/filter.xml</filterSource>
<verbose>true</verbose>
<failOnError>true</failOnError>
<group>Valtech</group>
<properties>
<installhook.aecu.class>de.valtech.aecu.core.installhook.AecuInstallHook</installhook.aecu.class>
</properties>
</configuration>
</plugin>
Manual script execution is useful in case you want to manually rerun a script (e.g. because it failed before). You can find the execute feature in AECU's tools menu.
Execution is done in two simple steps:
- Select the base path and run the search. This will show a list of runnable scripts.
- Run all scripts in batch or just single ones. If you run all you can change the order before (drag and drop with marker at the right).
Once execution is done you will see if the script(s) succeeded. Click on the history link to see the details.
You can find the history in AECU's tools menu.
The history shows all runs that were executed via package install hook, manual run and JMX. It will not display scripts that were executed directly via Groovy Console.
You can click on any run to see the full details. This will show the status for each script. You can also see the output of all scripts.
AECU adds its own binding to Groovy Console. You can reach it using "aecu" in your script. This provides methods to perform common tasks like property modification or node deletion.
It follows a collect, filter, execute process.
In the collect phase you define which nodes should be checked for a migration.
- forResources(String[] paths): use the given paths without any subnodes
- forChildResourcesOf(String path): use all direct childs of the given path (but no grandchilds)
- forDescendantResourcesOf(String path): use the whole subtree under this path excluding the parent root node
- forResourcesInSubtree(String path): use the whole subtree under this path including the parent root node
You can call these methods multiple times and combine them. They will be merged together.
Example:
println aecu.contentUpgradeBuilder()
.forResources((String[])["/content/we-retail/ca/en"])
.forChildResourcesOf("/content/we-retail/us/en")
.forDescendantResourcesOf("/content/we-retail/us/en/experience")
.forResourcesInSubtree("/content/we-retail/us/en/experience")
.doSetProperty("name", "value")
.run()
These methods can be used to filter the nodes that were collected above. Multiple filters can be applied for one run.
Filters the resources by property values.
- filterByHasProperty: matches all nodes that have the given property. The value of the property is not relevant.
- filterByProperty: matches all nodes that have the given attribute value. Filter does not match if attribute is not present.
- filterByProperties: use this to filter by a list of property values (e.g. sling:resourceType). All properties in the map are required to to match. Filter does not match if attribute does not exist.
- filterByMultiValuePropContains: checks if all condition values are contained in the defined attribute. Filter does not match if attribute does not exist.
filterByHasProperty(String name)
filterByProperty(String name, Object value)
filterByProperties(Map<String, String> properties)
filterByMultiValuePropContains(String name, Object[] conditionValues)
Example:
def conditionMap = [:]
conditionMap["sling:resourceType"] = "weretail/components/structure/page"
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByHasProperty("myProperty")
.filterByProperty("sling:resourceType", "wcm/foundation/components/responsivegrid")
.filterByProperties(conditionMap)
.filterByMultiValuePropContains("myAttribute", ["value"] as String[])
.doSetProperty("name", "value")
.run()
You can also filter nodes by their name.
- filterByNodeName(String name): process only nodes which have this exact name
- filterByNodeNameRegex(String regex): process nodes that have a name that matches the given regular expression
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.filterByNodeNameRegex("jcr.*")
.doSetProperty("name", "value")
.run()
You can combine filters with AND and OR to build more complex filters.
def conditionMap_type = [:]
conditionMap_type['sling:resourceType'] = "weretail/components/content/heroimage"
def conditionMap_file = [:]
conditionMap_file['fileReference'] = "/content/dam/we-retail/en/activities/running/fitness-woman.jpg"
def conditionMap_page = [:]
conditionMap_page['jcr:primaryType'] = "cq:PageContent"
def complexFilter = new ORFilter(
[ new FilterByProperties(conditionMap_page),
new ANDFilter( [
new FilterByProperties(conditionMap_type),
new FilterByProperties(conditionMap_file)
] )
])
println aecu.contentUpgradeBuilder()
.forDescendantResourcesOf("/content/we-retail/ca/en", false)
.filterWith(complexFilter)
.doSetProperty("name", "value")
.run()
- doSetProperty(String name, Object value): sets the given property to the value. Any existing value is overwritten.
- doDeleteProperty(String name): removes the property with the given name if existing.
- doRenameProperty(String oldName, String newName): renames the given property if existing. If the new property name already exists it will be overwritten.
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.doSetProperty("name", "value")
.doDeleteProperty("nameToDelete")
.doRenameProperty("oldName", "newName")
.run()
- doAddValuesToMultiValueProperty(String name, String[] values): adds the list of values to a property. The property is created if it does not yet exist.
- doRemoveValuesOfMultiValueProperty(String name, String[] values): removes the list of values from a given property.
- doReplaceValuesOfMultiValueProperty(String name, String[] oldValues, String[] newValues): removes the old values and adds the new values in a given property.
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.doAddValuesToMultiValueProperty("name", (String[])["value1", "value2"])
.doRemoveValuesOfMultiValueProperty("name", (String[])["value1", "value2"])
.doReplaceValuesOfMultiValueProperty("name", (String[])["old1", "old2"], (String[])["new1", "new2"])
.run()
This will copy or move a property to a subnode. You can also change the property name.
- doCopyPropertyToRelativePath(String name, String newName, String relativeResourcePath): copy the property to the given path under the new name.
- doMovePropertyToRelativePath(String name, String newName, String relativeResourcePath): move the property to the given path under the new name.
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.doCopyPropertyToRelativePath("name", "newName", "subnode")
.doMovePropertyToRelativePath("name", "newName", "subnode")
.run()
The matching nodes can be copied/moved to a new location. You can use ".." if you want to step back in path.
- doCopyResourceToRelativePath(String relativePath): copies the node to the given target path
- doMoveResourceToRelativePath(String relativePath): moves the node to the given target path
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.doCopyResourceToRelativePath("subNode")
.doCopyResourceToRelativePath("../subNode")
.doMoveResourceToRelativePath("subNode")
.run()
You can delete all nodes that match your collection and filter.
- doDeleteResource(): deletes the matching nodes
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.doDeleteResource()
.run()
AECU can run actions on the page that contains a filtered resource. This is e.g. helpful if you filter by page resource type. Please note that there is no check for duplicate actions. If you run a page action for two resources in the same page then the action will be executed twice.
- doActivateContainingPage(): activates the page that contains the current resource
- doDeactivateContainingPage(): deactivates the page that contains the current resource
- doDeleteContainingPage(): deletes the page (incl. subpages) that contains the current resource
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByProperty("sling:resourceType", "weretail/components/structure/page")
.doActivateContainingPage()
.doDeactivateContainingPage()
.doDeleteContainingPage()
.run()
Sometimes, you only want to print the path of the matched nodes.
- printPath(): prints the path of the matched node
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.filterByNodeName("jcr:content")
.printPath()
.run()
You can also hook in custom code to perform actions on resources. For this "doCustomResourceBasedAction()" can take a Lambda expression.
- doCustomResourceBasedAction(): run your custom code
def myAction = {
resource ->
hasChildren = resource.hasChildren()
String output = resource.path + " has children: "
output += hasChildren ? "yes" : "no"
return output
}
println aecu.contentUpgradeBuilder()
.forChildResourcesOf("/content/we-retail/ca/en")
.doCustomResourceBasedAction(myAction)
.run()
At the end you can run all actions or perform a dry-run first. The dry-run will just provide output about modifications but not save any changes. The normal run saves the session, no additional "session.save()" is required.
- run(): performs all actions and saves the session
- dryRun(): only prints actions but does not perform repository changes
- run(boolean dryRun): the "dryRun" parameter defines if it should be a run or dry-run
AECU provides JMX methods for executing scripts and reading the history. You can also check the version here.
This will execute the given script or folder. If a folder is specified then all files (incl. any subfolders) are executed. AECU will respect run modes during execution.
Parameters:
- Path: file or folder to execute
Prints the history of the specified last runs. The entries are sorted by date and start with the last run.
Parameters:
- Start index: starts with 0 (= latest history entry)
- Count: number of entries to print
This will print all files that are executable for a given path. You can use this to check which scripts of a given folder would be executed.
Parameters:
- Path: file or folder to check
Health checks show you the status of AECU itself and the last migration run. You can access them on the status page. For the status of older runs use AECU's history page.
You can access our AECU service (AecuService class) in case you have special requirements. See the API documentation.
The AECU tool is licensed under the MIT LICENSE.
Please see our history file.
See our developer zone.