blob: 2ed13656277216a43655fa6802b4700ee1eb9753 [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.superimposing.impl;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_OVERLAYABLE;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_REGISTER_PARENT;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import org.apache.commons.collections.IteratorUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.superimposing.SuperimposingResourceProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
@RunWith(MockitoJUnitRunner.class)
public class SuperimposingManagerImplTest {
@Mock
private Dictionary<String, Object> componentContextProperties;
@Mock
private ComponentContext componentContext;
@Mock
private BundleContext bundleContext;
@Mock
private ResourceResolverFactory resourceResolverFactory;
@Mock
private ResourceResolver resourceResolver;
@Mock(answer=Answers.RETURNS_DEEP_STUBS)
private Session session;
private List<ServiceRegistration> serviceRegistrations = new ArrayList<ServiceRegistration>();
private SuperimposingManagerImpl underTest;
private static final String ORIGINAL_PATH = "/root/path1";
private static final String SUPERIMPOSED_PATH = "/root/path2";
private static final String OBSERVATION_PATH = "/root";
@SuppressWarnings("unchecked")
@Before
public void setUp() throws LoginException {
when(componentContext.getBundleContext()).thenReturn(bundleContext);
when(componentContext.getProperties()).thenReturn(componentContextProperties);
when(componentContextProperties.get(SuperimposingManagerImpl.OBSERVATION_PATHS_PROPERTY)).thenReturn(new String[] { OBSERVATION_PATH });
when(resourceResolverFactory.getAdministrativeResourceResolver(any(Map.class))).thenReturn(resourceResolver);
when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
// collect a list of all service registrations to validate that they are all unregistered on shutdown
when(bundleContext.registerService(anyString(), anyObject(), any(Dictionary.class))).thenAnswer(new Answer<ServiceRegistration>() {
public ServiceRegistration answer(InvocationOnMock invocation) {
final ServiceRegistration mockRegistration = mock(ServiceRegistration.class);
serviceRegistrations.add(mockRegistration);
doAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
return serviceRegistrations.remove(mockRegistration);
}
}).when(mockRegistration).unregister();
return mockRegistration;
}
});
// simulate absolute path access to properties via session object
try {
when(session.itemExists(anyString())).thenAnswer(new Answer<Boolean>() {
public Boolean answer(InvocationOnMock invocation) throws Throwable {
final String absolutePath = (String)invocation.getArguments()[0];
final String nodePath = ResourceUtil.getParent(absolutePath);
final String propertyName = ResourceUtil.getName(absolutePath);
Resource resource = resourceResolver.getResource(nodePath);
if (resource!=null) {
ValueMap props = resource.adaptTo(ValueMap.class);
return props.containsKey(propertyName);
}
else {
return false;
}
}
});
when(session.getProperty(anyString())).thenAnswer(new Answer<Property>() {
public Property answer(InvocationOnMock invocation) throws Throwable {
final String absolutePath = (String)invocation.getArguments()[0];
final String nodePath = ResourceUtil.getParent(absolutePath);
final String propertyName = ResourceUtil.getName(absolutePath);
Resource resource = resourceResolver.getResource(nodePath);
if (resource!=null) {
ValueMap props = resource.adaptTo(ValueMap.class);
Object value = props.get(propertyName);
if (value==null) {
throw new PathNotFoundException();
}
Property prop = mock(Property.class);
when(prop.getName()).thenReturn(propertyName);
if (value instanceof String) {
when(prop.getString()).thenReturn((String)value);
}
else if (value instanceof Boolean) {
when(prop.getBoolean()).thenReturn((Boolean)value);
}
return prop;
}
else {
throw new PathNotFoundException();
}
}
});
}
catch (RepositoryException ex) {
throw new RuntimeException(ex);
}
}
private void initialize(boolean enabled) throws InterruptedException, LoginException, RepositoryException {
when(componentContextProperties.get(SuperimposingManagerImpl.ENABLED_PROPERTY)).thenReturn(enabled);
underTest = new SuperimposingManagerImpl().withResourceResolverFactory(resourceResolverFactory);
underTest.activate(componentContext);
if (underTest.isEnabled()) {
// verify observation registration
verify(session.getWorkspace().getObservationManager()).addEventListener(any(EventListener.class), anyInt(), eq(OBSERVATION_PATH), anyBoolean(), any(String[].class), any(String[].class), anyBoolean());
// wait until separate initialization thread has finished
while (!underTest.initialization.isDone()) {
Thread.sleep(10);
}
}
}
@After
public void tearDown() throws RepositoryException {
underTest.deactivate(componentContext);
if (underTest.isEnabled()) {
// verify observation and resource resolver are terminated correctly
verify(session.getWorkspace().getObservationManager()).removeEventListener(any(EventListener.class));
verify(resourceResolver).close();
}
// make sure all registrations are unregistered on shutdown
for (ServiceRegistration registration : serviceRegistrations) {
verify(registration, times(1)).unregister();
}
}
private Resource prepareSuperimposingResource(String superimposedPath, String sourcePath, boolean registerParent, boolean overlayable) {
Resource resource = mock(Resource.class);
when(resource.getPath()).thenReturn(superimposedPath);
ValueMap props = new ValueMapDecorator(new HashMap<String, Object>());
props.put(PROP_SUPERIMPOSE_SOURCE_PATH, sourcePath);
props.put(PROP_SUPERIMPOSE_REGISTER_PARENT, registerParent);
props.put(PROP_SUPERIMPOSE_OVERLAYABLE, overlayable);
when(resource.adaptTo(ValueMap.class)).thenReturn(props);
when(resourceResolver.getResource(superimposedPath)).thenReturn(resource);
return resource;
}
private void moveSuperimposedResource(Resource resource, String newPath) {
String oldPath = resource.getPath();
when(resource.getPath()).thenReturn(newPath);
when(resourceResolver.getResource(oldPath)).thenReturn(null);
when(resourceResolver.getResource(newPath)).thenReturn(resource);
}
@Test
public void testDisabled() throws InterruptedException, LoginException, RepositoryException {
// make sure that no exception is thrown when service is disabled on activate/deactivate
initialize(false);
verifyZeroInteractions(resourceResolverFactory);
verifyZeroInteractions(bundleContext);
}
@SuppressWarnings("unchecked")
@Test
public void testFindAllSuperimposings() throws InterruptedException, LoginException, RepositoryException {
// prepare a query that returns one existing superimposed resource
when(componentContextProperties.get(SuperimposingManagerImpl.FINDALLQUERIES_PROPERTY)).thenReturn("syntax|query");
when(resourceResolver.findResources("query", "syntax")).then(new Answer<Iterator<Resource>>() {
public Iterator<Resource> answer(InvocationOnMock invocation) {
return Arrays.asList(new Resource[] {
prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false)
}).iterator();
}
});
initialize(true);
// ensure the superimposed resource is detected and registered
List<SuperimposingResourceProvider> providers = IteratorUtils.toList(underTest.getRegisteredProviders());
assertEquals(1, providers.size());
SuperimposingResourceProvider provider = providers.iterator().next();
assertEquals(SUPERIMPOSED_PATH, provider.getRootPath());
assertEquals(ORIGINAL_PATH, provider.getSourcePath());
assertFalse(provider.isOverlayable());
verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
}
private EventIterator prepareNodeCreateEvent(Resource pResource) throws RepositoryException {
String resourcePath = pResource.getPath();
Event nodeEvent = mock(Event.class);
when(nodeEvent.getType()).thenReturn(Event.NODE_ADDED);
when(nodeEvent.getPath()).thenReturn(resourcePath);
Event propertyEvent = mock(Event.class);
when(propertyEvent.getType()).thenReturn(Event.PROPERTY_ADDED);
when(propertyEvent.getPath()).thenReturn(resourcePath + "/" + SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH);
EventIterator eventIterator = mock(EventIterator.class);
when(eventIterator.hasNext()).thenReturn(true, true, false);
when(eventIterator.nextEvent()).thenReturn(nodeEvent, propertyEvent);
return eventIterator;
}
private EventIterator prepareNodeChangeEvent(Resource pResource) throws RepositoryException {
String resourcePath = pResource.getPath();
Event propertyEvent = mock(Event.class);
when(propertyEvent.getType()).thenReturn(Event.PROPERTY_CHANGED);
when(propertyEvent.getPath()).thenReturn(resourcePath + "/" + SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH);
EventIterator eventIterator = mock(EventIterator.class);
when(eventIterator.hasNext()).thenReturn(true, false);
when(eventIterator.nextEvent()).thenReturn(propertyEvent);
return eventIterator;
}
private EventIterator prepareNodeRemoveEvent(Resource pResource) throws RepositoryException {
String resourcePath = pResource.getPath();
Event nodeEvent = mock(Event.class);
when(nodeEvent.getType()).thenReturn(Event.NODE_REMOVED);
when(nodeEvent.getPath()).thenReturn(resourcePath);
EventIterator eventIterator = mock(EventIterator.class);
when(eventIterator.hasNext()).thenReturn(true, false);
when(eventIterator.nextEvent()).thenReturn(nodeEvent);
return eventIterator;
}
private EventIterator prepareNodeMoveEvent(Resource pResource, String pOldPath) throws RepositoryException {
String resourcePath = pResource.getPath();
Event nodeRemoveEvent = mock(Event.class);
when(nodeRemoveEvent.getType()).thenReturn(Event.NODE_REMOVED);
when(nodeRemoveEvent.getPath()).thenReturn(pOldPath);
Event nodeCreateEvent = mock(Event.class);
when(nodeCreateEvent.getType()).thenReturn(Event.NODE_ADDED);
when(nodeCreateEvent.getPath()).thenReturn(resourcePath);
EventIterator eventIterator = mock(EventIterator.class);
when(eventIterator.hasNext()).thenReturn(true, true, false);
when(eventIterator.nextEvent()).thenReturn(nodeRemoveEvent, nodeCreateEvent);
return eventIterator;
}
@SuppressWarnings("unchecked")
@Test
public void testSuperimposedResourceCreateUpdateRemove() throws InterruptedException, LoginException, RepositoryException {
initialize(true);
// simulate node create event
Resource superimposedResource = prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false);
underTest.onEvent(prepareNodeCreateEvent(superimposedResource));
// ensure the superimposed resource is detected and registered
List<SuperimposingResourceProvider> providers = IteratorUtils.toList(underTest.getRegisteredProviders());
assertEquals(1, providers.size());
SuperimposingResourceProvider provider = providers.iterator().next();
assertEquals(SUPERIMPOSED_PATH, provider.getRootPath());
assertEquals(ORIGINAL_PATH, provider.getSourcePath());
assertFalse(provider.isOverlayable());
verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
// simulate a change in the original path
superimposedResource.adaptTo(ValueMap.class).put(PROP_SUPERIMPOSE_SOURCE_PATH, "/other/path");
underTest.onEvent(prepareNodeChangeEvent(superimposedResource));
// ensure the superimposed resource update is detected and a new provider instance is registered
providers = IteratorUtils.toList(underTest.getRegisteredProviders());
assertEquals(1, providers.size());
SuperimposingResourceProvider provider2 = providers.iterator().next();
assertEquals(SUPERIMPOSED_PATH, provider2.getRootPath());
assertEquals("/other/path", provider2.getSourcePath());
assertFalse(provider2.isOverlayable());
verify(bundleContext).registerService(anyString(), same(provider2), any(Dictionary.class));
// simulate node removal
underTest.onEvent(prepareNodeRemoveEvent(superimposedResource));
// ensure provider is removed
providers = IteratorUtils.toList(underTest.getRegisteredProviders());
assertEquals(0, providers.size());
}
@SuppressWarnings("unchecked")
@Test
public void testSuperimposedResourceCreateMove() throws InterruptedException, LoginException, RepositoryException {
when(componentContextProperties.get(SuperimposingManagerImpl.FINDALLQUERIES_PROPERTY)).thenReturn("syntax|query");
initialize(true);
// simulate node create event
final Resource superimposedResource = prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false);
underTest.onEvent(prepareNodeCreateEvent(superimposedResource));
// simulate a node move event
String oldPath = superimposedResource.getPath();
moveSuperimposedResource(superimposedResource, "/new/path");
// prepare a query that returns the moved superimposed resource
when(resourceResolver.findResources("query", "syntax")).then(new Answer<Iterator<Resource>>() {
public Iterator<Resource> answer(InvocationOnMock invocation) {
return Arrays.asList(new Resource[] {
superimposedResource
}).iterator();
}
});
underTest.onEvent(prepareNodeMoveEvent(superimposedResource, oldPath));
// ensure the superimposed resource update is detected and a new provider instance is registered
List<SuperimposingResourceProvider> providers = IteratorUtils.toList(underTest.getRegisteredProviders());
assertEquals(1, providers.size());
SuperimposingResourceProvider provider = providers.iterator().next();
assertEquals("/new/path", provider.getRootPath());
assertEquals(ORIGINAL_PATH, provider.getSourcePath());
assertFalse(provider.isOverlayable());
verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
}
}