commit 59147928d04fbe93f5453a8e259047f5cb28a31e Author: Brian Long Date: Mon Aug 26 10:35:28 2024 -0400 initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e59065e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Maven +target +pom.xml.versionsBackup + +# Eclipse +.project +.classpath +.settings +.vscode + +# IDEA +/.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..77cbcac --- /dev/null +++ b/README.md @@ -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= +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..70d953b --- /dev/null +++ b/pom.xml @@ -0,0 +1,161 @@ + + 4.0.0 + + com.inteligr8.alfresco + attribute-cleaner-platform-module + 1.0-SNAPSHOT + jar + + Attribute Cleaner ACS Platform Module + + + UTF-8 + 11 + 11 + 11 + + 5.2.0 + 7.4.2 + 180000 + 3.5.5 + 2.15.0-rc1 + + + + + + org.alfresco + acs-packaging + ${alfresco.platform.version} + pom + import + + + + + com.fasterxml.woodstox + woodstox-core + provided + + + + + + + + org.alfresco + alfresco-enterprise-repository + provided + + + org.alfresco + alfresco-elasticsearch-shared + + + + + org.alfresco + alfresco-repository + provided + + + + xpp3 + xpp3 + + + + + jakarta.servlet + jakarta.servlet-api + 4.0.4 + provided + + + + + commons-logging + commons-logging + provided + + + + + junit + junit + test + + + org.mockito + mockito-core + 4.11.0 + test + + + + + + + io.repaint.maven + tiles-maven-plugin + 2.36 + true + + + + com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.0.0,2.0.0) + + com.inteligr8.ootbee:beedk-acs-search-rad-tile:[1.0.1,2.0.0) + + + + com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.0.0,2.0.0) + + + + + + + maven-compiler-plugin + 3.13.0 + + + + maven-site-plugin + 3.12.1 + + + + maven-dependency-plugin + 3.7.1 + + + + + + + windows-extended-timeout + + + windows + + + + 1200000 + 2400000 + + + + + + + alfresco-private + https://artifacts.alfresco.com/nexus/content/groups/private + + + diff --git a/rad.ps1 b/rad.ps1 new file mode 100644 index 0000000..61bcb2f --- /dev/null +++ b/rad.ps1 @@ -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!" + diff --git a/rad.sh b/rad.sh new file mode 100644 index 0000000..7cb0a80 --- /dev/null +++ b/rad.sh @@ -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!" + diff --git a/src/main/java/com/inteligr8/alfresco/attrclean/AbstractBootstrapService.java b/src/main/java/com/inteligr8/alfresco/attrclean/AbstractBootstrapService.java new file mode 100644 index 0000000..1c30511 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/attrclean/AbstractBootstrapService.java @@ -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> 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 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 entry : attributes.entrySet()) { + this.execute(entry); + } + } + + public abstract void execute(Entry entry); + + private Map queryJmx() { + return this.queryAttrs(".PropertyBackedBeans"); + } + + private Map queryShardRegistry() { + Map attributes = new LinkedHashMap<>(); + attributes.putAll(this.queryAttrs(".SHARD_STATE")); + attributes.putAll(this.queryAttrs(".SHARD_SUBSCRIPTION")); + return attributes; + } + + private Map queryCustom() { + Map 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> 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 queryAttrs(Serializable... selectKeys) { + Map 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 , V> void putAddToList(Map map, K key, V value) { + CV c = map.get(key); + if (c == null) { + c = (CV) new LinkedList(); + map.put(key, c); + } + c.add(value); + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/attrclean/BootstrapService.java b/src/main/java/com/inteligr8/alfresco/attrclean/BootstrapService.java new file mode 100644 index 0000000..837097a --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/attrclean/BootstrapService.java @@ -0,0 +1,9 @@ +package com.inteligr8.alfresco.attrclean; + +public interface BootstrapService { + + boolean validateAndNormalize(); + + void execute(); + +} diff --git a/src/main/java/com/inteligr8/alfresco/attrclean/ClearBootstrapService.java b/src/main/java/com/inteligr8/alfresco/attrclean/ClearBootstrapService.java new file mode 100644 index 0000000..775b712 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/attrclean/ClearBootstrapService.java @@ -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 entry) { + String keysAsStr = StringUtils.join(entry.getKey(), "/"); + this.logger.debug(" Removing: {}", keysAsStr); + this.attributeService.removeAttribute(entry.getKey()); + this.logger.warn(" Removed: {}", keysAsStr); + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/attrclean/ListBootstrapService.java b/src/main/java/com/inteligr8/alfresco/attrclean/ListBootstrapService.java new file mode 100644 index 0000000..fc0e594 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/attrclean/ListBootstrapService.java @@ -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 entry) { + this.logger.atLevel(this.logLevel).log(" {}: {}", entry.getKey(), entry.getValue()); + } + +} diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/alfresco-global.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/alfresco-global.properties new file mode 100644 index 0000000..2c08c3a --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/alfresco-global.properties @@ -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= diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/log4j2.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/log4j2.properties new file mode 100644 index 0000000..49b760d --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/log4j2.properties @@ -0,0 +1,3 @@ + +logger.inteligr8-jmx.name=com.inteligr8.alfresco.jmx +logger.inteligr8-jmx.level=INFO diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-bootstrap-context.xml b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-bootstrap-context.xml new file mode 100644 index 0000000..7564ab6 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-bootstrap-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-context.xml b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-context.xml new file mode 100644 index 0000000..e3bd95a --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module.properties new file mode 100644 index 0000000..d6d5838 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.attribute-cleaner-platform-module/module.properties @@ -0,0 +1,4 @@ +module.id=${project.groupId}.${project.artifactId} +module.title=${project.name} +module.description=${project.description} +module.version=${project.version}