System Architecture
Venzia Datalinks is built as a comprehensive Alfresco Content Services add-on with three main architectural layers:
Backend Layer (Java)
Alfresco Repository AMP that handles datalink registration, validation, and REST API endpoints
Frontend Layer (Angular)
Alfresco Content App extension providing UI components and state management
Integration Layer
REST connector framework for external data sources with authentication support
Backend Architecture
Core Components
The backend is organized into three main packages:
1. DataLink Component
The main module component that initializes on Alfresco startup:
package es.venzia.aqua.datalink;
public class DataLinkComponent extends AbstractModuleComponent {
private NodeService nodeService;
private NodeLocatorService nodeLocatorService;
@Override
protected void executeInternal() throws Throwable {
logger.info("DataLinkComponent has been executed");
}
}
Configured in service-context.xml:
<bean id="es.venzia.aqua.datalink.DataLinkComponent"
class="es.venzia.aqua.datalink.DataLinkComponent"
parent="module.baseComponent">
<property name="moduleId" value="aqua-datalink-platform-jar" />
<property name="nodeService" ref="NodeService" />
<property name="nodeLocatorService" ref="nodeLocatorService" />
</bean>
2. Schema Validation (SchemaLoader)
Loads and validates datalink configurations against JSON Schema:
package es.venzia.aqua.datalink.core;
public class SchemaLoader {
private String schemaFile;
private Schema schema = null;
public void init() throws JSONException, IOException {
Resource resource = resourceLoader.getResource(schemaFile);
String body = IOUtils.toString(resource.getInputStream(),
StandardCharsets.UTF_8.name());
JSONObject jsonSchema = new JSONObject(new JSONTokener(body));
org.everit.json.schema.loader.SchemaLoader loader =
org.everit.json.schema.loader.SchemaLoader.builder()
.schemaJson(jsonSchema)
.draftV7Support()
.build();
this.schema = loader.load().build();
logger.info("Datalink Schema has been registered");
}
}
The schema is loaded from classpath:alfresco/schema/datalink.schema.json and validates all datalink configuration files on startup.
3. DataLink Registration (RegisterDataLink)
Scans and registers all datalink configurations:
package es.venzia.aqua.datalink.core;
public class RegisterDataLink {
private SchemaLoader schemaLoader;
@Value("classpath:alfresco/extension/datalink/datalink-*.json")
Resource[] resources;
private JSONArray dataLinks;
public void init() throws IOException {
dataLinks = new JSONArray();
for (Resource resource : resources) {
try {
String jsonData = IOUtils.toString(
resource.getInputStream(),
StandardCharsets.UTF_8.name()
);
JSONObject datalinkEntry = new JSONObject(jsonData);
schemaLoader.getSchema().validate(datalinkEntry);
dataLinks.put(datalinkEntry);
logger.info("Datalink " + datalinkEntry.getString("name")
+ " added");
} catch (ValidationException ex) {
logger.error(ex);
}
}
}
}
All datalink JSON files must match the pattern datalink-*.json in the alfresco/extension/datalink/ directory to be automatically discovered.
Web Script Layer
The REST API endpoint exposes registered datalinks to the frontend:
package es.venzia.aqua.datalink.webscript;
public class GetListDataLinkWebScript extends AbstractWebScript {
private RegisterDataLink registerDataLink;
@Override
public void execute(WebScriptRequest req, WebScriptResponse res)
throws IOException {
res.setContentType(Format.JSON.mimetype());
res.getWriter().write(registerDataLink.getDataLinks().toString());
res.getWriter().flush();
}
}
Configured in webscript-context.xml:
<bean id="webscript.venzia.datalink.v1.datalink-list.get"
class="es.venzia.aqua.datalink.webscript.GetListDataLinkWebScript"
parent="webscript">
<property name="registerDataLink" ref="registerDataLink" />
</bean>
Endpoint: GET /alfresco/service/aqua/datalink/v1/list
Frontend Architecture
Angular Module Structure
The frontend is organized as an Angular standalone library:
@NgModule({
declarations: [DatalinkDialogComponent, AddDatalinkDialogComponent],
imports: [
VenziaDatalinkStoreModule,
TranslateModule,
MatDialogModule,
MatIconModule,
DatalinkListComponent,
DatalinkAddPanelComponent,
MatButtonModule
],
providers: [provideExtensionConfig(['venzia-datalink.plugin.json'])]
})
export class VenziaDatalinkModule {
constructor(translation: TranslationService, extensions: ExtensionService) {
translation.addTranslationFolder('datalink', 'assets/datalink');
extensions.setEvaluators({
'datalink.canDatalink': rules.canDatalink
});
}
}
Service Layer
VenziaDatalinkApiService
Communicates with Alfresco backend:
@Injectable({ providedIn: 'root' })
export class VenziaDatalinkApiService {
constructor(
private apiService: AlfrescoApiService,
private logService: LogService
) {}
loadDataLink(): Observable<any> {
return from(
this.executeWebScript('GET', 'aqua/datalink/v1/list',
null, null, 'service', null)
).pipe(
catchError((err) => this.handleError(err))
);
}
private executeWebScript(
httpMethod: string,
scriptPath: string,
scriptArgs?: any,
contextRoot?: string,
servicePath?: string,
postBody?: any
): Promise<any> {
return this.apiService
.getInstance()
.contentClient.callApi(
scriptPath, httpMethod, {}, scriptArgs, {}, {},
postBody, contentTypes, accepts, null,
contextRoot + '/' + servicePath
);
}
}
VenziaDatalinkRestService
Handles external REST API calls with authentication:
@Injectable({ providedIn: 'root' })
export class VenziaDatalinkRestService {
constructor(private http: HttpClient) {}
callApi(endpoint: string, params?: any, authdata?: string): Observable<any> {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
Authorization: `Basic ${authdata}`
}),
params: {}
};
if (params) {
httpOptions.params = params;
}
return this.http.get(endpoint, httpOptions)
.pipe(map(this.extractData));
}
}
State Management (NgRx)
Datalinks use NgRx for state management:
@Injectable()
export class VenziaDatalinkEffects {
dataLinkDoc$ = createEffect(
() => this.actions$.pipe(
ofType<DataLinkOpenAction>(VenziaDatalinkActionTypes.DatalinkOpen),
map((action) => {
if (action.payload) {
this.venziaDatalinkService.dataLinkDoc(action.payload);
} else {
this.store
.select(getAppSelection)
.pipe(take(1))
.subscribe((selection) => {
if (!selection.isEmpty) {
this.venziaDatalinkService.dataLinkDoc(selection.first);
}
});
}
})
),
{ dispatch: false }
);
}
Component Hierarchy
@Component({
selector: 'aqua-datalink-manager',
templateUrl: './datalink-manager.component.html',
standalone: true
})
export class DataLinkManagerComponent {
@Input() nodeId: string;
@ViewChild('datalinkList') datalinkList: DatalinkListComponent;
openAddDatalinkDialog(event: Event) {
this.datalinkList.openAddDatalinkDialog(event);
}
removeSelectionRows(event: Event) {
this.datalinkList.deleteSelectRows(event);
}
}
Data Flow
Initialization Flow
Alfresco Startup
SchemaLoader loads JSON schema → RegisterDataLink scans and validates datalink configurations → DataLinkComponent executes
Frontend Load
VenziaDatalinkModule registers extension evaluators → User opens datalink dialog
Data Retrieval
VenziaDatalinkApiService.loadDataLink() calls /aqua/datalink/v1/list → Backend returns all registered datalinks
Display
DatalinkListComponent renders tabs for each datalink → Shows linked data from node properties
Search and Link Flow
User Action
User clicks “Add Datalink” button in dialog
Search Dialog
VenziaDatalinkSearchDialogService opens search dialog with selected datalink configuration
External API Call
VenziaDatalinkRestService.callApi() calls the configured REST endpoint with authentication
User Selection
User selects records from search results
Property Update
Selected data is serialized to JSON → NodesApiService.updateNode() saves to aspect property
Reload
Dialog reloads to display newly linked data
Content Model
Datalinks use Alfresco aspects to store linked data:
<model name="dlnk:venzia" xmlns="http://www.alfresco.org/model/dictionary/1.0">
<namespaces>
<namespace uri="http://venzia.es/model/datalink/1.0" prefix="dlnk"/>
</namespaces>
<aspects>
<aspect name="dlnk:employee">
<title>Employee</title>
<properties>
<property name="dlnk:employee">
<type>d:text</type>
<index enabled="true">
<atomic>true</atomic>
<stored>false</stored>
<tokenised>false</tokenised>
</index>
</property>
</properties>
</aspect>
</aspects>
</model>
Each datalink requires a corresponding aspect and text property in the content model. The property stores the linked data as JSON.
Configuration Schema
All datalink configurations are validated against this schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["name", "aspectName", "aspectPropertyName", "connectorRest", "order"],
"properties": {
"name": { "type": "string" },
"aspectName": { "type": "string" },
"aspectPropertyName": { "type": "string" },
"order": { "type": "integer" },
"connectorRest": {
"type": "object",
"properties": {
"url": { "type": "string" },
"authentication": {
"type": "object",
"properties": {
"type": { "enum": ["none", "basic"] },
"username": { "type": "string" },
"password": { "type": "string" }
}
},
"searchParam": { "type": "string" }
}
},
"columns": {
"type": "array",
"items": {
"required": ["primaryKey", "name", "type"],
"properties": {
"primaryKey": { "type": "boolean" },
"name": { "type": "string" },
"label": { "type": "string" },
"type": { "type": "string" },
"hidden": { "type": "boolean" }
}
}
}
}
}
Key Design Patterns
Dependency Injection
All backend services use Spring dependency injection for loose coupling and testability.
Observable Pattern
The frontend uses RxJS observables throughout for reactive data flow and asynchronous operations.
Component Communication
Parent-child component communication uses @Input() and @Output() decorators with EventEmitters.
State Management
NgRx provides centralized state management with actions, effects, and reducers.
The architecture is designed for extensibility. New datalink types can be added by creating JSON configuration files without modifying code.