/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr.sequencer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.modeshape.jcr.api.observation.Event.Sequencing.NODE_SEQUENCED; import static org.modeshape.jcr.api.observation.Event.Sequencing.NODE_SEQUENCING_FAILURE; import static org.modeshape.jcr.api.observation.Event.Sequencing.OUTPUT_PATH; import static org.modeshape.jcr.api.observation.Event.Sequencing.SELECTED_PATH; import static org.modeshape.jcr.api.observation.Event.Sequencing.SEQUENCED_NODE_ID; import static org.modeshape.jcr.api.observation.Event.Sequencing.SEQUENCED_NODE_PATH; import static org.modeshape.jcr.api.observation.Event.Sequencing.SEQUENCER_NAME; import static org.modeshape.jcr.api.observation.Event.Sequencing.USER_ID; import java.io.InputStream; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Workspace; import javax.jcr.observation.EventIterator; import javax.jcr.observation.EventListener; import javax.jcr.observation.EventListenerIterator; import javax.jcr.observation.ObservationManager; import org.junit.Assert; import org.modeshape.jcr.JcrLexicon; import org.modeshape.jcr.JcrSession; import org.modeshape.jcr.RepositoryConfiguration; import org.modeshape.jcr.SingleUseAbstractTest; import org.modeshape.jcr.TestingEnvironment; import org.modeshape.jcr.api.JcrConstants; import org.modeshape.jcr.api.observation.Event; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class which serves as base for various sequencer unit tests. In addition to this, it uses the sequencing events fired by * ModeShape's {@link javax.jcr.observation.ObservationManager} to perform various assertions and therefore, acts as a test for * those as well. * * @author Horia Chiorean */ public abstract class AbstractSequencerTest extends SingleUseAbstractTest { private static final int DEFAULT_WAIT_TIME_SECONDS = 15; protected Node rootNode; private ObservationManager observationManager; /** * A [node path, node instance] map which is populated by the listener, once each sequencing event is received */ private final Map<String, Node> sequencedNodes = new ConcurrentHashMap<>(); /** * A [node path, latch] map which is used to block tests waiting for sequenced output, until either the node has been * sequenced or a timeout occurs */ private final ConcurrentHashMap<String, CountDownLatch> nodeSequencedLatches = new ConcurrentHashMap<String, CountDownLatch>(); /** * A [node path, latch] map which is used to block tests waiting for a sequencing failure, until either the failure has * occurred or a timeout occurs */ private final ConcurrentHashMap<String, CountDownLatch> sequencingFailureLatches = new ConcurrentHashMap<String, CountDownLatch>(); /** * A [sequenced node path, event] map which will hold all the received sequencing events, both in failure and non-failure * cases, using the path of the sequenced node as key. */ private final ConcurrentHashMap<String, Event> sequencingEvents = new ConcurrentHashMap<String, Event>(); private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public void beforeEach() throws Exception { super.beforeEach(); rootNode = session.getRootNode(); addSequencingListeners(session); } protected void addSequencingListeners( JcrSession session ) throws RepositoryException { observationManager = ((Workspace)session.getWorkspace()).getObservationManager(); observationManager.addEventListener(new SequencingListener(), NODE_SEQUENCED, null, true, null, null, false); observationManager.addEventListener(new SequencingFailureListener(), NODE_SEQUENCING_FAILURE, null, true, null, null, false); } @Override public void afterEach() throws Exception { for (EventListenerIterator it = observationManager.getRegisteredEventListeners(); it.hasNext();) { observationManager.removeEventListener(it.nextEventListener()); } super.afterEach(); cleanupData(); } private void cleanupData() { sequencedNodes.clear(); sequencingEvents.clear(); nodeSequencedLatches.clear(); sequencingFailureLatches.clear(); } @Override protected RepositoryConfiguration createRepositoryConfiguration(String repositoryName) throws Exception { RepositoryConfiguration config = RepositoryConfiguration.read(getRepositoryConfigStream(), repositoryName); return config.with(new TestingEnvironment()); } /** * Returns an input stream to a JSON file which will be used to configure the repository. By default, this is * config/repo-config.json * * @return a {@code InputStream} instance */ protected InputStream getRepositoryConfigStream() { return resourceStream("config/repo-config.json"); } /** * Creates a nt:file node, under the root node, at the given path and with the jcr:data property pointing at the filepath. * * @param nodeRelativePath the path under the root node, where the nt:file will be created. * @param filePath a path relative to {@link Class#getResourceAsStream(String)} where a file is expected at runtime * @return the new node * @throws RepositoryException if anything fails */ protected Node createNodeWithContentFromFile( String nodeRelativePath, String filePath ) throws RepositoryException { Node parent = rootNode; for (String pathSegment : nodeRelativePath.split("/")) { parent = parent.addNode(pathSegment); } Node content = parent.addNode(JcrConstants.JCR_CONTENT); content.setProperty(JcrConstants.JCR_DATA, ((javax.jcr.Session)session).getValueFactory().createBinary(resourceStream(filePath))); session.save(); return parent; } /** * Retrieves a sequenced node using 5 seconds as maximum wait time. * * @param parentNode an existing {@link Node} * @param relativePath the path under the parent node at which the sequenced node is expected to appear (note that this must * be the path to the "new" node, always. * @return either the sequenced node or null, if something has failed. * @throws Exception if anything unexpected happens * @see AbstractSequencerTest#getOutputNode(javax.jcr.Node, String, int) */ protected Node getOutputNode( Node parentNode, String relativePath ) throws Exception { return this.getOutputNode(parentNode, relativePath, DEFAULT_WAIT_TIME_SECONDS); } /** * Attempts to retrieve a node (which is expected to have been sequenced) under an existing parent node at a relative path. * The sequenced node "appears" when the {@link SequencingListener} is notified of the sequencing process. The thread which * calls this method either returns immediately if the node has already been sequenced, or waits a number of seconds for it to * become available. * * @param parentNode an existing {@link Node} * @param relativePath the path under the parent node at which the sequenced node is expected to appear (note that this must * be the path to the "new" node, always. * @param waitTimeSeconds the max number of seconds to wait. * @return either the sequenced node or null, if something has failed. * @throws Exception if anything unexpected happens * @throws java.lang.AssertionError if the specified period of time has elapsed, but not enough sequencing events were * received */ protected Node getOutputNode( Node parentNode, String relativePath, int waitTimeSeconds ) throws Exception { String parentNodePath = parentNode.getPath(); String expectedPath = parentNodePath.endsWith("/") ? parentNodePath + relativePath : parentNodePath + "/" + relativePath; return getOutputNode(expectedPath, waitTimeSeconds); } protected Node getOutputNode( String expectedPath ) throws InterruptedException { return getOutputNode(expectedPath, DEFAULT_WAIT_TIME_SECONDS); } /** * Retrieves a new node under the given path, as a result of sequencing, or returns null if the given timeout occurs. * * @param expectedPath * @param waitTimeSeconds * @return the output node * @throws InterruptedException */ protected Node getOutputNode( String expectedPath, int waitTimeSeconds ) throws InterruptedException { if (!sequencedNodes.containsKey(expectedPath)) { createWaitingLatchIfNecessary(expectedPath, nodeSequencedLatches); logger.debug("Waiting for sequenced node at: " + expectedPath); CountDownLatch countDownLatch = nodeSequencedLatches.get(expectedPath); countDownLatch.await(waitTimeSeconds, TimeUnit.SECONDS); } nodeSequencedLatches.remove(expectedPath); return sequencedNodes.remove(expectedPath); } protected void expectSequencingFailure( Node sequencedNode ) throws Exception { expectSequencingFailure(sequencedNode, 5); } protected void expectSequencingFailure( Node sequencedNode, int waitTimeSeconds ) throws Exception { String nodePath = sequencedNode.getPath(); createWaitingLatchIfNecessary(nodePath, sequencingFailureLatches); CountDownLatch countDownLatch = sequencingFailureLatches.get(nodePath); assertTrue("Sequencing failure event not received", countDownLatch.await(waitTimeSeconds, TimeUnit.SECONDS)); sequencingFailureLatches.remove(nodePath); } protected void createWaitingLatchIfNecessary( String expectedPath, ConcurrentHashMap<String, CountDownLatch> latchesMap ) { latchesMap.putIfAbsent(expectedPath, new CountDownLatch(1)); } protected void smokeCheckSequencingEvent( Event event, int expectedEventType, String... expectedEventInfoKeys ) throws RepositoryException { assertEquals(event.getType(), expectedEventType); Map<?, ?> info = event.getInfo(); assertNotNull(info); for (String extraInfoKey : expectedEventInfoKeys) { assertNotNull(info.get(extraInfoKey)); } } protected void assertCreatedBySessionUser( Node node, Session session ) throws RepositoryException { assertEquals(session.getUserID(), node.getProperty(JcrLexicon.CREATED_BY.getString()).getString()); } private Map<?, ?> getSequencingEventInfo( Node sequencedNode ) throws RepositoryException { Event receivedEvent = sequencingEvents.get(sequencedNode.getPath()); assertNotNull(receivedEvent); return receivedEvent.getInfo(); } protected Map<?, ?> assertSequencingEventInfo( Node sequencedNode, String expectedUserId, String expectedSequencerName, String expectedSelectedPath, String expectedOutputPath ) throws RepositoryException { Map<?, ?> sequencingEventInfo = getSequencingEventInfo(sequencedNode); Assert.assertEquals(expectedUserId, sequencingEventInfo.get(Event.Sequencing.USER_ID)); Assert.assertEquals(expectedSequencerName, sequencingEventInfo.get(Event.Sequencing.SEQUENCER_NAME)); Assert.assertEquals(sequencedNode.getIdentifier(), sequencingEventInfo.get(Event.Sequencing.SEQUENCED_NODE_ID)); Assert.assertEquals(sequencedNode.getPath(), sequencingEventInfo.get(Event.Sequencing.SEQUENCED_NODE_PATH)); Assert.assertEquals(expectedSelectedPath, sequencingEventInfo.get(Event.Sequencing.SELECTED_PATH)); Assert.assertEquals(expectedOutputPath, sequencingEventInfo.get(Event.Sequencing.OUTPUT_PATH)); return sequencingEventInfo; } protected final class SequencingListener implements EventListener { @SuppressWarnings( "synthetic-access" ) @Override public void onEvent( EventIterator events ) { while (events.hasNext()) { try { Event event = (Event)events.nextEvent(); smokeCheckSequencingEvent(event, NODE_SEQUENCED, SEQUENCED_NODE_ID, SEQUENCED_NODE_PATH, OUTPUT_PATH, SELECTED_PATH, SEQUENCER_NAME, USER_ID); sequencingEvents.putIfAbsent((String)event.getInfo().get(SEQUENCED_NODE_PATH), event); String nodePath = event.getPath(); logger.debug("New sequenced node at: " + nodePath); try { Node node = session.getNode(nodePath); sequencedNodes.putIfAbsent(nodePath, node); } catch (PathNotFoundException e) { logger.debug("Node at {0} removed by another thread", nodePath); } // signal the node is available createWaitingLatchIfNecessary(nodePath, nodeSequencedLatches); nodeSequencedLatches.get(nodePath).countDown(); } catch (Exception e) { throw new RuntimeException(e); } } } } protected final class SequencingFailureListener implements EventListener { @SuppressWarnings( "synthetic-access" ) @Override public void onEvent( EventIterator events ) { while (events.hasNext()) { try { Event event = (Event)events.nextEvent(); smokeCheckSequencingEvent(event, NODE_SEQUENCING_FAILURE, SEQUENCED_NODE_ID, SEQUENCED_NODE_PATH, Event.Sequencing.SEQUENCING_FAILURE_CAUSE, OUTPUT_PATH, SELECTED_PATH, SEQUENCER_NAME, USER_ID); String nodePath = event.getPath(); sequencingEvents.putIfAbsent(nodePath, event); createWaitingLatchIfNecessary(nodePath, sequencingFailureLatches); sequencingFailureLatches.get(nodePath).countDown(); } catch (Exception e) { throw new RuntimeException(e); } } } } }