SLING-10135 Add the ability to locate non JCR based ResourceBundle resources (#5)
SLING-10135 Add the ability to locate non JCR based ResourceBundle resources
diff --git a/bnd.bnd b/bnd.bnd
index 24986a0..221feb5 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -4,6 +4,8 @@
Require-Capability:\
osgi.implementation;filter:="(&(osgi.implementation=osgi.http)(version>=1.0)(!(version>=2.0)))"
+Provide-Capability: osgi.extender;osgi.extender="org.apache.sling.i18n.resourcebundle.locator.registrar";version:Version="1.0"
+
Sling-Nodetypes:\
SLING-INF/nodetypes/jcrlanguage.cnd,\
SLING-INF/nodetypes/message.cnd
diff --git a/pom.xml b/pom.xml
index acc467d..166f6a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.sling</groupId>
<artifactId>sling-bundle-parent</artifactId>
- <version>38</version>
+ <version>40</version>
<relativePath />
</parent>
@@ -35,7 +35,9 @@
<description>Support for creating Java I18N ResourceBundles from repository resources.</description>
<properties>
+ <project.build.outputTimestamp>2020-10-15T22:24:47Z</project.build.outputTimestamp>
<org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+ <jackrabbit.version>2.14.3</jackrabbit.version>
</properties>
<scm>
@@ -48,6 +50,15 @@
<build>
<plugins>
<plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>**/*.json.props</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ <plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
@@ -148,7 +159,7 @@
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-jcr-commons</artifactId>
- <version>2.2.4</version>
+ <version>${jackrabbit.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -201,25 +212,42 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
- <version>1.10.19</version>
+ <version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
- <version>1.6.4</version>
+ <version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.powermock</groupId>
- <artifactId>powermock-api-mockito</artifactId>
- <version>1.6.4</version>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-api-mockito2</artifactId>
+ <version>2.0.9</version>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- workaround SLING-7159 - the jackrabbit version from o.a.sling.commons.testing is not compatible with java9+.
+ remove this after the deprecated dependency to o.a.sling.commons.testing has been replaced -->
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-api</artifactId>
+ <version>${jackrabbit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <!-- workaround SLING-7159 - the jackrabbit version from o.a.sling.commons.testing is not compatible with java9+.
+ remove this after the deprecated dependency to o.a.sling.commons.testing has been replaced -->
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-core</artifactId>
+ <version>${jackrabbit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.commons.testing</artifactId>
- <version>2.0.14</version>
+ <version>2.1.2</version>
<scope>test</scope>
<exclusions>
<!-- slf4j simple implementation logs INFO + higher to stdout (we don't want that behaviour) -->
@@ -227,6 +255,16 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
+ <!-- workaround SLING-7159 - the jackrabbit version is not compatible with java9+
+ so exclude these here and declare newer version of jackrabbit artifacts above. -->
+ <exclusion>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-core</artifactId>
+ </exclusion>
</exclusions>
</dependency>
<!-- using log4j under slf4j to allow fine-grained logging config (see src/test/resources/log4j.properties) -->
diff --git a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
index a791954..e884e31 100644
--- a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
+++ b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
@@ -21,7 +21,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
@@ -76,13 +76,18 @@
JcrResourceBundle(final Locale locale, final String baseName,
final ResourceResolver resourceResolver) {
+ this(locale, baseName, resourceResolver, Collections.<LocatorPaths>emptyList());
+ }
+
+ JcrResourceBundle(final Locale locale, final String baseName,
+ final ResourceResolver resourceResolver, List<LocatorPaths> locatorPaths) {
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);
+ final Set<String> roots = loadPotentialLanguageRoots(resourceResolver, locale, baseName, locatorPaths);
this.resources = loadFully(resourceResolver, roots, this.languageRoots);
if (log.isInfoEnabled()) {
@@ -318,37 +323,32 @@
this.scanForSlingMessages(dictionaryResource, targetDictionary);
}
- private Set<String> loadPotentialLanguageRoots(ResourceResolver resourceResolver, Locale locale, String baseName) {
- final String localeString = locale.toString();
- final String localeStringLower = localeString.toLowerCase();
- final String localeRFC4646String = toRFC4646String(locale);
- final String localeRFC4646StringLower = localeRFC4646String.toLowerCase();
-
+ private Set<String> loadPotentialLanguageRoots(ResourceResolver resourceResolver, Locale locale, final String baseName, Collection<LocatorPaths> locatorPaths) {
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();
- ValueMap properties = bundle.adaptTo(ValueMap.class);
- String language = properties.get(PROP_LANGUAGE, String.class);
- if (language != null && language.length() > 0) {
- if (language.equals(localeString)
- || language.equals(localeStringLower)
- || language.equals(localeRFC4646String)
- || language.equals(localeRFC4646StringLower)) {
- // basename might be a multivalue (see https://issues.apache.org/jira/browse/SLING-4547)
- String[] baseNames = properties.get(PROP_BASENAME, new String[]{});
- if (baseName == null || Arrays.asList(baseNames).contains(baseName)) {
- paths.add(bundle.getPath());
- }
+ if (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());
}
}
}
- return Collections.unmodifiableSet(paths);
- }
- // Would be nice if Locale.toString() output RFC 4646, but it doesn't
- private static String toRFC4646String(Locale locale) {
- return locale.toString().replace('_', '-');
+ return Collections.unmodifiableSet(paths);
}
@Override
diff --git a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
index ae9b74e..166160f 100644
--- a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
+++ b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
@@ -37,6 +37,7 @@
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Semaphore;
import java.util.regex.Pattern;
@@ -52,6 +53,7 @@
import org.apache.sling.commons.scheduler.Scheduler;
import org.apache.sling.i18n.ResourceBundleProvider;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
+import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
@@ -62,6 +64,7 @@
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;
@@ -162,6 +165,31 @@
private long invalidationDelay;
+ private BundleTracker<Set<LocatorPaths>> locatorPathsTracker;
+ private List<LocatorPaths> locatorPaths = new CopyOnWriteArrayList<>();
+
+ /**
+ * Add a set of paths to the set that are inspected to
+ * look for resource bundle resources
+ *
+ * @param locatorPathsSet set of locator paths to check
+ */
+ public void registerLocatorPaths(Set<LocatorPaths> locatorPathsSet) {
+ this.locatorPaths.addAll(locatorPathsSet);
+ clearCache();
+ }
+
+ /**
+ * Remove a set of paths from the set that are inspected to
+ * look for resource bundle resources
+ *
+ * @param locatorPathsSet set of locator paths to no longer check
+ */
+ public void unregisterLocatorPaths(Set<LocatorPaths> locatorPathsSet) {
+ this.locatorPaths.removeAll(locatorPathsSet);
+ clearCache();
+ }
+
private ResourceResolver createResourceResolver() throws LoginException {
return resourceResolverFactory.getServiceResourceResolver(null);
}
@@ -405,6 +433,11 @@
this.bundleContext = context;
this.invalidationDelay = config.invalidation_delay();
+
+ locatorPathsTracker = new BundleTracker<>(this.bundleContext,
+ Bundle.ACTIVE, new LocatorPathsTracker(this));
+ locatorPathsTracker.open();
+
if (this.resourceResolverFactory != null) { // this is only null during test execution!
scheduleReloadBundles(false);
}
@@ -413,6 +446,11 @@
@Deactivate
protected void deactivate() {
+ if (locatorPathsTracker != null) {
+ locatorPathsTracker.close();
+ locatorPathsTracker = null;
+ }
+
clearCache();
this.bundleContext = null;
}
@@ -527,7 +565,7 @@
* 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);
+ final JcrResourceBundle bundle = new JcrResourceBundle(locale, baseName, resolver, locatorPaths);
// set parent resource bundle
Locale parentLocale = getParentLocale(locale);
diff --git a/src/main/java/org/apache/sling/i18n/impl/LocatorPaths.java b/src/main/java/org/apache/sling/i18n/impl/LocatorPaths.java
new file mode 100644
index 0000000..34f1cfd
--- /dev/null
+++ b/src/main/java/org/apache/sling/i18n/impl/LocatorPaths.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * Details about locator paths for a bundle
+ */
+class LocatorPaths {
+ private final String path;
+ private final int traverseDepth;
+ private final long forBundleId;
+
+ public LocatorPaths(String path, int traverseDepth, long forBundleId) {
+ this.path = path;
+ this.traverseDepth = traverseDepth;
+ this.forBundleId = forBundleId;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public int getTraverseDepth() {
+ return traverseDepth;
+ }
+
+ public long getForBundleId() {
+ return forBundleId;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (forBundleId ^ (forBundleId >>> 32));
+ result = prime * result + ((path == null) ? 0 : path.hashCode());
+ result = prime * result + traverseDepth;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LocatorPaths other = (LocatorPaths) obj;
+ if (forBundleId != other.forBundleId)
+ return false;
+ if (path == null) {
+ if (other.path != null)
+ return false;
+ } else if (!path.equals(other.path))
+ return false;
+ if (traverseDepth != other.traverseDepth)
+ return false;
+ return true;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/i18n/impl/LocatorPathsTracker.java b/src/main/java/org/apache/sling/i18n/impl/LocatorPathsTracker.java
new file mode 100644
index 0000000..55d31be
--- /dev/null
+++ b/src/main/java/org/apache/sling/i18n/impl/LocatorPathsTracker.java
@@ -0,0 +1,105 @@
+/*
+ * 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 java.util.HashSet;
+import java.util.Set;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.commons.osgi.ManifestHeader.Entry;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.Constants;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles watching bundles for registration of ResourceBundle locator paths
+ */
+class LocatorPathsTracker implements BundleTrackerCustomizer<Set<LocatorPaths>> {
+
+ private static final String CAPABILITY_I18N_RESOURCEBUNDLE_LOCATOR = "org.apache.sling.i18n.resourcebundle.locator";
+ private static final String ATTR_PATHS = "paths";
+ private static final String ATTR_DEPTH = "depth";
+
+ private Logger log = LoggerFactory.getLogger(getClass());
+
+ private JcrResourceBundleProvider rbp;
+
+ public LocatorPathsTracker(JcrResourceBundleProvider provider) {
+ this.rbp = provider;
+ }
+
+ @Override
+ public Set<LocatorPaths> addingBundle(Bundle bundle, BundleEvent event) {
+ log.debug("Considering bundle for registering resource bundle locator paths: {}",
+ bundle.getSymbolicName());
+ Set<LocatorPaths> pathsSet = null;
+
+ String provideCapability = bundle.getHeaders().get(Constants.PROVIDE_CAPABILITY);
+ if (provideCapability != null) {
+ ManifestHeader header = ManifestHeader.parse(provideCapability);
+ for (Entry entry : header.getEntries()) {
+ if (CAPABILITY_I18N_RESOURCEBUNDLE_LOCATOR.equals(entry.getValue())) {
+ String paths = entry.getAttributeValue(ATTR_PATHS);
+ if (paths != null) {
+ if (pathsSet == null) {
+ pathsSet = new HashSet<>();
+ }
+
+ // use optional depth value if supplied (or 1 by default)
+ int traversalDepth = 1;
+ String depth = entry.getAttributeValue(ATTR_DEPTH);
+ if (depth != null && !depth.isEmpty()) {
+ traversalDepth = Integer.parseInt(depth);
+ }
+
+ // treat paths value as a csv
+ String[] items = paths.split(",");
+ for (String path : items) {
+ path = path.trim(); // ignore surrounding spaces
+ if (!path.isEmpty()) {
+ pathsSet.add(new LocatorPaths(path, traversalDepth, bundle.getBundleId()));
+ }
+ }
+ }
+ }
+ }
+
+ if (pathsSet != null) {
+ log.info("Registered {} resource bundle locator paths for bundle: {}",
+ pathsSet.size(), bundle.getSymbolicName());
+ this.rbp.registerLocatorPaths(pathsSet);
+ }
+ }
+ return pathsSet;
+ }
+
+ @Override
+ public void modifiedBundle(Bundle bundle, BundleEvent event, Set<LocatorPaths> baseNamesSet) {
+ // no-op
+ }
+
+ @Override
+ public void removedBundle(Bundle bundle, BundleEvent event, Set<LocatorPaths> baseNamesSet) {
+ log.info("Unregistered {} resource bundle locator paths for bundle: {}",
+ baseNamesSet.size(), bundle.getSymbolicName());
+ this.rbp.unregisterLocatorPaths(baseNamesSet);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/i18n/impl/LocatorPathsVisitor.java b/src/main/java/org/apache/sling/i18n/impl/LocatorPathsVisitor.java
new file mode 100644
index 0000000..b919a40
--- /dev/null
+++ b/src/main/java/org/apache/sling/i18n/impl/LocatorPathsVisitor.java
@@ -0,0 +1,78 @@
+/*
+ * 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 java.util.Iterator;
+import java.util.Set;
+
+import org.apache.sling.api.resource.AbstractResourceVisitor;
+import org.apache.sling.api.resource.Resource;
+
+/**
+ * Visitor implementation for traversing the resources under
+ * the locator path
+ */
+class LocatorPathsVisitor extends AbstractResourceVisitor {
+ private final PotentialLanguageRootCheck check;
+ private final Set<String> paths;
+ private int traverseDepth;
+
+ /**
+ * Constructor to prepare visitor
+ *
+ * @param check the callback to check the resource for a match
+ * @param paths the language
+ * @param traverseDepth the maximum depth to traverse the descendant
+ */
+ public LocatorPathsVisitor(PotentialLanguageRootCheck check, Set<String> paths) {
+ this.check = check;
+ this.paths = paths;
+ }
+
+ public void accept(Resource res, int traverseDepth) {
+ this.traverseDepth = traverseDepth;
+ super.accept(res);
+ }
+
+ /**
+ * Override to stop traversal after the specified depth has
+ * been reached
+ */
+ @Override
+ protected void traverseChildren(Iterator<Resource> children) {
+ if (this.traverseDepth > 0) {
+ // decrement before drilling into children
+ this.traverseDepth--;
+ super.traverseChildren(children);
+ // back to original depth
+ this.traverseDepth++;
+ }
+ }
+
+ /**
+ * Check the given resource for a match
+ */
+ @Override
+ protected void visit(Resource res) {
+ if (check.isResourceBundle(res)) {
+ paths.add(res.getPath());
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/i18n/impl/PotentialLanguageRootCheck.java b/src/main/java/org/apache/sling/i18n/impl/PotentialLanguageRootCheck.java
new file mode 100644
index 0000000..3bee10f
--- /dev/null
+++ b/src/main/java/org/apache/sling/i18n/impl/PotentialLanguageRootCheck.java
@@ -0,0 +1,76 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Locale;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+
+/**
+ * Logic to check if a resource is for a resource bundle
+ */
+class PotentialLanguageRootCheck {
+ private final String baseName;
+ private final String localeString;
+ private final String localeStringLower;
+ private final String localeRFC4646String;
+ private final String localeRFC4646StringLower;
+
+ public PotentialLanguageRootCheck(String baseName, Locale locale) {
+ this.baseName = baseName;
+ this.localeString = locale.toString();
+ this.localeStringLower = localeString.toLowerCase();
+ this.localeRFC4646String = toRFC4646String(locale);
+ this.localeRFC4646StringLower = localeRFC4646String.toLowerCase();
+ }
+
+ /**
+ * Checks if the specified resource is a match for a resource bundle resource
+ *
+ * @param resource the resource to check
+ */
+ public boolean isResourceBundle(Resource resource) {
+ boolean match = false;
+ ValueMap properties = resource.adaptTo(ValueMap.class);
+ if (properties != null) {
+ String language = properties.get(JcrResourceBundle.PROP_LANGUAGE, String.class);
+ if (language != null && language.length() > 0) {
+ if (language.equals(localeString)
+ || language.equals(localeStringLower)
+ || language.equals(localeRFC4646String)
+ || language.equals(localeRFC4646StringLower)) {
+ // basename might be a multivalue (see https://issues.apache.org/jira/browse/SLING-4547)
+ String[] baseNames = properties.get(JcrResourceBundle.PROP_BASENAME, new String[]{});
+ if (baseName == null || Arrays.asList(baseNames).contains(baseName)) {
+ match = true;
+ }
+ }
+ }
+ }
+ return match;
+ }
+
+ // Would be nice if Locale.toString() output RFC 4646, but it doesn't
+ private static String toRFC4646String(Locale locale) {
+ return locale.toString().replace('_', '-');
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java b/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
index b99071e..8c8acb4 100644
--- a/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
+++ b/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
@@ -20,9 +20,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.eq;
+import static org.mockito.AdditionalMatchers.or;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.powermock.api.mockito.PowerMockito.doAnswer;
@@ -46,6 +47,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
@@ -56,8 +58,6 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Test case to verify that each bundle is only loaded once, even
@@ -73,8 +73,6 @@
return Arrays.asList(Boolean.TRUE, Boolean.FALSE);
}
- private static final Logger LOG = LoggerFactory.getLogger(ConcurrentJcrResourceBundleLoadingTest.class);
-
@Mock JcrResourceBundle english;
@Mock JcrResourceBundle german;
@@ -108,8 +106,8 @@
}
});
doReturn(null).when(provider, "createResourceResolver");
- doReturn(english).when(provider, "createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH));
- doReturn(german).when(provider, "createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.GERMAN));
+ doReturn(english).when(provider, "createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH));
+ doReturn(german).when(provider, "createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.GERMAN));
Mockito.when(german.getLocale()).thenReturn(Locale.GERMAN);
Mockito.when(english.getLocale()).thenReturn(Locale.ENGLISH);
Mockito.when(german.getParent()).thenReturn(english);
@@ -122,7 +120,7 @@
assertEquals(german, provider.getResourceBundle(Locale.GERMAN));
assertEquals(german, provider.getResourceBundle(Locale.GERMAN));
- verifyPrivate(provider, times(2)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), any(Locale.class));
+ verifyPrivate(provider, times(2)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), any(Locale.class));
}
@Test
@@ -141,8 +139,8 @@
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
- verifyPrivate(provider, times(1)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH));
- verifyPrivate(provider, times(1)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.GERMAN));
+ verifyPrivate(provider, times(1)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH));
+ verifyPrivate(provider, times(1)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.GERMAN));
}
@Test
@@ -159,8 +157,8 @@
provider.getResourceBundle(Locale.ENGLISH);
provider.getResourceBundle(Locale.GERMAN);
- verifyPrivate(provider, times(1)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH));
- verifyPrivate(provider, times(2)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.GERMAN));
+ verifyPrivate(provider, times(1)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH));
+ verifyPrivate(provider, times(2)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.GERMAN));
}
@Test
@@ -177,8 +175,8 @@
provider.getResourceBundle(Locale.ENGLISH);
provider.getResourceBundle(Locale.GERMAN);
- verifyPrivate(provider, times(2)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH));
- verifyPrivate(provider, times(2)).invoke("createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.GERMAN));
+ verifyPrivate(provider, times(2)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH));
+ verifyPrivate(provider, times(2)).invoke("createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.GERMAN));
}
/**
@@ -199,7 +197,7 @@
newBundleReady.set(true);
return newBundle;
}
- }).when(provider, "createResourceBundle", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH));
+ }).when(provider, "createResourceBundle", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH));
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
@Override
@@ -224,6 +222,6 @@
}
verifyPrivate(provider, verificationMode)
- .invoke("getResourceBundleInternal", any(ResourceResolver.class), eq(null), eq(Locale.ENGLISH), anyBoolean());
+ .invoke("getResourceBundleInternal", or(ArgumentMatchers.isNull(), any(ResourceResolver.class)), eq(null), eq(Locale.ENGLISH), anyBoolean());
}
}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/i18n/impl/LocatorPathsTest.java b/src/test/java/org/apache/sling/i18n/impl/LocatorPathsTest.java
new file mode 100644
index 0000000..01d4bbc
--- /dev/null
+++ b/src/test/java/org/apache/sling/i18n/impl/LocatorPathsTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import org.junit.Test;
+
+/**
+ * SLING-10135 Test LocatorPaths methods
+ */
+public class LocatorPathsTest {
+
+ @Test
+ public void testGetPath() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertEquals("/lib/path", paths1.getPath());
+ }
+
+ @Test
+ public void testGetTraverseDepth() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 2);
+
+ assertEquals(1, paths1.getTraverseDepth());
+ }
+
+ @Test
+ public void testGetForBundleId() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 2);
+
+ assertEquals(2, paths1.getForBundleId());
+ }
+
+ @Test
+ public void testEquals() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertEquals(paths1, paths2);
+ }
+
+ @Test
+ public void testEqualsNullPaths() {
+ LocatorPaths paths1 = new LocatorPaths(null, 1, 1);
+ LocatorPaths paths2 = new LocatorPaths(null, 1, 1);
+
+ assertEquals(paths1, paths2);
+ }
+
+ @Test
+ public void testSelfEquals() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertEquals(paths1, paths1);
+ }
+
+ @Test
+ public void testNullNotEquals() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertEquals(false, paths1.equals(null));
+ }
+
+
+ @Test
+ public void testHashCode() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+ @Test
+ public void testNotEquals() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+
+ assertNotEquals(paths1, new Object());
+ }
+
+ @Test
+ public void testNotEqualsPath() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path2", 1, 1);
+
+ assertNotEquals(paths1, paths2);
+ assertNotEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+ @Test
+ public void testNotEqualsNullPath1() {
+ LocatorPaths paths1 = new LocatorPaths(null, 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path2", 1, 1);
+
+ assertNotEquals(paths1, paths2);
+ assertNotEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+ @Test
+ public void testNotEqualsNullPath2() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths(null, 1, 1);
+
+ assertNotEquals(paths1, paths2);
+ assertNotEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+ @Test
+ public void testNotEqualsDepth() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path", 2, 1);
+
+ assertNotEquals(paths1, paths2);
+ assertNotEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+ @Test
+ public void testNotEqualsBundleId() {
+ LocatorPaths paths1 = new LocatorPaths("/lib/path", 1, 1);
+ LocatorPaths paths2 = new LocatorPaths("/lib/path", 1, 2);
+
+ assertNotEquals(paths1, paths2);
+ assertNotEquals(paths1.hashCode(), paths2.hashCode());
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/i18n/it/I18nTestSupport.java b/src/test/java/org/apache/sling/i18n/it/I18nTestSupport.java
index d6feee1..98c67ad 100644
--- a/src/test/java/org/apache/sling/i18n/it/I18nTestSupport.java
+++ b/src/test/java/org/apache/sling/i18n/it/I18nTestSupport.java
@@ -21,11 +21,14 @@
import org.apache.sling.testing.paxexam.TestSupport;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
import static org.ops4j.pax.exam.CoreOptions.composite;
import static org.ops4j.pax.exam.CoreOptions.junitBundles;
import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
@@ -48,7 +51,9 @@
newConfiguration("org.apache.sling.jcr.base.internal.LoginAdminWhitelist")
.put("whitelist.bundles.regexp", "PAXEXAM-PROBE-.*")
.asOption(),
- junitBundles()
+ junitBundles(),
+ optionalRemoteDebug(),
+ optionalJacocoCommand()
);
}
@@ -60,4 +65,30 @@
);
}
+ /**
+ * Optionally configure jacoco vmOption supplied by the "jacoco.command"
+ * system property.
+ */
+ protected ModifiableCompositeOption optionalJacocoCommand() {
+ VMOption option = null;
+ String property = System.getProperty("jacoco.command");
+ if (property != null) {
+ option = vmOption(property);
+ }
+ return composite(option);
+ }
+
+ /**
+ * Optionally configure remote debugging on the port supplied by the "debugPort"
+ * system property.
+ */
+ protected ModifiableCompositeOption optionalRemoteDebug() {
+ VMOption option = null;
+ String property = System.getProperty("debugPort");
+ if (property != null) {
+ option = vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s", property));
+ }
+ return composite(option);
+ }
+
}
diff --git a/src/test/java/org/apache/sling/i18n/it/ResourceBundleLocatorIT.java b/src/test/java/org/apache/sling/i18n/it/ResourceBundleLocatorIT.java
new file mode 100644
index 0000000..e42f532
--- /dev/null
+++ b/src/test/java/org/apache/sling/i18n/it/ResourceBundleLocatorIT.java
@@ -0,0 +1,258 @@
+/*
+ * 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.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.streamBundle;
+import static org.ops4j.pax.tinybundles.core.TinyBundles.withBnd;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import javax.inject.Inject;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.i18n.ResourceBundleProvider;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.ops4j.pax.tinybundles.core.TinyBundle;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+
+/**
+ * Tests for SLING-10135 for locating resource bundle resources
+ * that are not stored in the JCR repository
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class ResourceBundleLocatorIT extends I18nTestSupport {
+ private final Logger logger = LoggerFactory.getLogger(ResourceBundleLocatorIT.class);
+
+ public static final String MSG_KEY1 = "hello";
+ public static final String MSG_KEY2 = "test1";
+
+ public static final String BASENAME0 = "org.apache.sling.i18n.testing0.Resources";
+ public static final String BASENAME1 = "org.apache.sling.i18n.testing1.Resources";
+ public static final String BASENAME2 = "org.apache.sling.i18n.testing2.Resources";
+ public static final String BASENAME3 = "org.apache.sling.i18n.testing3.Resources";
+ public static final String BASENAME4 = "org.apache.sling.i18n.testing4.Resources";
+ public static final String BASENAME5 = "org.apache.sling.i18n.testing5.Resources";
+ public static final String BASENAME6 = "org.apache.sling.i18n.testing6.Resources";
+
+ @Inject
+ private SlingRepository repository;
+
+ @Inject
+ private ResourceBundleProvider resourceBundleProvider;
+
+ private Session session;
+
+ @Override
+ @Configuration
+ public Option[] configuration() {
+ // create 4 tiny bundles with different configurations for testing
+ Option[] bundle = new Option[7];
+ for (int i=0; i <=6; i++) {
+ String bundleSymbolicName = String.format("TEST-I18N-BUNDLE-%d", i);
+
+ String baseName = String.format("org.apache.sling.i18n.testing%d.Resources", i);
+
+ String traversePath;
+ if (i == 5) {
+ traversePath = "";
+ } else if (i == 6) {
+ traversePath = null;
+ } else {
+ traversePath = String.format("/libs/i18n/testing%d", i); //NOSONAR
+ }
+ String resourcePath;
+ if (i <= 1) {
+ resourcePath = String.format("/libs/i18n/testing%d", i); //NOSONAR
+ } else if (i == 2) {
+ resourcePath = String.format("/libs/i18n/testing%d/folder1", i); //NOSONAR
+ } else {
+ resourcePath = String.format("/libs/i18n/testing%d/folder1/folder2", i); //NOSONAR
+ }
+ String pathInBundle = String.format("SLING-INF%s", resourcePath);
+ final Multimap<String, String> content = ImmutableListMultimap.of(
+ pathInBundle, "Resources.json",
+ pathInBundle, "Resources.json.props",
+ pathInBundle, "Resources_en_CA.json",
+ pathInBundle, "Resources_en_CA.json.props"
+ );
+ int traverseDepth;
+ if (i < 4) {
+ traverseDepth = i;
+ } else {
+ traverseDepth = 1;
+ }
+ try {
+ bundle[i] = buildContentBundle(bundleSymbolicName,
+ pathInBundle, resourcePath,
+ traversePath, traverseDepth,
+ content, baseName);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to build the content bundle", e);
+ }
+ }
+
+ return composite(composite(super.configuration()),
+ composite(bundle))
+ .getOptions();
+ }
+
+ /**
+ * Add content to our test bundle
+ */
+ protected void addContent(final TinyBundle bundle, String pathInBundle, String resourcePath, Object ... args) throws IOException {
+ pathInBundle += "/" + resourcePath;
+ resourcePath = "/test-content/" + resourcePath;
+ try (final InputStream is = getClass().getResourceAsStream(resourcePath)) {
+ assertNotNull("Expecting resource to be found:" + resourcePath, is);
+ logger.info("Adding resource to bundle, path={}, resource={}", pathInBundle, resourcePath);
+ if (args != null) {
+ String value = IOUtils.toString(is, StandardCharsets.UTF_8);
+ value = String.format(value, args);
+ try (final InputStream valueStream = new ByteArrayInputStream(value.getBytes())) {
+ bundle.add(pathInBundle, valueStream);
+ }
+ } else {
+ bundle.add(pathInBundle, is);
+ }
+ }
+ }
+
+ protected Option buildContentBundle(String bundleSymbolicName,
+ String pathInBundle, String resourcePath,
+ String traversePath, int traverseDepth,
+ final Multimap<String, String> content, String basename) throws IOException {
+ final TinyBundle bundle = TinyBundles.bundle();
+ bundle.set(Constants.BUNDLE_SYMBOLICNAME, bundleSymbolicName);
+ bundle.set(Constants.REQUIRE_CAPABILITY, "osgi.extender;filter:=\"(&(osgi.extender=org.apache.sling.i18n.resourcebundle.locator.registrar)(version<=1.0.0)(!(version>=2.0.0)))\"");
+ if (traverseDepth <= 0) {
+ if (traversePath == null) {
+ bundle.set(Constants.PROVIDE_CAPABILITY, "org.apache.sling.i18n.resourcebundle.locator");
+ } else {
+ bundle.set(Constants.PROVIDE_CAPABILITY, String.format("org.apache.sling.i18n.resourcebundle.locator;paths=\"%s\"", traversePath));
+ }
+ } else {
+ if (traversePath == null) {
+ bundle.set(Constants.PROVIDE_CAPABILITY, String.format("org.apache.sling.i18n.resourcebundle.locator;depth=%d", traverseDepth));
+ } else {
+ bundle.set(Constants.PROVIDE_CAPABILITY, String.format("org.apache.sling.i18n.resourcebundle.locator;paths=\"%s\";depth=%d", traversePath, traverseDepth));
+ }
+ }
+ bundle.set("Sling-Bundle-Resources", String.format("%s;path:=%s;propsJSON:=props", resourcePath, pathInBundle));
+
+ for (final Map.Entry<String, String> entry : content.entries()) {
+ String entryPathInBundle = entry.getKey();
+ String entryResourcePath = entry.getValue();
+ if (entryResourcePath.endsWith(".props")) {
+ // content is a template so we need to pass the args to replace the placeholder tokens
+ addContent(bundle, entryPathInBundle, entryResourcePath, basename);
+ } else {
+ // content is not a template, so no need to pass any args
+ addContent(bundle, entryPathInBundle, entryResourcePath);
+ }
+ }
+ return streamBundle(
+ bundle.build(withBnd())
+ ).start();
+ }
+
+ @Before
+ public void setup() throws RepositoryException {
+ session = repository.loginAdministrative(null);
+ }
+
+ @After
+ public void cleanup() throws RepositoryException {
+ session.logout();
+ }
+
+ private void assertMessage(final String key, final Locale locale, final String basename, final String value) {
+ final ResourceBundle resourceBundle = resourceBundleProvider.getResourceBundle(basename, locale);
+ assertNotNull(resourceBundle);
+ assertEquals(value, resourceBundle.getString(key));
+ }
+
+ @Test
+ public void testLocatedResourceBundleDepthNotSpecified() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME0, "World");
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME0, "Canada");
+ }
+
+ @Test
+ public void testLocatedResourceBundleDepth1() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME1, "World");
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME1, "Canada");
+ }
+
+ @Test
+ public void testLocatedResourceBundleDepth2() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME2, "World");
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME2, "Canada");
+ }
+
+ @Test
+ public void testLocatedResourceBundleDepth3() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME3, "World");
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME3, "Canada");
+ }
+
+ @Test
+ public void testNotLocatedResourceBundle() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME4, MSG_KEY1);
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME4, MSG_KEY1);
+ }
+
+ @Test
+ public void testNotLocatedResourceBundleEmptyLocatorPath() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME5, MSG_KEY1);
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME5, MSG_KEY1);
+ }
+
+ @Test
+ public void testNotLocatedResourceBundleNotSpecifiedLocatorPath() throws RepositoryException {
+ assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME6, MSG_KEY1);
+ assertMessage(MSG_KEY1, Locale.CANADA, BASENAME6, MSG_KEY1);
+ }
+}
diff --git a/src/test/resources/test-content/Resources.json b/src/test/resources/test-content/Resources.json
new file mode 100644
index 0000000..c6d82d7
--- /dev/null
+++ b/src/test/resources/test-content/Resources.json
@@ -0,0 +1,4 @@
+{
+ "hello":"World",
+ "test1":"value1",
+}
diff --git a/src/test/resources/test-content/Resources.json.props b/src/test/resources/test-content/Resources.json.props
new file mode 100644
index 0000000..f7e5140
--- /dev/null
+++ b/src/test/resources/test-content/Resources.json.props
@@ -0,0 +1,4 @@
+{
+ "jcr:language":"en",
+ "sling:basename":"%s"
+}
diff --git a/src/test/resources/test-content/Resources_en_CA.json b/src/test/resources/test-content/Resources_en_CA.json
new file mode 100644
index 0000000..9fc837e
--- /dev/null
+++ b/src/test/resources/test-content/Resources_en_CA.json
@@ -0,0 +1,4 @@
+{
+ "hello":"Canada",
+ "test1":"value1ca",
+}
diff --git a/src/test/resources/test-content/Resources_en_CA.json.props b/src/test/resources/test-content/Resources_en_CA.json.props
new file mode 100644
index 0000000..97c8711
--- /dev/null
+++ b/src/test/resources/test-content/Resources_en_CA.json.props
@@ -0,0 +1,4 @@
+{
+ "jcr:language":"en_CA",
+ "sling:basename":"%s"
+}