Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLING-11744 : Allow configuration of include and exclude paths for bu… #9

Merged
merged 3 commits into from Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/main/java/org/apache/sling/i18n/impl/Config.java
@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.i18n.impl;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name ="Apache Sling I18N Resource Bundle Provider",
description ="ResourceBundleProvider service which loads the messages from the repository.")
public @interface Config {

@AttributeDefinition(name = "Default Locale",
description = "The default locale to assume if none can be "+
"resolved otherwise. This value must be in the form acceptable to the "+
"java.util.Locale class.")
String locale_default() default "en";

@AttributeDefinition(name = "Preload Bundles",
description = "Whether or not to eagerly load the resource bundles "+
"on bundle start or a cache invalidation.")
boolean preload_bundles() default false;

@AttributeDefinition(name = "Invalidation Delay",
description = "In case of dictionary change events the cached "+
"resource bundle becomes invalid after the given delay (in ms). ")
long invalidation_delay() default 5000;

@AttributeDefinition(name="Included paths",
description="Translations in paths starting with one of these values will be ignored")
jsedding marked this conversation as resolved.
Show resolved Hide resolved
String[] included_paths() default {"/libs", "/apps"};

@AttributeDefinition(name="Excluded paths",
description="Translations in paths starting with one of these values will be ignored")
String[] excluded_paths() default {"/var/eventing"};
}
30 changes: 20 additions & 10 deletions src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
Expand Up @@ -64,6 +64,8 @@ public class JcrResourceBundle extends ResourceBundle {

static final String PROP_MIXINS = "jcr:mixinTypes";

static final String PROP_PATH = "jcr:path";

static final String QUERY_LANGUAGE_ROOTS = "//element(*,mix:language)[@jcr:language]";

private final Map<String, Object> resources;
Expand All @@ -75,14 +77,16 @@ public class JcrResourceBundle extends ResourceBundle {
private final Set<String> languageRoots = new HashSet<>();

JcrResourceBundle(final Locale locale, final String baseName,
final ResourceResolver resourceResolver, List<LocatorPaths> locatorPaths) {
final ResourceResolver resourceResolver,
final List<LocatorPaths> locatorPaths,
final PathFilter filter) {
this.locale = locale;
this.baseName = baseName;

log.info("Finding all dictionaries for '{}' (basename: {}) ...", locale, baseName == null ? "<none>" : baseName);

final long start = System.currentTimeMillis();
final Set<String> roots = loadPotentialLanguageRoots(resourceResolver, locale, baseName, locatorPaths);
final Set<String> roots = loadPotentialLanguageRoots(resourceResolver, locale, baseName, locatorPaths, filter);
this.resources = loadFully(resourceResolver, roots, this.languageRoots);

if (log.isInfoEnabled()) {
Expand Down Expand Up @@ -318,27 +322,33 @@ private void loadSlingMessageDictionary(final Resource dictionaryResource, final
this.scanForSlingMessages(dictionaryResource, targetDictionary);
}

private Set<String> loadPotentialLanguageRoots(ResourceResolver resourceResolver, Locale locale, final String baseName, Collection<LocatorPaths> locatorPaths) {
private Set<String> loadPotentialLanguageRoots(final ResourceResolver resourceResolver,
final Locale locale,
final String baseName,
final Collection<LocatorPaths> locatorPaths,
final PathFilter filter) {
final Set<String> paths = new LinkedHashSet<>();

PotentialLanguageRootCheck check = new PotentialLanguageRootCheck(baseName, locale);

// first consider resource bundles in the JCR repository
final Iterator<Resource> bundles = resourceResolver.findResources(QUERY_LANGUAGE_ROOTS, "xpath");
while (bundles.hasNext()) {
Resource bundle = bundles.next();
if (check.isResourceBundle(bundle)) {
final Resource bundle = bundles.next();
if (filter.includePath(bundle.getPath()) && check.isResourceBundle(bundle)) {
paths.add(bundle.getPath());
}
}

if (locatorPaths != null && !locatorPaths.isEmpty()) {
// next traverse the ancestors of all of the locator paths
LocatorPathsVisitor visitor = new LocatorPathsVisitor(check, paths);
for (LocatorPaths locator : locatorPaths) {
Resource parentResource = resourceResolver.getResource(locator.getPath());
if (parentResource != null) {
visitor.accept(parentResource, locator.getTraverseDepth());
final LocatorPathsVisitor visitor = new LocatorPathsVisitor(check, paths);
for (final LocatorPaths locator : locatorPaths) {
if ( filter.includePath(locator.getPath())) {
final Resource parentResource = resourceResolver.getResource(locator.getPath());
if (parentResource != null) {
visitor.accept(parentResource, locator.getTraverseDepth());
}
}
}
}
Expand Down
Expand Up @@ -20,6 +20,7 @@

import static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_BASENAME;
import static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_LANGUAGE;
import static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_PATH;

import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -61,9 +62,7 @@
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.util.tracker.BundleTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -83,49 +82,24 @@
ResourceChangeListener.CHANGES + "=REMOVED",
ResourceChangeListener.CHANGES + "=CHANGED"
})
@Designate(ocd = JcrResourceBundleProvider.Config.class)
@Designate(ocd = Config.class)
public class JcrResourceBundleProvider implements ResourceBundleProvider, ResourceChangeListener, ExternalResourceChangeListener {

/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());

/**
* A regular expression pattern matching all custom country codes.
* @see <a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#User-assigned_code_elements">User-assigned code elements</a>
*/
private static final Pattern USER_ASSIGNED_COUNTRY_CODES_PATTERN = Pattern.compile("aa|q[m-z]|x[a-z]|zz");

@ObjectClassDefinition(name ="Apache Sling I18N Resource Bundle Provider",
description ="ResourceBundleProvider service which loads the messages from the repository.")
public @interface Config {

@AttributeDefinition(name = "Default Locale",
description = "The default locale to assume if none can be "+
"resolved otherwise. This value must be in the form acceptable to the "+
"java.util.Locale class.")
String locale_default() default "en";

@AttributeDefinition(name = "Preload Bundles",
description = "Whether or not to eagerly load the resource bundles "+
"on bundle start or a cache invalidation.")
boolean preload_bundles() default false;

@AttributeDefinition(name = "Invalidation Delay",
description = "In case of dictionary change events the cached "+
"resource bundle becomes invalid after the given delay (in ms). ")
long invalidation_delay() default 5000;

@AttributeDefinition(name="Excluded paths",
description="Events happening in paths starting with one of these values will be ignored")
String[] excluded_paths() default {"/var/eventing"};
}

@Reference
private Scheduler scheduler;

/** job names of scheduled jobs for reloading individual bundles */
private final Collection<String> scheduledJobNames = Collections.synchronizedList(new ArrayList<String>()) ;

/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());

@Reference
private ResourceResolverFactory resourceResolverFactory;

Expand Down Expand Up @@ -167,8 +141,15 @@ public class JcrResourceBundleProvider implements ResourceBundleProvider, Resour

private BundleTracker<Set<LocatorPaths>> locatorPathsTracker;
private List<LocatorPaths> locatorPaths = new CopyOnWriteArrayList<>();

Config config;

/**
* Filter to check for allowed paths
*/
private volatile PathFilter pathFilter;

private volatile boolean preloadBundles;

private volatile long invalidationDelay;

/**
* Add a set of paths to the set that are inspected to
Expand Down Expand Up @@ -243,7 +224,7 @@ public void onChange(final List<ResourceChange> changes) {
try {
for (final ResourceChange change : changes) {

if (canIgnoreChange(change)) {
if (!this.pathFilter.includePath(change.getPath())) {
continue;
}
this.onChange(status, change);
Expand All @@ -268,17 +249,6 @@ public void onChange(final List<ResourceChange> changes) {
}
}

// skip if the change happens within a path configured in excludedPaths
protected boolean canIgnoreChange(final ResourceChange change) {
for (String excludedPath: this.config.excluded_paths()) {
if (change.getPath().startsWith(excludedPath)) {
return true;
}
}
return false;
}


private void onChange(final ChangeStatus status, final ResourceChange change)
throws LoginException {
log.debug("onChange: Detecting change {} for path '{}'", change.getType(), change.getPath());
Expand Down Expand Up @@ -372,7 +342,7 @@ private void scheduleReloadBundles(final boolean withDelay) {
// defer this job
final ScheduleOptions options;
if (withDelay) {
options = scheduler.AT(new Date(System.currentTimeMillis() + this.config.invalidation_delay()));
options = scheduler.AT(new Date(System.currentTimeMillis() + this.invalidationDelay));
} else {
options = scheduler.NOW();
}
Expand All @@ -391,7 +361,7 @@ private void scheduleReloadBundle(final JcrResourceBundle bundle) {
final Key key = new Key(bundle.getBaseName(), bundle.getLocale());

// defer this job
ScheduleOptions options = scheduler.AT(new Date(System.currentTimeMillis() + this.config.invalidation_delay()));
ScheduleOptions options = scheduler.AT(new Date(System.currentTimeMillis() + this.invalidationDelay));
final String jobName = "ResourceBundleProvider: reload bundle with key " + key.toString();
scheduledJobNames.add(jobName);
options.name(jobName);
Expand All @@ -406,7 +376,7 @@ public void run() {

void reloadBundle(final Key key) {
log.info("Reloading resource bundle for {}", key);
if (!this.config.preload_bundles()) {
if (!this.preloadBundles) {
// remove bundle from cache
resourceBundleCache.remove(key);
// unregister bundle
Expand All @@ -429,7 +399,7 @@ void reloadBundle(final Key key) {
reloadBundle(new Key(dependentBundle.getBaseName(), dependentBundle.getLocale()));
}

if (this.config.preload_bundles()) {
if (this.preloadBundles) {
// reload the bundle from the repository (will also fill cache and register as a service)
getResourceBundleInternal(null, key.baseName, key.locale, true);
}
Expand All @@ -444,27 +414,26 @@ void reloadBundle(final Key key) {
*/
@Activate
protected void activate(final BundleContext context, final Config config) throws LoginException {

this.config = config;
final String localeString = config.locale_default();
this.defaultLocale = toLocale(localeString);
this.defaultLocale = toLocale(config.locale_default());
this.preloadBundles = config.preload_bundles();
this.invalidationDelay = config.invalidation_delay();
this.pathFilter = new PathFilter(config.included_paths(), config.excluded_paths());
this.bundleContext = context;

locatorPathsTracker = new BundleTracker<>(this.bundleContext,
this.locatorPathsTracker = new BundleTracker<>(this.bundleContext,
Bundle.ACTIVE, new LocatorPathsTracker(this));
locatorPathsTracker.open();
this.locatorPathsTracker.open();

if (this.resourceResolverFactory != null) { // this is only null during test execution!
scheduleReloadBundles(false);
}

}

@Deactivate
protected void deactivate() {
if (locatorPathsTracker != null) {
locatorPathsTracker.close();
locatorPathsTracker = null;
if (this.locatorPathsTracker != null) {
this.locatorPathsTracker.close();
this.locatorPathsTracker = null;
}

clearCache();
Expand Down Expand Up @@ -581,7 +550,7 @@ private void registerResourceBundle(Key key, JcrResourceBundle resourceBundle) {
* is not available to access the resources.
*/
private JcrResourceBundle createResourceBundle(final ResourceResolver resolver, final String baseName, final Locale locale) {
final JcrResourceBundle bundle = new JcrResourceBundle(locale, baseName, resolver, locatorPaths);
final JcrResourceBundle bundle = new JcrResourceBundle(locale, baseName, resolver, locatorPaths, this.pathFilter);

// set parent resource bundle
Locale parentLocale = getParentLocale(locale);
Expand Down Expand Up @@ -655,14 +624,14 @@ private void clearCache() {
}

private void preloadBundles() {
if (this.config.preload_bundles()) {
if (this.preloadBundles) {
try ( final ResourceResolver resolver = createResourceResolver() ) {
final Iterator<Map<String, Object>> bundles = resolver.queryResources(
JcrResourceBundle.QUERY_LANGUAGE_ROOTS, "xpath");
final Set<Key> usedKeys = new HashSet<>();
while (bundles.hasNext()) {
final Map<String,Object> bundle = bundles.next();
if (bundle.containsKey(PROP_LANGUAGE)) {
if (bundle.containsKey(PROP_LANGUAGE) && this.pathFilter.includePath(bundle.get(PROP_PATH).toString())) {
final Locale locale = toLocale(bundle.get(PROP_LANGUAGE).toString());
String baseName = null;
if (bundle.containsKey(PROP_BASENAME)) {
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/org/apache/sling/i18n/impl/PathFilter.java
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.i18n.impl;

import org.apache.sling.api.resource.path.PathSet;

public class PathFilter {

/**
* Included paths
*/
private final PathSet includedPaths;

/**
* Excluded paths
*/
private final PathSet excludedPaths;

public PathFilter() {
this(null, null);
}

public PathFilter(final String[] includes, final String[] excludes) {
this.includedPaths = includes == null ? null : PathSet.fromStrings(includes);
this.excludedPaths = excludes == null ? null : PathSet.fromStrings(excludes);
}

/**
* Check whether the path is in the included but not in the excluded paths
* @param path The path to check
* @return {@code true} if the path can be included
*/
public boolean includePath(final String path) {
boolean included = this.includedPaths == null || this.includedPaths.matches(path) != null;
if (included) {
included = this.excludedPaths == null || this.excludedPaths.matches(path) == null;
}
return included;
}
}