blob: 5d83705095296f7dee09d205d04e80288a1448d3 [file] [log] [blame]
/*
* 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.resourceresolver.impl.providers;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.api.resource.path.PathSet;
import org.apache.sling.api.resource.runtime.dto.AuthType;
import org.apache.sling.api.resource.runtime.dto.RuntimeDTO;
import org.apache.sling.resourceresolver.impl.Fixture;
import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker.ChangeListener;
import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker.ObservationReporterGenerator;
import org.apache.sling.spi.resource.provider.ObservationReporter;
import org.apache.sling.spi.resource.provider.ObserverConfiguration;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.event.EventAdmin;
public class ResourceProviderTrackerTest {
@Rule
public OsgiContext context = new OsgiContext();
private EventAdmin eventAdmin;
private ResourceProviderInfo rp2Info;
private Fixture fixture;
@Before
public void prepare() throws Exception {
eventAdmin = context.getService(EventAdmin.class);
fixture = new Fixture(context.bundleContext());
}
private ResourceProviderTracker registerDefaultResourceProviderTracker() throws Exception {
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp = mock(ResourceProvider.class);
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp2 = mock(ResourceProvider.class);
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp3 = mock(ResourceProvider.class);
fixture.registerResourceProvider(rp, "/", AuthType.no);
rp2Info = fixture.registerResourceProvider(rp2, "/path", AuthType.lazy);
fixture.registerResourceProvider(rp3, "invalid", AuthType.no);
ResourceProviderTracker tracker = new ResourceProviderTracker();
tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()));
tracker.activate(context.bundleContext(), eventAdmin, new DoNothingChangeListener());
return tracker;
}
@Test
public void activate() throws Exception {
ResourceProviderTracker tracker = registerDefaultResourceProviderTracker();
// since the OSGi mocks are asynchronous we don't have to wait for the changes to propagate
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(2));
fixture.unregisterResourceProvider(rp2Info);
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(1));
}
@Test
public void deactivate() throws Exception {
ResourceProviderTracker tracker = registerDefaultResourceProviderTracker();
tracker.deactivate();
assertThat(tracker.getResourceProviderStorage().getAllHandlers(), hasSize(0));
}
@Test
public void testActivationDeactivation() throws Exception {
final ResourceProviderTracker tracker = new ResourceProviderTracker();
tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()));
// create boolean markers for the listener
final AtomicBoolean addedCalled = new AtomicBoolean(false);
final AtomicBoolean removedCalled = new AtomicBoolean(false);
final ChangeListener listener = new ChangeListener() {
@Override
public void providerAdded() {
addedCalled.set(true);
}
@Override
public void providerRemoved(String name, String pid, boolean stateful, boolean used) {
removedCalled.set(true);
}
};
// activate and check that no listener is called yet
tracker.activate(context.bundleContext(), eventAdmin, listener);
assertFalse(addedCalled.get());
assertFalse(removedCalled.get());
// add a new resource provider
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp = mock(ResourceProvider.class);
final ResourceProviderInfo info = fixture.registerResourceProvider(rp, "/", AuthType.no);
// check added is called but not removed
assertTrue(addedCalled.get());
assertFalse(removedCalled.get());
// verify a single provider
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(1));
// reset boolean markers
addedCalled.set(false);
removedCalled.set(false);
// remove provider
fixture.unregisterResourceProvider(info);
// verify removed is called but not added
assertTrue(removedCalled.get());
assertFalse(addedCalled.get());
// no provider anymore
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(0));
}
@Test
public void testReactivation() throws Exception {
final ResourceProviderTracker tracker = new ResourceProviderTracker();
tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()));
// create boolean markers for the listener
final AtomicBoolean addedCalled = new AtomicBoolean(false);
final AtomicBoolean removedCalled = new AtomicBoolean(false);
final ChangeListener listener = new ChangeListener() {
@Override
public void providerAdded() {
addedCalled.set(true);
}
@Override
public void providerRemoved(String name, String pid, boolean stateful, boolean used) {
removedCalled.set(true);
}
};
// activate and check that no listener is called yet
tracker.activate(context.bundleContext(), eventAdmin, listener);
assertFalse(addedCalled.get());
assertFalse(removedCalled.get());
// activate and check that no listener is called yet
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp = mock(ResourceProvider.class);
final ResourceProviderInfo info = fixture.registerResourceProvider(rp, "/", AuthType.no);
// check added is called but not removed
assertTrue(addedCalled.get());
assertFalse(removedCalled.get());
// verify a single provider
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(1));
// reset boolean markers
addedCalled.set(false);
removedCalled.set(false);
// add overlay provider with higher service ranking
@SuppressWarnings("unchecked")
ResourceProvider<Object> rp2 = mock(ResourceProvider.class);
final ResourceProviderInfo infoOverlay = fixture.registerResourceProvider(rp2, "/", AuthType.no, 1000);
// check added and removed is called
assertTrue(addedCalled.get());
assertTrue(removedCalled.get());
// verify a single provider
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(1));
// reset boolean markers
addedCalled.set(false);
removedCalled.set(false);
// unregister overlay provider
fixture.unregisterResourceProvider(infoOverlay);
// check added and removed is called
assertTrue(addedCalled.get());
assertTrue(removedCalled.get());
// verify a single provider
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(1));
// reset boolean markers
addedCalled.set(false);
removedCalled.set(false);
// unregister first provider
fixture.unregisterResourceProvider(info);
// check removed is called but not added
assertTrue(removedCalled.get());
assertFalse(addedCalled.get());
// verify no provider
assertThat(tracker.getResourceProviderStorage().getAllHandlers().size(), equalTo(0));
}
/**
* This test verifies that shadowing of Resource observation is deterministic when ResourceProviders get registered and unregistered,
* meaning it is independent of the order in which those events happen.
* <p>
* It does so by
* 1) registering a ResourceProvider A on a deeper path then root (shadowing root)
* 2) registering a ResourceProvider B on root
* 3) unregistering the ResourceProvider A
* 4) and registering the ResoucreProvider A
* <p>
* This guarantees in both cases (A before B and B before A) the same excludes are applied in the ObservationReporter.
*
* @throws InvalidSyntaxException
*/
@Test
public void testDeterministicObservationShadowing() throws InvalidSyntaxException {
final ResourceProviderTracker tracker = new ResourceProviderTracker();
final Map<String, List<String>> excludeSets = new HashMap<>();
tracker.activate(context.bundleContext(), eventAdmin, null);
tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()) {
@Override
public ObservationReporter create(Path path, PathSet excludes) {
List<String> excludeSetsPerPath = excludeSets.get(path.getPath());
if (excludeSetsPerPath == null) {
excludeSetsPerPath = new ArrayList<>(1);
excludeSets.put(path.getPath(), excludeSetsPerPath);
}
excludeSetsPerPath.clear();
for (Path exclude : excludes) {
excludeSetsPerPath.add(exclude.getPath());
}
return super.create(path, excludes);
}
});
ResourceProvider<?> rp = mock(ResourceProvider.class);
ResourceProviderInfo info;
// register RP on /path, empty exclude set expected
info = fixture.registerResourceProvider(rp, "/path", AuthType.no);
assertNull(excludeSets.get("/"));
assertThat(excludeSets.get("/path"), hasSize(0));
// register RP on /, expect /path excluded
fixture.registerResourceProvider(rp, "/", AuthType.no);
assertThat(excludeSets.get("/"), hasSize(1));
assertThat(excludeSets.get("/"), contains("/path"));
assertThat(excludeSets.get("/path"), hasSize(0));
// unregister RP on /path, empty exclude set expected
fixture.unregisterResourceProvider(info);
assertThat(excludeSets.get("/"), hasSize(0));
assertThat(excludeSets.get("/path"), hasSize(0));
// register RP on /path again, expect /path excluded
fixture.registerResourceProvider(rp, "/path", AuthType.no);
assertThat(excludeSets.get("/"), hasSize(1));
assertThat(excludeSets.get("/"), contains("/path"));
assertThat(excludeSets.get("/path"), hasSize(0));
}
@Test
public void testUpdateOnlyOnIntersectingProviders() throws InvalidSyntaxException {
final ResourceProviderTracker tracker = new ResourceProviderTracker();
tracker.activate(context.bundleContext(), eventAdmin, null);
tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()));
ResourceProvider<?> rootRp = mock(ResourceProvider.class);
ResourceProvider<?> fooRp = mock(ResourceProvider.class);
ResourceProvider<?> barRp = mock(ResourceProvider.class);
ResourceProvider<?> foobarRp = mock(ResourceProvider.class);
ResourceProviderInfo info;
// register RPs and verify how often update() gets called
fixture.registerResourceProvider(rootRp, "/", AuthType.no);
verify(rootRp, never()).update(anyLong());
fixture.registerResourceProvider(fooRp, "/foo", AuthType.no);
verify(rootRp, times(1)).update(anyLong());
verify(fooRp, never()).update(anyLong());
info = fixture.registerResourceProvider(barRp, "/bar", AuthType.no);
verify(rootRp, times(2)).update(anyLong());
verify(fooRp, never()).update(anyLong());
verify(barRp, never()).update(anyLong());
fixture.unregisterResourceProvider(info);
verify(rootRp, times(3)).update(anyLong());
verify(fooRp, never()).update(anyLong());
verify(barRp, never()).update(anyLong());
fixture.registerResourceProvider(foobarRp, "/foo/bar", AuthType.no);
verify(rootRp, times(4)).update(anyLong());
verify(fooRp, times(1)).update(anyLong());
verify(barRp, never()).update(anyLong());
verify(foobarRp, never()).update(anyLong());
}
@Test
public void fillDto() throws Exception {
ResourceProviderTracker tracker = registerDefaultResourceProviderTracker();
RuntimeDTO dto = new RuntimeDTO();
tracker.fill(dto);
assertThat( dto.providers, arrayWithSize(2));
assertThat( dto.failedProviders, arrayWithSize(1));
}
static class DoNothingObservationReporter implements ObservationReporter {
@Override
public void reportChanges(Iterable<ResourceChange> changes, boolean distribute) {
}
@Override
public void reportChanges(ObserverConfiguration config, Iterable<ResourceChange> changes, boolean distribute) {
}
@Override
public List<ObserverConfiguration> getObserverConfigurations() {
return Collections.emptyList();
}
}
static class SimpleObservationReporterGenerator implements ObservationReporterGenerator {
private final ObservationReporter reporter;
SimpleObservationReporterGenerator(ObservationReporter reporter) {
this.reporter = reporter;
}
@Override
public ObservationReporter createProviderReporter() {
return reporter;
}
@Override
public ObservationReporter create(Path path, PathSet excludes) {
return reporter;
}
}
static final class DoNothingChangeListener implements ChangeListener {
@Override
public void providerAdded() {
// TODO Auto-generated method stub
}
@Override
public void providerRemoved(String name, String pid, boolean stateful, boolean used) {
// TODO Auto-generated method stub
}
}
}