initial checkin

This commit is contained in:
Brian Long 2024-08-26 10:35:28 -04:00
commit 59147928d0
14 changed files with 748 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Maven
target
pom.xml.versionsBackup
# Eclipse
.project
.classpath
.settings
.vscode
# IDEA
/.idea/

51
README.md Normal file
View File

@ -0,0 +1,51 @@
# Inteligr8 ACS Attribute Cleaner Platform Module Library
This is an Alfresco Content Services platform module that provides attribute service tools at the bootstrap and runtime of ACS.
# Features
You can enable this module by installing it and explicitly setting the following property:
```ini
inteligr8.attrcleaner.enabled=true
```
You may also change the log level of any output from this module using the following property (default is `info`):
```ini
inteligr8.attrcleaner.log-level=info
```
## Query
At startup, you can output the contents of the ACS attribute service by setting the scope of this feature (default is `jmx`):
```ini
inteligr8.attrcleaner.feature.list.scope=jmx
```
Here are the possible values:
| Scope | Description |
| ---------------- | ----------- |
| `none` | Do not list any attributes from the attribute service. |
| `jmx` | List all JMX attributes (`.PropertyBackedBeans`) from the attribute service. |
| `shard-registry` | List all shard registry attributes (`.SHARD_STATE` and `.SHARD_SUBSCRIPTION`) from the attribute service. |
| `custom` | List all shard registry attributes in the attribute service that match the `inteligr8.attrcleaner.feature.list.keys` value. |
When using `custom`, you can query for certain keys using the following:
```ini
inteligr8.attrcleaner.feature.list.keys=\.SHARD\_STATE,\.SHARD\_SUBSCRIPTION
```
The keys are expected to be **comma delimited** and **regular expression** patterns.
# Clear
At startup, you can clear the contents of the ACS attribute service by setting the scope of this feature (defualt is `none`). See the section on *Query* for details. Everything is the same, except the property names are as follows:
```ini
inteligr8.attrcleaner.feature.clear.scope=
inteligr8.attrcleaner.feature.clear.keys=
```

161
pom.xml Normal file
View File

@ -0,0 +1,161 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.inteligr8.alfresco</groupId>
<artifactId>attribute-cleaner-platform-module</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Attribute Cleaner ACS Platform Module</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.release>11</maven.compiler.release>
<alfresco.sdk.version>5.2.0</alfresco.sdk.version>
<alfresco.platform.version>7.4.2</alfresco.platform.version>
<acs-platform.timeout>180000</acs-platform.timeout>
<cxf.version>3.5.5</cxf.version>
<jackson.version>2.15.0-rc1</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>acs-packaging</artifactId>
<version>${alfresco.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- alfresco-repository makes 'runtime' scope, but need it to compile -->
<dependency>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Needed by this module, but provided by ACS -->
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-enterprise-repository</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-elasticsearch-shared</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-repository</artifactId>
<scope>provided</scope>
<exclusions>
<!-- JDK 9+ Eclipse build issue -->
<exclusion>
<groupId>xpp3</groupId>
<artifactId>xpp3</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>4.0.4</version>
<scope>provided</scope>
</dependency>
<!-- Included by pdfbox/aws; already provided by ACS -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<scope>provided</scope>
</dependency>
<!-- Including for testing purposes only -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.36</version>
<extensions>true</extensions>
<configuration>
<tiles>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-rad-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.0.0,2.0.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-search-rad-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-search-rad-tile:[1.0.1,2.0.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-aps-rad-tile -->
<!-- Not much point to this without the bootstrapped processes and task implementations
<tile>com.inteligr8.ootbee:beedk-aps-rad-tile:[1.0.0,2.0.0)</tile>
-->
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-module-tile -->
<tile>com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.0.0,2.0.0)</tile>
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-acs-platform-self-it-tile
<tile>com.inteligr8.ootbee:beedk-acs-platform-self-it-tile:[1.0.0,2.0.0)</tile> -->
</tiles>
</configuration>
</plugin>
<!-- avoids log4j dependency -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<!-- avoids struts dependency -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<!-- Force use of a new maven-dependency-plugin that doesn't download struts dependency -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.7.1</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>windows-extended-timeout</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<properties>
<acs-share.timeout>1200000</acs-share.timeout>
<acs-platform.timeout>2400000</acs-platform.timeout>
</properties>
</profile>
</profiles>
<repositories>
<repository>
<id>alfresco-private</id>
<url>https://artifacts.alfresco.com/nexus/content/groups/private</url>
</repository>
</repositories>
</project>

74
rad.ps1 Normal file
View File

@ -0,0 +1,74 @@
function discoverArtifactId {
$script:ARTIFACT_ID=(mvn -q -Dexpression=project"."artifactId -DforceStdout help:evaluate)
}
function rebuild {
echo "Rebuilding project ..."
mvn process-classes
}
function start_ {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad process-classes
}
function start_log {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad "-Ddocker.showLogs" process-classes
}
function stop_ {
discoverArtifactId
echo "Stopping Docker containers that supported rapid application development ..."
docker container ls --filter name=${ARTIFACT_ID}-*
echo "Stopping containers ..."
docker container stop (docker container ls -q --filter name=${ARTIFACT_ID}-*)
echo "Removing containers ..."
docker container rm (docker container ls -aq --filter name=${ARTIFACT_ID}-*)
}
function tail_logs {
param (
$container
)
discoverArtifactId
docker container logs -f (docker container ls -q --filter name=${ARTIFACT_ID}-${container})
}
function list {
discoverArtifactId
docker container ls --filter name=${ARTIFACT_ID}-*
}
switch ($args[0]) {
"start" {
start_
}
"start_log" {
start_log
}
"stop" {
stop_
}
"restart" {
stop_
start_
}
"rebuild" {
rebuild
}
"tail" {
tail_logs $args[1]
}
"containers" {
list
}
default {
echo "Usage: .\rad.ps1 [ start | start_log | stop | restart | rebuild | tail {container} | containers ]"
}
}
echo "Completed!"

71
rad.sh Normal file
View File

@ -0,0 +1,71 @@
#!/bin/sh
discoverArtifactId() {
ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate`
}
rebuild() {
echo "Rebuilding project ..."
mvn process-classes
}
start() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad process-classes
}
start_log() {
echo "Rebuilding project and starting Docker containers to support rapid application development ..."
mvn -Drad -Ddocker.showLogs process-classes
}
stop() {
discoverArtifactId
echo "Stopping Docker containers that supported rapid application development ..."
docker container ls --filter name=${ARTIFACT_ID}-*
echo "Stopping containers ..."
docker container stop `docker container ls -q --filter name=${ARTIFACT_ID}-*`
echo "Removing containers ..."
docker container rm `docker container ls -aq --filter name=${ARTIFACT_ID}-*`
}
tail_logs() {
discoverArtifactId
docker container logs -f `docker container ls -q --filter name=${ARTIFACT_ID}-$1`
}
list() {
discoverArtifactId
docker container ls --filter name=${ARTIFACT_ID}-*
}
case "$1" in
start)
start
;;
start_log)
start_log
;;
stop)
stop
;;
restart)
stop
start
;;
rebuild)
rebuild
;;
tail)
tail_logs $2
;;
containers)
list
;;
*)
echo "Usage: ./rad.sh [ start | start_log | stop | restart | rebuild | tail {container} | containers ]"
exit 1
esac
echo "Completed!"

View File

@ -0,0 +1,220 @@
package com.inteligr8.alfresco.attrclean;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.attributes.AttributeService.AttributeQueryCallback;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
public abstract class AbstractBootstrapService extends AbstractLifecycleBean implements BootstrapService {
public enum Scope {
None,
JMX,
ShardRegistry,
Custom;
static Scope caseInsensitiveValueOf(String value) {
for (Scope scope : Scope.values())
if (scope.toString().equalsIgnoreCase(value))
return scope;
return null;
}
}
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Pattern rootKeyPattern = Pattern.compile("(.+)/.*");
@Autowired
@Qualifier("attributeService")
private AttributeService attributeService;
@Value("${inteligr8.attrcleaner.enabled}")
private boolean enabled;
@Value("${inteligr8.attrcleaner.log-level}")
private String logLevelRaw;
protected Level logLevel;
protected Scope scope;
protected Map<String, List<Pattern>> keyPatterns;
protected abstract String getRawScope();
protected abstract String getRawKeys();
@Override
protected void onBootstrap(ApplicationEvent aevent) {
if (!this.enabled) {
this.logger.info("Inteligr8 Attribute Cleaner module is disabled");
return;
}
this.logger.info("Inteligr8 Attribute Cleaner {} bootstrapping", this.getClass().getSimpleName());
if (this.validateAndNormalize())
this.execute();
}
@Override
protected void onShutdown(ApplicationEvent aevent) {
// do nothing
}
@Override
public boolean validateAndNormalize() {
this.logLevel = Level.valueOf(this.logLevelRaw.toUpperCase());
PropertyCheck.mandatory(this, "inteligr8.attrcleaner.log-level", this.logLevel);
this.scope = Scope.caseInsensitiveValueOf(this.getRawScope().replace("-", ""));
this.logger.trace("Attribute cleaner {} scope is {}", this.getClass().getSimpleName(), this.scope);
if (this.scope == null)
throw new IllegalArgumentException();
if (Scope.None.equals(this.scope)) {
this.logger.debug("Attribute cleaner {} feature is off", this.getClass().getSimpleName());
return false;
}
this.keyPatterns = new LinkedHashMap<>();
for (String keyRaw : this.getRawKeys().split(",")) {
this.logger.trace("Attribute cleaner {} key: {}", this.getClass().getSimpleName(), keyRaw);
if (keyRaw.length() == 0) {
this.logger.debug("Skipping empty key");
continue;
}
Matcher matcher = this.rootKeyPattern.matcher(keyRaw);
if (!matcher.find()) {
this.logger.warn("Key must have a root element; skipping: {}", keyRaw);
continue;
}
String rootKey = matcher.group(1).replace("\\.", ".");
Pattern keyPattern = Pattern.compile(keyRaw);
this.logger.debug("Validated key pattern: {} => {}", rootKey, keyPattern);
this.putAddToList(this.keyPatterns, rootKey, keyPattern);
}
return true;
}
@Override
public void execute() {
Map<Serializable[], Serializable> attributes = null;
switch (this.scope) {
case None:
throw new IllegalArgumentException();
case JMX:
attributes = this.queryJmx();
break;
case ShardRegistry:
attributes = this.queryShardRegistry();
break;
case Custom:
attributes = this.queryCustom();
break;
default:
throw new IllegalArgumentException();
}
this.logger.atLevel(this.logLevel).log("Queried {} Attributes: ", attributes.size());
for (Entry<Serializable[], Serializable> entry : attributes.entrySet()) {
this.execute(entry);
}
}
public abstract void execute(Entry<Serializable[], Serializable> entry);
private Map<Serializable[], Serializable> queryJmx() {
return this.queryAttrs(".PropertyBackedBeans");
}
private Map<Serializable[], Serializable> queryShardRegistry() {
Map<Serializable[], Serializable> attributes = new LinkedHashMap<>();
attributes.putAll(this.queryAttrs(".SHARD_STATE"));
attributes.putAll(this.queryAttrs(".SHARD_SUBSCRIPTION"));
return attributes;
}
private Map<Serializable[], Serializable> queryCustom() {
Map<Serializable[], Serializable> attributes = new LinkedHashMap<>();
AttributeQueryCallback aqc = new AttributeQueryCallback() {
@Override
public boolean handleAttribute(Long attrId, Serializable value, Serializable[] keys) {
String keysAsStr = StringUtils.stripEnd(StringUtils.join(keys, "/"), "/");
for (Entry<String, List<Pattern>> patterns : keyPatterns.entrySet()) {
for (Pattern pattern : patterns.getValue()) {
if (pattern.matcher(keysAsStr).matches()) {
logger.debug("{} matches attribute: {}", pattern, keysAsStr);
attributes.put(keys, value);
} else {
logger.trace("{} does not match attribute: {}", pattern, keysAsStr);
}
}
}
return true;
}
};
for (String rootKey : this.keyPatterns.keySet()) {
this.logger.debug("Querying for attributes with root key: {}", rootKey);
this.attributeService.getAttributes(aqc, rootKey);
}
return attributes;
}
private Map<Serializable[], Serializable> queryAttrs(Serializable... selectKeys) {
Map<Serializable[], Serializable> attributes = new LinkedHashMap<>();
AttributeQueryCallback aqc = new AttributeQueryCallback() {
@Override
public boolean handleAttribute(Long attrId, Serializable value, Serializable[] keys) {
logger.trace("Found attribute: {}", Arrays.toString(keys));
attributes.put(keys, value);
return true;
}
};
this.logger.debug("Querying for attributes with keys: {}", Arrays.toString(selectKeys));
this.attributeService.getAttributes(aqc, selectKeys);
return attributes;
}
@SuppressWarnings("unchecked")
private <K, CV extends Collection<V>, V> void putAddToList(Map<K, CV> map, K key, V value) {
CV c = map.get(key);
if (c == null) {
c = (CV) new LinkedList<V>();
map.put(key, c);
}
c.add(value);
}
}

View File

@ -0,0 +1,9 @@
package com.inteligr8.alfresco.attrclean;
public interface BootstrapService {
boolean validateAndNormalize();
void execute();
}

View File

@ -0,0 +1,50 @@
package com.inteligr8.alfresco.attrclean;
import java.io.Serializable;
import java.util.Map.Entry;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(value = -10) // higher than average
public class ClearBootstrapService extends AbstractBootstrapService {
private final Logger logger = LoggerFactory.getLogger(ClearBootstrapService.class);
@Autowired
@Qualifier("attributeService")
private AttributeService attributeService;
@Value("${inteligr8.attrcleaner.feature.clear.scope}")
private String scopeRaw;
@Value("${inteligr8.attrcleaner.feature.clear.keys}")
private String keysRaw;
@Override
public String getRawScope() {
return this.scopeRaw;
}
@Override
public String getRawKeys() {
return this.keysRaw;
}
@Override
public void execute(Entry<Serializable[], Serializable> entry) {
String keysAsStr = StringUtils.join(entry.getKey(), "/");
this.logger.debug(" Removing: {}", keysAsStr);
this.attributeService.removeAttribute(entry.getKey());
this.logger.warn(" Removed: {}", keysAsStr);
}
}

View File

@ -0,0 +1,46 @@
package com.inteligr8.alfresco.attrclean;
import java.io.Serializable;
import java.util.Map.Entry;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(value = -100) // higher than average
public class ListBootstrapService extends AbstractBootstrapService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
@Qualifier("attributeService")
private AttributeService attributeService;
@Value("${inteligr8.attrcleaner.feature.list.scope}")
private String scopeRaw;
@Value("${inteligr8.attrcleaner.feature.list.keys}")
private String keysRaw;
@Override
public String getRawScope() {
return this.scopeRaw;
}
@Override
public String getRawKeys() {
return this.keysRaw;
}
@Override
public void execute(Entry<Serializable[], Serializable> entry) {
this.logger.atLevel(this.logLevel).log(" {}: {}", entry.getKey(), entry.getValue());
}
}

View File

@ -0,0 +1,15 @@
inteligr8.attrcleaner.enabled=false
inteligr8.attrcleaner.log-level=info
# list attributes: `none` | `jmx` | `shard-registry` | `custom`
inteligr8.attrcleaner.feature.list.scope=jmx
# when `custom`, what attributes to list
# supports regex; e.g.: \.PropertyBackedBeans/.*
inteligr8.attrcleaner.feature.list.keys=
# clear attributes: `none` | `jmx` | `shard-registry` | `custom`
inteligr8.attrcleaner.feature.clear.scope=none
# when `custom`, what attributes to clear
# supports regex; e.g.: \.PropertyBackedBeans/.*
inteligr8.attrcleaner.feature.clear.keys=

View File

@ -0,0 +1,3 @@
logger.inteligr8-jmx.name=com.inteligr8.alfresco.jmx
logger.inteligr8-jmx.level=INFO

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Use this file for beans to be loaded in whatever order Alfresco/Spring decides -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- Enable Spring annotation scanning for classes in package -->
<context:component-scan base-package="com.inteligr8.alfresco.jmx"
name-generator="org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator">
<context:include-filter type="assignable" expression="com.inteligr8.alfresco.jmx.BootstrapService" />
</context:component-scan>
</beans>

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Use this file for beans to be loaded in whatever order Alfresco/Spring decides -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- Enable Spring annotation scanning for classes aps-public-rest-api -->
<context:component-scan base-package="com.inteligr8.alfresco.jmx"
name-generator="org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator">
<context:exclude-filter type="assignable" expression="com.inteligr8.alfresco.jmx.BootstrapService" />
</context:component-scan>
</beans>

View File

@ -0,0 +1,4 @@
module.id=${project.groupId}.${project.artifactId}
module.title=${project.name}
module.description=${project.description}
module.version=${project.version}