You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by ju...@apache.org on 2011/03/04 15:29:09 UTC
svn commit: r1077966 - in /jackrabbit/trunk:
jackrabbit-core/src/test/java/org/apache/jackrabbit/core/
jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/
Author: jukka
Date: Fri Mar 4 14:29:09 2011
New Revision: 1077966
URL: http://svn.apache.org/viewvc?rev=1077966&view=rev
Log:
JCR-2874: Locked helper class improvements
Patch by Alex Parvulescu
Added:
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java
jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java
Modified:
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java
Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java?rev=1077966&r1=1077965&r2=1077966&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java Fri Mar 4 14:29:09 2011
@@ -29,7 +29,8 @@ import java.util.Iterator;
import java.util.Random;
/**
- * <code>LockTest</code> tests the utility {@link org.apache.jackrabbit.util.Locked}.
+ * <code>LockTest</code> tests the utility
+ * {@link org.apache.jackrabbit.util.Locked}.
*/
public class LockTest extends AbstractJCRTest {
@@ -41,40 +42,43 @@ public class LockTest extends AbstractJC
/**
* Tests the utility {@link org.apache.jackrabbit.util.Locked} by
- * implementing running multiple threads concurrently that apply changes
- * to a lockable node.
+ * implementing running multiple threads concurrently that apply changes to
+ * a lockable node.
*/
public void testLockUtility() throws RepositoryException {
final Node lockable = testRootNode.addNode(nodeName1);
lockable.addMixin(mixLockable);
- testRootNode.save();
+ superuser.save();
- final List worker = new ArrayList();
+ final List<Thread> worker = new ArrayList<Thread>();
for (int i = 0; i < NUM_THREADS; i++) {
worker.add(new Thread() {
private final int threadNumber = worker.size();
public void run() {
- Session s;
+ final Session s;
try {
s = getHelper().getSuperuserSession();
} catch (RepositoryException e) {
+ fail(e.getMessage());
return;
}
try {
for (int i = 0; i < NUM_CHANGES; i++) {
Node n = (Node) s.getItem(lockable.getPath());
new Locked() {
- protected Object run(Node n) throws RepositoryException {
+ protected Object run(Node n)
+ throws RepositoryException {
String nodeName = "node" + threadNumber;
if (n.hasNode(nodeName)) {
n.getNode(nodeName).remove();
} else {
n.addNode(nodeName);
}
- n.save();
- log.println("Thread" + threadNumber + ": saved modification");
+ s.save();
+ log.println("Thread" + threadNumber
+ + ": saved modification");
return null;
}
@@ -83,9 +87,11 @@ public class LockTest extends AbstractJC
Thread.sleep(new Random().nextInt(100));
}
} catch (RepositoryException e) {
- log.println("exception while running code with lock:" + e.getMessage());
+ log.println("exception while running code with lock:"
+ + e.getMessage());
} catch (InterruptedException e) {
- log.println(Thread.currentThread() + " interrupted while waiting for lock");
+ log.println(Thread.currentThread()
+ + " interrupted while waiting for lock");
} finally {
s.logout();
}
@@ -93,13 +99,13 @@ public class LockTest extends AbstractJC
});
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
- ((Thread) it.next()).start();
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
+ it.next().start();
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
try {
- ((Thread) it.next()).join();
+ it.next().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
@@ -114,41 +120,46 @@ public class LockTest extends AbstractJC
final Node counter = testRootNode.addNode(nodeName1);
counter.setProperty("value", 0);
counter.addMixin(mixLockable);
- testRootNode.save();
+ superuser.save();
- final List worker = new ArrayList();
+ final List<Thread> worker = new ArrayList<Thread>();
for (int i = 0; i < NUM_THREADS; i++) {
worker.add(new Thread() {
private final int threadNumber = worker.size();
public void run() {
- Session s;
+ final Session s;
try {
s = getHelper().getSuperuserSession();
} catch (RepositoryException e) {
+ fail(e.getMessage());
return;
}
try {
for (int i = 0; i < NUM_VALUE_GETS; i++) {
Node n = (Node) s.getItem(counter.getPath());
long currentValue = ((Long) new Locked() {
- protected Object run(Node n) throws RepositoryException {
+ protected Object run(Node n)
+ throws RepositoryException {
Property seqProp = n.getProperty("value");
long value = seqProp.getLong();
seqProp.setValue(++value);
- seqProp.save();
+ s.save();
return new Long(value);
}
}.with(n, false)).longValue();
- log.println("Thread" + threadNumber + ": got sequence number: " + currentValue);
+ log.println("Thread" + threadNumber
+ + ": got sequence number: " + currentValue);
// do a random wait
Thread.sleep(new Random().nextInt(100));
}
} catch (RepositoryException e) {
- log.println("exception while running code with lock:" + e.getMessage());
+ log.println("exception while running code with lock:"
+ + e.getMessage());
} catch (InterruptedException e) {
- log.println(Thread.currentThread() + " interrupted while waiting for lock");
+ log.println(Thread.currentThread()
+ + " interrupted while waiting for lock");
} finally {
s.logout();
}
@@ -156,13 +167,13 @@ public class LockTest extends AbstractJC
});
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
- ((Thread) it.next()).start();
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
+ it.next().start();
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
try {
- ((Thread) it.next()).join();
+ it.next().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
@@ -180,46 +191,55 @@ public class LockTest extends AbstractJC
final Node counter = testRootNode.addNode(nodeName1);
counter.setProperty("value", 0);
counter.addMixin(mixLockable);
- testRootNode.save();
+ superuser.save();
- final List worker = new ArrayList();
+ final List<Thread> worker = new ArrayList<Thread>();
for (int i = 0; i < NUM_THREADS; i++) {
worker.add(new Thread() {
private final int threadNumber = worker.size();
public void run() {
- Session s;
+ final Session s;
try {
s = getHelper().getSuperuserSession();
} catch (RepositoryException e) {
+ fail(e.getMessage());
return;
}
try {
for (int i = 0; i < NUM_VALUE_GETS; i++) {
Node n = (Node) s.getItem(counter.getPath());
Object ret = new Locked() {
- protected Object run(Node n) throws RepositoryException {
+ protected Object run(Node n)
+ throws RepositoryException {
Property seqProp = n.getProperty("value");
long value = seqProp.getLong();
seqProp.setValue(++value);
- seqProp.save();
+ s.save();
return new Long(value);
}
- }.with(n, false, 10 * 1000); // expect a value after ten seconds
+ }.with(n, false, 10 * 1000); // expect a value after
+ // ten seconds
if (ret == Locked.TIMED_OUT) {
- log.println("Thread" + threadNumber + ": could not get a sequence number within 10 seconds");
+ log.println("Thread"
+ + threadNumber
+ + ": could not get a sequence number within 10 seconds");
} else {
long currentValue = ((Long) ret).longValue();
- log.println("Thread" + threadNumber + ": got sequence number: " + currentValue);
+ log.println("Thread" + threadNumber
+ + ": got sequence number: "
+ + currentValue);
}
// do a random wait
Thread.sleep(new Random().nextInt(100));
}
} catch (RepositoryException e) {
- log.println("exception while running code with lock:" + e.getMessage());
+ log.println("exception while running code with lock:"
+ + e.getMessage());
} catch (InterruptedException e) {
- log.println(Thread.currentThread() + " interrupted while waiting for lock");
+ log.println(Thread.currentThread()
+ + " interrupted while waiting for lock");
} finally {
s.logout();
}
@@ -227,13 +247,13 @@ public class LockTest extends AbstractJC
});
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
- ((Thread) it.next()).start();
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
+ it.next().start();
}
- for (Iterator it = worker.iterator(); it.hasNext(); ) {
+ for (Iterator<Thread> it = worker.iterator(); it.hasNext();) {
try {
- ((Thread) it.next()).join();
+ it.next().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Added: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java?rev=1077966&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java Fri Mar 4 14:29:09 2011
@@ -0,0 +1,335 @@
+/*
+ * 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.jackrabbit.core;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.lock.LockException;
+
+import org.apache.jackrabbit.test.AbstractJCRTest;
+import org.apache.jackrabbit.util.LockedWrapper;
+
+/**
+ * tests for the utility class {@link org.apache.jackrabbit.util.LockedWrapper}.
+ */
+public class LockedWrapperTest extends AbstractJCRTest {
+
+ private static final int NUM_THREADS = 100;
+
+ private static final int NUM_CHANGES = 10;
+
+ private static final int NUM_VALUE_GETS = 10;
+
+ private final Random random = new Random();
+
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ /**
+ * Tests the utility {@link org.apache.jackrabbit.util.Locked} by
+ * implementing running multiple threads concurrently that apply changes to
+ * a lockable node.
+ */
+ public void testConcurrentUpdates() throws RepositoryException,
+ InterruptedException {
+
+ final Node lockable = testRootNode.addNode(nodeName1);
+ lockable.addMixin(mixLockable);
+ superuser.save();
+
+ List<Future<?>> futures = new ArrayList<Future<?>>();
+ ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS);
+ for (int i = 0; i < NUM_THREADS; i++) {
+ futures.add(e.submit(buildNewConcurrentUpdateCallable(i,
+ lockable.getPath(), false)));
+ }
+ e.shutdown();
+ assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS));
+
+ for (Future<?> future : futures) {
+ try {
+ future.get();
+ } catch (ExecutionException ex) {
+ fail(ex.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Tests the utility {@link org.apache.jackrabbit.util.Locked} by
+ * implementing running multiple threads concurrently that apply changes to
+ * a lockable node, also refreshing the session on each operation
+ */
+ public void testConcurrentUpdatesWithSessionRefresh()
+ throws RepositoryException, InterruptedException {
+
+ final Node lockable = testRootNode.addNode(nodeName1);
+ lockable.addMixin(mixLockable);
+ superuser.save();
+
+ List<Future<?>> futures = new ArrayList<Future<?>>();
+ ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS);
+ for (int i = 0; i < NUM_THREADS; i++) {
+ futures.add(e.submit(buildNewConcurrentUpdateCallable(i,
+ lockable.getPath(), true)));
+ }
+ e.shutdown();
+ assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS));
+
+ for (Future<?> future : futures) {
+ try {
+ future.get();
+ } catch (ExecutionException ex) {
+ fail(ex.getMessage());
+ }
+ }
+ }
+
+ public Callable<Void> buildNewConcurrentUpdateCallable(
+ final int threadNumber, final String lockablePath,
+ final boolean withSessionRefresh) {
+
+ return new Callable<Void>() {
+
+ public Void call() throws RepositoryException, InterruptedException {
+
+ final Session s = getHelper().getSuperuserSession();
+
+ try {
+
+ for (int i = 0; i < NUM_CHANGES; i++) {
+
+ if (withSessionRefresh) {
+ s.refresh(false);
+ }
+ Node n = s.getNode(lockablePath);
+
+ new LockedWrapper<Void>() {
+ @Override
+ protected Void run(Node node)
+ throws RepositoryException {
+ String nodeName = "node" + threadNumber;
+ if (node.hasNode(nodeName)) {
+ node.getNode(nodeName).remove();
+ } else {
+ node.addNode(nodeName);
+ }
+ s.save();
+ log.println("Thread" + threadNumber
+ + ": saved modification");
+ return null;
+ }
+ }.with(n, false);
+
+ // do a random wait
+ Thread.sleep(random.nextInt(100));
+ }
+ } finally {
+ s.logout();
+ }
+ return null;
+ }
+
+ };
+ }
+
+ public void testTimeout() throws RepositoryException, InterruptedException {
+
+ final Node lockable = testRootNode.addNode("testTimeout"
+ + System.currentTimeMillis());
+ lockable.addMixin(mixLockable);
+ superuser.save();
+
+ ExecutorService e = Executors.newFixedThreadPool(2);
+ Future<?> lockMaster = e.submit(buildNewTimeoutCallable(
+ lockable.getPath(), true));
+ Future<?> lockSlave = e.submit(buildNewTimeoutCallable(
+ lockable.getPath(), false));
+ e.shutdown();
+
+ try {
+ lockMaster.get();
+ lockSlave.get();
+ } catch (ExecutionException ex) {
+ if (ex.getCause().getClass().isAssignableFrom(LockException.class)) {
+ return;
+ }
+ fail(ex.getMessage());
+ }
+ fail("was expecting a LockException");
+ }
+
+ public Callable<Void> buildNewTimeoutCallable(final String lockablePath,
+ final boolean isLockMaster) {
+
+ return new Callable<Void>() {
+
+ public Void call() throws RepositoryException, InterruptedException {
+
+ // allow master to be first to obtain the lock on the node
+ if (!isLockMaster) {
+ TimeUnit.SECONDS.sleep(2);
+ }
+
+ final Session s = getHelper().getSuperuserSession();
+
+ try {
+ Node n = s.getNode(lockablePath);
+
+ new LockedWrapper<Void>() {
+
+ @Override
+ protected Void run(Node node)
+ throws RepositoryException {
+
+ if (isLockMaster) {
+ try {
+ TimeUnit.SECONDS.sleep(15);
+ } catch (InterruptedException e) {
+ //
+ }
+ }
+ return null;
+ }
+ }.with(n, false, 2000);
+
+ } finally {
+ s.logout();
+ }
+ return null;
+ }
+ };
+ }
+
+ /**
+ * Tests concurrent updates on a persistent counter
+ */
+ public void testSequence() throws RepositoryException, InterruptedException {
+
+ counter = new AtomicInteger(0);
+
+ final Node lockable = testRootNode.addNode(nodeName1);
+ lockable.setProperty("value", counter.getAndIncrement());
+ lockable.addMixin(mixLockable);
+ superuser.save();
+
+ List<Future<Long>> futures = new ArrayList<Future<Long>>();
+ ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS);
+ for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) {
+ futures.add(e.submit(buildNewSequenceUpdateCallable(
+ lockable.getPath(), false)));
+ }
+ e.shutdown();
+ assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS));
+
+ for (Future<Long> future : futures) {
+ try {
+ Long v = future.get();
+ log.println("Got sequence number: " + v);
+ } catch (ExecutionException ex) {
+ fail(ex.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Tests concurrent updates on a persistent counter, with session refresh
+ */
+ public void testSequenceWithSessionRefresh() throws RepositoryException,
+ InterruptedException {
+
+ counter = new AtomicInteger(0);
+
+ final Node lockable = testRootNode.addNode(nodeName1);
+ lockable.setProperty("value", counter.getAndIncrement());
+ lockable.addMixin(mixLockable);
+ superuser.save();
+
+ List<Future<Long>> futures = new ArrayList<Future<Long>>();
+ ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS);
+ for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) {
+ futures.add(e.submit(buildNewSequenceUpdateCallable(
+ lockable.getPath(), true)));
+ }
+ e.shutdown();
+ assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS));
+
+ for (Future<Long> future : futures) {
+ try {
+ Long v = future.get();
+ log.println("Got sequence number: " + v);
+ } catch (ExecutionException ex) {
+ fail(ex.getMessage());
+ }
+ }
+ }
+
+ public Callable<Long> buildNewSequenceUpdateCallable(
+ final String counterPath, final boolean withSessionRefresh) {
+
+ return new Callable<Long>() {
+
+ public Long call() throws RepositoryException, InterruptedException {
+
+ final Session s = getHelper().getSuperuserSession();
+
+ try {
+ if (withSessionRefresh) {
+ s.refresh(false);
+ }
+ Node n = s.getNode(counterPath);
+
+ long value = new LockedWrapper<Long>() {
+
+ @Override
+ protected Long run(Node node)
+ throws RepositoryException {
+
+ Property seqProp = node.getProperty("value");
+ long value = seqProp.getLong();
+ seqProp.setValue(++value);
+ s.save();
+ return value;
+ }
+ }.with(n, false);
+
+ // check that the sequence is ok
+ assertEquals(counter.getAndIncrement(), value);
+
+ // do a random wait
+ Thread.sleep(random.nextInt(100));
+
+ return value;
+ } finally {
+ s.logout();
+ }
+ }
+ };
+ }
+}
Added: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java?rev=1077966&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java (added)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java Fri Mar 4 14:29:09 2011
@@ -0,0 +1,107 @@
+/*
+ * 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.jackrabbit.util;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.lock.LockException;
+
+/**
+ * <code>LockedWrapper</code> is a wrapper class to {@link Locked} which adds
+ * generics support and wraps the <code>Locked.TIMED_OUT</code> object into a
+ * {@link LockException}.
+ */
+public abstract class LockedWrapper<T> extends Locked {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T with(Node lockable, boolean isDeep) throws RepositoryException,
+ InterruptedException {
+ return (T) super.with(lockable, isDeep);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean,
+ * boolean)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T with(Node lockable, boolean isDeep, boolean isSessionScoped)
+ throws RepositoryException, InterruptedException {
+ return (T) super.with(lockable, isDeep, isSessionScoped);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean,
+ * long)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T with(Node lockable, boolean isDeep, long timeout)
+ throws UnsupportedRepositoryOperationException,
+ RepositoryException, InterruptedException {
+
+ Object r = super.with(lockable, isDeep, timeout);
+ if (r == Locked.TIMED_OUT) {
+ throw new LockException("Node locked.");
+ }
+ return (T) r;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean,
+ * long, boolean)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object with(Node lockable, boolean isDeep, long timeout,
+ boolean isSessionScoped)
+ throws UnsupportedRepositoryOperationException,
+ RepositoryException, InterruptedException {
+
+ Object r = super.with(lockable, isDeep, timeout, isSessionScoped);
+ if (r == Locked.TIMED_OUT) {
+ throw new LockException("Node locked.");
+ }
+ return (T) r;
+ }
+
+ /**
+ * This method is executed while holding the lock.
+ *
+ * @param node
+ * The <code>Node</code> on which the lock is placed.
+ * @return an object which is then returned by {@link #with with()}.
+ * @throws RepositoryException
+ * if an error occurs.
+ */
+ @Override
+ protected abstract T run(Node node) throws RepositoryException;
+
+}
\ No newline at end of file