| /* |
| * 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)); |
| } |
| |
| } |