This is an Alfresco Content Services module to perform filer operations. It adds the ability to define rules to move an incoming or an updated document into the desired folder structure based on its characteristics (mainly its type, aspects and metadata). It offers a fluent API to define each level of the classification and whether they should be created on the fly. It also allows to inherit specific metadata that are defined at any level of the folder hierarchy to ensure consistency and availability.
- Runs with Alfresco Content Services 6.2 and JDK 11 (compatible with ACS 5.2 and JDK 8)
- Standard JAR packaging and layout
- AMP as an assembly
- Tested with JUnit 5, Mockito 3 and PostgreSQL 10
The project can be built and tested by running the following Maven command:
mvn -Pdelivery clean package
The artifacts can be added to the dependency of your project in its pom.xml:
<dependency>
<groupId>com.atolcd.alfresco.filer</groupId>
<artifactId>alfresco-filer-core</artifactId>
<version>1.1.0</version>
</dependency>
The core of the module is based on Alfresco Content Services policies to detect nodes to be filed and an engine to execute filer actions.
The filer defines 3 concepts:
- a fileable: a node (document or folder) that can be automatically filed,
- a filer subscriber: a container in which nodes are automatically filed,
- a filer segment: a folder that is part of a hierarchy in which a node is filed, it can be deleted automatically if empty
The filer engine uses policies to detect changes in the repository and trigger its own rule mechanism to adapt the classification of the nodes:
- onCreateChildAssociation on a filer subscriber, to label the incoming node as fileable,
- onAddAspect on a fileable node, to trigger the initial classification,
- onUpdateProperties and onMoveNode on a fileable node, to check for updates that could change its classification,
- onDeleteNode on a fileable node, to remove a classification left empty.
A filer action is evaluated by the filer engine to determine whether it applies to the node to be filed and then it performs the selected action.
First, it is required to provide the conditions upon which a filer action will be executed. The matching is actually performed in two passes to allow to quickly bypass classification if the node does not support a filer action based on some general requirements such as the containing site, its aspects or type. The second check allows for a more thorough inspection, including for example the metadata of the node. Finally, it is possible to define the action itself. This is indeed the actual classification, which would trigger the navigation or the creation of the folder structure.
Creating a filer action is done by implementing FilerAction
or directly inheriting AbstractFilerAction
.
Let's take a simple example where a document that contains a particular description with a department data (e.g. department: treasury;) created in 2019 should be filed into the "treasury/2019" path inside the document library of the site. Here is the corresponding implementation:
public class DepartmentFilerAction extends AbstractFilerAction {
@Override
public boolean supportsActionResolution(final FilerEvent event) {
return event.getNode().getAspects().contains(ContentModel.ASPECT_TITLED)
&& event.getNode().getType().get().equals(ContentModel.TYPE_CONTENT);
}
@Override
public boolean supportsActionExecution(final RepositoryNode node) {
return node.getProperty(ContentModel.PROP_DESCRIPTION, String.class).orElse("").matches("department:.+;");
}
@Override
protected void execute(final FilerBuilder builder) {
builder.root(FilerNodeUtils::getSiteNodeRef)
.folder()
.named().with(SiteService.DOCUMENT_LIBRARY).get()
.folder().asSegment()
.named().with(node -> {
Pattern regex = Pattern.compile("department:\\s*(.+);");
Matcher matcher = regex.matcher(node.getProperty(ContentModel.PROP_DESCRIPTION, String.class).get());
matcher.find();
return matcher.group(1);
}).getOrCreate()
.folder().asSegment()
.named().withPropertyDate(ContentModel.PROP_CREATED, "yyyy").getOrCreate()
.updateAndMove();
}
}
You can also look at example actions used in the tests and their corresponding folder structure creation put together in a dedicated service.
It is possible to create as many actions as needed. They are automatically registered by the FilerRegistry
if they inherit from AbstractFilerAction
.
You just need to define the corresponding Spring bean:
<bean id="you.name.it" parent="filer.action.base" class="your.implementation.XXXFilerAction"/>
You can also look at example beans used in the tests.
Actions are evaluated by the FilerService
in order. They are first sorted by the explicit order defined in the action (FilerAction
implements Ordered
) and then alphabetically by bean name.
The first action that matches the conditions is selected and its classification is applied.
Another characteristic of this module is the ability to define which metadata should be inherited on the fileable node and also on the folder structure. It uses a specific marker aspect to label which aspects should have their metadata duplicated. First, the metadata of the inherited aspects are retrieved from the parent folder to supplement the node being filed. Then, each level of the classification can define the number of metadata they inherit.
For example, instead of using the description property, a custom aspect with a specific department label property can be set directly on the department folder. In this case any document created in it could also have the property directly added on them to make a search on the department of documents easier. The corresponding action implementation would look like this:
@Override
protected void execute(final FilerBuilder builder) {
builder.root(FilerNodeUtils::getSiteNodeRef)
.folder()
.named().with(SiteService.DOCUMENT_LIBRARY).get()
.folder(MyModel.TYPE_DEPARTMENT).asSegment()
.mandatoryPropertyInheritance(MyModel.ASPECT_DEPARTMENT)
.named().withProperty(MyModel.PROP_DEPARTMENT_LABEL).getOrCreate()
.folder().asSegment()
.named().withPropertyDate(ContentModel.PROP_CREATED, "yyyy").getOrCreate()
.updateAndMove();
}
}