Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
18 changed files
with
1,462 additions
and
20 deletions.
There are no files selected for viewing
170 changes: 170 additions & 0 deletions
170
fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLocks.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
143
fcrepo-http-api/src/test/java/org/fcrepo/http/api/FedoraLocksTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.