Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement basic node locking.
- Add support for deep locking.

Resolves: https://www.pivotaltracker.com/story/show/66093788
  • Loading branch information
Michael Durbin authored and Andrew Woods committed Apr 18, 2014
1 parent 8959c92 commit a766c91
Show file tree
Hide file tree
Showing 18 changed files with 1,462 additions and 20 deletions.
170 changes: 170 additions & 0 deletions fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLocks.java
@@ -0,0 +1,170 @@
/**
* Copyright 2013 DuraSpace, Inc.
*
* 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.fcrepo.http.api;

import com.codahale.metrics.annotation.Timed;
import com.hp.hpl.jena.datatypes.xsd.XSDDatatype;
import com.hp.hpl.jena.graph.Triple;
import org.fcrepo.http.commons.AbstractResource;
import org.fcrepo.http.commons.api.rdf.HttpIdentifierTranslator;
import org.fcrepo.http.commons.session.InjectedSession;
import org.fcrepo.jcr.FedoraJcrTypes;
import org.fcrepo.kernel.Lock;
import org.fcrepo.kernel.utils.iterators.RdfStream;
import org.slf4j.Logger;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import static com.hp.hpl.jena.graph.NodeFactory.createLiteral;
import static com.hp.hpl.jena.graph.NodeFactory.createURI;
import static com.hp.hpl.jena.graph.Triple.create;
import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.noContent;
import static org.fcrepo.http.commons.domain.RDFMediaType.N3;
import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2;
import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE;
import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X;
import static org.fcrepo.kernel.RdfLexicon.HAS_LOCK_TOKEN;
import static org.fcrepo.kernel.RdfLexicon.IS_DEEP;
import static org.fcrepo.kernel.RdfLexicon.LOCKS;
import static org.slf4j.LoggerFactory.getLogger;

/**
* @author Mike Durbin
*/
@Component
@Scope("prototype")
@Path("/{path: .*}/fcr:lock")
public class FedoraLocks extends AbstractResource implements FedoraJcrTypes {

private static final Logger LOGGER = getLogger(FedoraLocks.class);

@InjectedSession
protected Session session;

/**
* Gets a description of the lock resource.
*/
@GET
@Produces({TURTLE, N3, N3_ALT2, RDF_XML, NTRIPLES, APPLICATION_XML, TEXT_PLAIN, TURTLE_X,
TEXT_HTML, APPLICATION_XHTML_XML})
public RdfStream getLock(@PathParam("path") final List<PathSegment> pathList) throws RepositoryException {

final String path = toPath(pathList);
final Node node = session.getNode(path);
final Lock lock = lockService.getLock(session, path);
return getLockRdfStream(node, lock);
}

/**
* Creates a lock at the given path that is tied to the current session.
* Only the current session may make changes to the current path while
* the lock is held.
* @param timeout a number of seconds before which the lock will not be removed
* by the system. (note, this is just a hint at the expected
* lifespan of the lock, the system makes no guarantees that it
* will actually be removed)
* @param isDeep if true the created lock will affect all nodes in the subgraph
* below
*/
@POST
@Timed
public Response createLock(@PathParam("path") final List<PathSegment> pathList,
@QueryParam("timeout") @DefaultValue("-1") final long timeout,
@QueryParam("deep") @DefaultValue("false") final boolean isDeep)
throws RepositoryException, URISyntaxException {
try {
final String path = toPath(pathList);
final Node node = session.getNode(path);
final Lock lock = lockService.acquireLock(session, path, timeout, isDeep);
session.save();
final String location = getTranslator().getSubject(node.getPath()).getURI();
LOGGER.debug("Locked {} with lock token {}.", path, lock.getLockToken());
return created(new URI(location)).entity(location).header("Lock-Token", lock.getLockToken()).build();
} finally {
session.logout();
}
}

/**
* Deletes a lock at the given path, freeing up other sessions to make
* changes to it or its descendants.
* @return
*/
@DELETE
@Timed
public Response deleteLock(@PathParam("path") final List<PathSegment> pathList)
throws RepositoryException, URISyntaxException {
try {
final String path = toPath(pathList);
lockService.releaseLock(session, path);
session.save();
LOGGER.debug("Unlocked {}.", path);
return noContent().build();
} finally {
session.logout();
}
}

private HttpIdentifierTranslator getTranslator() {
return new HttpIdentifierTranslator(session, FedoraNodes.class, uriInfo);
}

private RdfStream getLockRdfStream(Node node, Lock lock) throws RepositoryException {
HttpIdentifierTranslator translator = getTranslator();
final com.hp.hpl.jena.graph.Node nodeSubject = translator.getSubject(node.getPath()).asNode();
final com.hp.hpl.jena.graph.Node lockSubject = createURI(nodeSubject.getURI() + "/" + FCR_LOCK);

final Triple[] lockTriples;
final Triple locksT = create(lockSubject, LOCKS.asNode(), nodeSubject);
final Triple isDeepT = create(lockSubject, IS_DEEP.asNode(),
createLiteral("false", "", XSDDatatype.XSDboolean));
if (lock.getLockToken() != null) {
lockTriples = new Triple[] {
locksT,
isDeepT,
create(lockSubject, HAS_LOCK_TOKEN.asNode(), createLiteral(lock.getLockToken()))};
} else {
lockTriples = new Triple[] {
locksT, isDeepT };
}
return new RdfStream(lockTriples).topic(lockSubject).session(session);
}

}
143 changes: 143 additions & 0 deletions fcrepo-http-api/src/test/java/org/fcrepo/http/api/FedoraLocksTest.java
@@ -0,0 +1,143 @@
/**
* Copyright 2013 DuraSpace, Inc.
*
* 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.fcrepo.http.api;

import com.hp.hpl.jena.graph.Triple;
import org.fcrepo.kernel.Lock;
import org.fcrepo.kernel.services.LockService;
import org.fcrepo.kernel.utils.iterators.RdfStream;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URISyntaxException;
import java.util.UUID;

import static org.fcrepo.http.commons.test.util.PathSegmentImpl.createPathList;
import static org.fcrepo.http.commons.test.util.TestHelpers.getUriInfoImpl;
import static org.fcrepo.http.commons.test.util.TestHelpers.mockSession;
import static org.fcrepo.http.commons.test.util.TestHelpers.setField;
import static org.fcrepo.kernel.RdfLexicon.HAS_LOCK_TOKEN;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

/**
* @author Mike Durbin
*/
public class FedoraLocksTest {

private static final long timeout = 300;

FedoraLocks testObj;

@Mock
private LockService mockLockService;

@Mock
private Lock mockLock;

@Mock
private Node mockNode;

@Mock
private NodeType mockNodeType;

Session mockSession;

private UriInfo mockUriInfo;

@Before
public void setUp() throws Exception {
initMocks(this);
testObj = new FedoraLocks();
setField(testObj, "lockService", mockLockService);
this.mockUriInfo = getUriInfoImpl();
mockSession = mockSession(testObj);
setField(testObj, "session", mockSession);
when(mockLock.getLockToken()).thenReturn("token");
}

@Test
public void testGetLock() throws RepositoryException {
final String pid = UUID.randomUUID().toString();
final String path = "/" + pid;
initializeMockNode(path);
when(mockLockService.getLock(mockSession, path)).thenReturn(mockLock);

testObj.getLock(createPathList(pid));

verify(mockLockService).getLock(mockSession, path);
}

@Test
public void testCreateLock() throws RepositoryException, URISyntaxException {
final String pid = UUID.randomUUID().toString();
final String path = "/" + pid;
initializeMockNode(path);
when(mockLockService.acquireLock(mockSession, path, timeout, false)).thenReturn(mockLock);

final Response response = testObj.createLock(createPathList(pid), timeout, false);

verify(mockLockService).acquireLock(mockSession, path, timeout, false);
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}

@Test
public void testDeleteLock() throws RepositoryException, URISyntaxException {
final String pid = UUID.randomUUID().toString();
final String path = "/" + pid;
initializeMockNode(path);

final Response response = testObj.deleteLock(createPathList(pid));

verify(mockLockService).releaseLock(mockSession, path);
Assert.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}

@Test
public void testRDFGenerationForLockToken() throws RepositoryException {
final String pid = UUID.randomUUID().toString();
final String path = "/" + pid;
initializeMockNode(path);
when(mockLockService.getLock(mockSession, path)).thenReturn(mockLock);

RdfStream stream = testObj.getLock(createPathList(pid));
while (stream.hasNext()) {
Triple t = stream.next();
if (t.getPredicate().getURI().equals(HAS_LOCK_TOKEN.getURI())
&& t.getObject().getLiteralValue().equals(mockLock.getLockToken())) {
return;
}
}
fail("Unable to find the lock token in the returned RDF!");
}

private void initializeMockNode(String path) throws RepositoryException {
when(mockNode.getPath()).thenReturn(path);
when(mockSession.getNode(path)).thenReturn(mockNode);
}

}
Expand Up @@ -28,6 +28,7 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.UUID;

import com.hp.hpl.jena.update.GraphStore;
import org.apache.http.HttpResponse;
Expand Down Expand Up @@ -205,4 +206,27 @@ protected static void addMixin(final String pid, final String mixinUrl) throws I
.getStatusCode());
}

/**
* Gets a random (but valid) pid for use in testing. This pid
* is guaranteed to be unique within runs of this application.
*/
protected static String getRandomUniquePid() {
return UUID.randomUUID().toString();
}

/**
* Gets a random (but valid) property name for use in testing.
*/
protected static String getRandomPropertyName() {
return UUID.randomUUID().toString();
}

/**
* Gets a random (but valid) property value for use in testing.
*/
protected static String getRandomPropertyValue() {
return UUID.randomUUID().toString();
}


}

0 comments on commit a766c91

Please sign in to comment.