From aiup-alfresco
Scaffolds a custom Alfresco ContentStore connector (extending AbstractContentStore) with reader/writer, Spring wiring, and unit test. In-Process SDK (Maven) only.
How this command is triggered — by the user, by Claude, or both
Slash command
/aiup-alfresco:content-store [path to REQUIREMENTS.md or description]This command is limited to the following tools:
The summary Claude sees in its command listing — used to decide when to auto-load this command
# /content-store — Custom Content Store Generator > **In-Process SDK only** — content stores deploy inside the ACS JVM as part of the Platform > JAR. The repository reads/writes all binaries through the `ContentService`, which delegates > to the configured content store. **Never** read or write files on the local filesystem > directly — go through the store's `ContentReader`/`ContentWriter`. Generate a custom content store connector from requirements. ## Input Read `REQUIREMENTS.md` to identify content store requirements: 1. Resolve the Platform JAR project's `Root path` from Section 2...
In-Process SDK only — content stores deploy inside the ACS JVM as part of the Platform JAR. The repository reads/writes all binaries through the
ContentService, which delegates to the configured content store. Never read or write files on the local filesystem directly — go through the store'sContentReader/ContentWriter.
Generate a custom content store connector from requirements.
Read REQUIREMENTS.md to identify content store requirements:
Resolve the Platform JAR project's Root path from Section 2 (Project Architecture).
Platform JAR project, stop and explain that /content-store
only applies to the in-process Platform JAR project.Read the "Content store requirements" sub-section (Section 8 Deployment Requirements or a dedicated storage section).
/requirements first (or provide a
description as $ARGUMENTS).AbstractContentStore),
or a wrapper over the default store (caching / encrypting / aggregating).From Section 2, derive:
{platform-project-root} — . for Platform JAR only mode; {name}-platform/ for Mixed mode{module-id} — the Platform JAR artifactId (bare artifact ID, e.g. my-extension).
Read from <artifactId> in the platform pom.xml or derive as {platform-artifactId} from
Section 2. Never use the full module.id property value as the directory name.{java-package} — the Java package declared in Section 2{prefix} — the namespace prefix declared in Section 5 (Content Model Requirements)Derive from content store requirements:
{Store} — PascalCase store name (e.g. S3, Encrypted, Tiered){store-root-property} — the configurable root location property (e.g. dir.contentstore.{prefix})The content store class and its Spring wiring are required. Generate dedicated reader/writer classes only for a standalone backing store; a wrapper store delegates to the wrapped store's reader/writer.
{platform-project-root}/src/main/java/{package}/content/{Store}ContentStore.java
package {package}.content;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class {Store}ContentStore extends AbstractContentStore {
private static final Logger LOG = LoggerFactory.getLogger({Store}ContentStore.class);
private String rootLocation;
@Override
public boolean isWriteSupported() {
return true;
}
@Override
public ContentReader getReader(String contentUrl) {
// Resolve contentUrl to a backing object and return a reader over it
return new {Store}ContentReader(contentUrl, rootLocation);
}
@Override
public ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl) {
// newContentUrl may be null — generate one with createNewUrl()/newContentUrl if so
String url = (newContentUrl != null) ? newContentUrl : createNewUrl();
return new {Store}ContentWriter(url, rootLocation, existingContentReader);
}
@Override
public String getRootLocation() {
return rootLocation;
}
public void setRootLocation(String rootLocation) {
this.rootLocation = rootLocation;
}
}
Key rules for the content store class:
org.alfresco.repo.content.AbstractContentStore — it provides URL handling and the
getWriter(...) template method that calls your getWriterInternal(...).isWriteSupported(), getReader(String), getWriterInternal(ContentReader, String),
and getRootLocation().protocol://path form (e.g. store://...); use the inherited helpers
(createNewUrl()) to mint new URLs rather than hand-building them.{platform-project-root}/src/main/java/{package}/content/{Store}ContentReader.java
package {package}.content;
import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.service.cmr.repository.ContentReader;
import java.nio.channels.ReadableByteChannel;
public class {Store}ContentReader extends AbstractContentReader {
private final String rootLocation;
protected {Store}ContentReader(String contentUrl, String rootLocation) {
super(contentUrl);
this.rootLocation = rootLocation;
}
@Override
public boolean exists() {
// Return true if the backing object for getContentUrl() exists
return false;
}
@Override
public long getLastModified() {
return 0L;
}
@Override
public long getSize() {
return 0L;
}
@Override
protected ContentReader createReader() {
return new {Store}ContentReader(getContentUrl(), rootLocation);
}
@Override
protected ReadableByteChannel getDirectReadableChannel() {
// Open and return a channel over the backing object
throw new UnsupportedOperationException("Implement backing read channel");
}
}
{platform-project-root}/src/main/java/{package}/content/{Store}ContentWriter.java
package {package}.content;
import org.alfresco.repo.content.AbstractContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import java.nio.channels.WritableByteChannel;
public class {Store}ContentWriter extends AbstractContentWriter {
private final String rootLocation;
protected {Store}ContentWriter(String contentUrl, String rootLocation, ContentReader existingContentReader) {
super(contentUrl, existingContentReader);
this.rootLocation = rootLocation;
}
@Override
public long getSize() {
return 0L;
}
@Override
protected ContentReader createReader() {
return new {Store}ContentReader(getContentUrl(), rootLocation);
}
@Override
protected WritableByteChannel getDirectWritableChannel() {
// Open and return a channel that writes to the backing object
throw new UnsupportedOperationException("Implement backing write channel");
}
}
{platform-project-root}/src/main/resources/alfresco/extension/{prefix}-content-store-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- The custom store. Its root location is property-configurable. -->
<bean id="{prefix}.{store}ContentStore"
class="{package}.content.{Store}ContentStore">
<property name="rootLocation" value="${dir.contentstore.{prefix}:${dir.contentstore}}"/>
</bean>
<!--
Make the custom store the active store. ACS resolves binaries through the bean named
'fileContentStore'. Overriding that bean id points the repository at the custom store.
For a WRAPPER (caching/encrypting), inject the real fileContentStore as the delegate
instead of replacing it outright.
-->
<bean id="fileContentStore"
class="{package}.content.{Store}ContentStore">
<property name="rootLocation" value="${dir.contentstore.{prefix}:${dir.contentstore}}"/>
</bean>
</beans>
Use an
alfresco/extension/context file (auto-discovered) so the store override loads after the core content services are defined. Do not place it underalfresco/module/.../context/if it overridesfileContentStore— the extension classpath is the correct override location.
Key rules for wiring:
fileContentStore. Overriding that id swaps in the
custom store; for a caching/encrypting wrapper, set the wrapped store as a delegate property
rather than discarding the default.${dir.contentstore.{prefix}:${dir.contentstore}}).{platform-project-root}/src/test/java/{package}/content/{Store}ContentStoreTest.java
package {package}.content;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class {Store}ContentStoreTest {
private {Store}ContentStore store;
@BeforeEach
void setUp() {
store = new {Store}ContentStore();
store.setRootLocation("test-root");
}
@Test
void isWriteSupported_returnsTrue() {
assertTrue(store.isWriteSupported());
}
@Test
void getRootLocation_returnsConfiguredValue() {
assertEquals("test-root", store.getRootLocation());
}
}
{module-id} is the Platform JAR artifactId — the bare artifact ID. Derive it as
{platform-artifactId} from Section 2 or from <artifactId> in the platform pom.xml.
Never use the full module.id property value as the directory name.{platform-project-root} is . for Platform JAR only mode, or {name}-platform/ for Mixed modesrc/main/java/{package}/content/{prefix}.{store}ContentStore; the active store is the bean named fileContentStoredir.contentstore.{prefix} with a default of ${dir.contentstore}AbstractContentReader / AbstractContentWriterContentReader/ContentWriterACS ships org.alfresco.repo.content.caching.CachingContentStore (wraps a slow/remote backing
store with a local cache) and an encrypting content store. For a wrapper, set the backing store
as a delegate property on the wrapper bean and make the wrapper the fileContentStore —
the backing store remains a separate bean it delegates to.
npx claudepluginhub aborroy/aiup-alfresco