Skip to content

Commit

Permalink
Add support for version reversion
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Durbin authored and Andrew Woods committed Apr 10, 2014
1 parent 789823d commit 7df0138
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 21 deletions.
Expand Up @@ -19,6 +19,7 @@
import com.codahale.metrics.annotation.Timed;
import org.fcrepo.http.api.versioning.VersionAwareHttpGraphSubjects;
import org.fcrepo.http.commons.api.rdf.HttpGraphSubjects;
import org.fcrepo.http.commons.domain.PATCH;
import org.fcrepo.http.commons.responses.HtmlTemplate;
import org.fcrepo.http.commons.session.InjectedSession;
import org.fcrepo.http.commons.session.SessionFactory;
Expand Down Expand Up @@ -130,6 +131,29 @@ public Response addVersion(@PathParam("path")
return addVersion(toPath(pathList), label);
}

/**
* Reverts the resource at the given path to the version specified by
* the label.
* @param pathList
* @param label
* @return
* @throws RepositoryException
*/
@PATCH
@Path("/{label:.+}")
public Response revertToVersion(@PathParam("path") final List<PathSegment> pathList,
@PathParam("label") final String label) throws RepositoryException {
final String path = toPath(pathList);
LOGGER.info("Reverting {} to version {}.", path,
label);
try {
versionService.revertToVersion(session.getWorkspace(), path, label);
return noContent().build();
} finally {
session.logout();
}
}

/**
* Create a new version checkpoint with no label.
*/
Expand Down
Expand Up @@ -6,6 +6,9 @@
<span class="glyphicon glyphicon-warning-sign"></span>
This is a <strong>historic version</strong> and cannot be modified.
</div>
<form id="action_revert" method="PATCH" action="$uriInfo.getAbsolutePath().toString()" data-redirect-after-submit="$helpers.getVersionSubjectUrl($uriInfo, $topic)" >
<button type="submit" class="btn btn-primary">Revert to this Version</button>
</form>
#end

#if ($content != "")
Expand Down
12 changes: 12 additions & 0 deletions fcrepo-http-api/src/main/resources/views/common.js
Expand Up @@ -112,6 +112,7 @@ $(function() {
$('#action_import').submit(sendImport);
$('#action_cnd_update').submit(sendCndUpdate);
$('#action_sparql_select').submit(sendSparqlQuery);
$('#action_revert').submit(patchAndReload);

});

Expand All @@ -127,6 +128,17 @@ function submitAndFollowLocation() {
return false;
}

function patchAndReload() {
var $form = $(this);
var patchURI = $form.attr('action');

$.ajax({url: patchURI, type: "PATCH", data: "", success: function(data, textStatus, request) {
window.location = $form.attr('data-redirect-after-submit');
}, error: ajaxErrorHandler});

return false;
}

function submitAndRedirectToBase() {
var $form = $(this);

Expand Down
Expand Up @@ -23,15 +23,20 @@
import static org.fcrepo.http.commons.test.util.TestHelpers.setField;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import java.util.Collection;
import java.util.UUID;

import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
Expand Down Expand Up @@ -138,4 +143,26 @@ public void testAddVersionLabel() throws RepositoryException {
assertNotNull(response);
}

@Test
public void testRevertToVersion() throws RepositoryException {
final String pid = UUID.randomUUID().toString();
final String versionLabel = UUID.randomUUID().toString();
when(mockNodes.getObject(any(Session.class), anyString())).thenReturn(
mockResource);
final Response response = testObj.revertToVersion(createPathList(pid), versionLabel);
verify(mockVersions).revertToVersion(testObj.session.getWorkspace(), "/" + pid, versionLabel);
assertNotNull(response);
}

@Test (expected = PathNotFoundException.class)
public void testRevertToVersionFailure() throws RepositoryException {
final String pid = UUID.randomUUID().toString();
final String versionLabel = UUID.randomUUID().toString();
when(mockNodes.getObject(any(Session.class), anyString())).thenReturn(
mockResource);
doThrow(PathNotFoundException.class)
.when(mockVersions).revertToVersion(any(Workspace.class), anyString(), anyString());
testObj.revertToVersion(createPathList(pid), versionLabel);
}

}
Expand Up @@ -21,7 +21,6 @@
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.sparql.core.Quad;
import com.hp.hpl.jena.update.GraphStore;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
Expand All @@ -31,8 +30,6 @@
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import javax.ws.rs.core.Response;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
Expand All @@ -44,12 +41,13 @@
import static com.hp.hpl.jena.graph.NodeFactory.createLiteral;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
import static java.util.UUID.randomUUID;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static org.fcrepo.kernel.RdfLexicon.DC_TITLE;
import static org.fcrepo.kernel.RdfLexicon.HAS_PRIMARY_TYPE;
import static org.fcrepo.kernel.RdfLexicon.HAS_VERSION;
import static org.fcrepo.kernel.RdfLexicon.VERSIONING_POLICY;
import static org.fcrepo.kernel.RdfLexicon.MIX_NAMESPACE;
import static org.fcrepo.kernel.RdfLexicon.VERSIONING_POLICY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -79,7 +77,7 @@ public void testAddAndRetrieveVersion() throws Exception {
addMixin( pid, MIX_NAMESPACE + "versionable" );

logger.info("Setting a title");
patchLiteralProperty(serverAddress + pid, "http://purl.org/dc/elements/1.1/title", "First Title");
patchLiteralProperty(serverAddress + pid, DC_TITLE.getURI(), "First Title");

final GraphStore nodeResults = getContent(serverAddress + pid);
assertTrue("Should find original title", nodeResults.contains(Node.ANY, Node.ANY, DC_TITLE.asNode(), NodeFactory.createLiteral("First Title")));
Expand All @@ -88,7 +86,7 @@ public void testAddAndRetrieveVersion() throws Exception {
postObjectVersion(pid, "v0.0.1");

logger.info("Replacing the title");
patchLiteralProperty(serverAddress + pid, "http://purl.org/dc/elements/1.1/title", "Second Title");
patchLiteralProperty(serverAddress + pid, DC_TITLE.getURI(), "Second Title");

final GraphStore versionResults = getContent(serverAddress + pid + "/fcr:versions/v0.0.1");
logger.info("Got version profile:");
Expand Down Expand Up @@ -139,7 +137,7 @@ public void testCreateUnlabeledVersion() throws Exception {
addMixin( objId, MIX_NAMESPACE + "versionable" );

logger.info("Setting a title");
patchLiteralProperty(serverAddress + objId, "http://purl.org/dc/elements/1.1/title", "Example Title");
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), "Example Title");

logger.info("Posting an unlabeled version");
postObjectVersion(objId);
Expand All @@ -153,19 +151,19 @@ public void testCreateTwoVersionsWithSameLabel() throws Exception {
addMixin( objId, MIX_NAMESPACE + "versionable" );

logger.info("Setting a title");
patchLiteralProperty(serverAddress + objId, "http://purl.org/dc/elements/1.1/title", "First title");
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), "First title");

logger.info("posting a version with label \"label\"");
postObjectVersion(objId, "label");

logger.info("Resetting the title");
patchLiteralProperty(serverAddress + objId, "http://purl.org/dc/elements/1.1/title", "Second title");
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), "Second title");

logger.info("posting a version with label \"label\"");
postObjectVersion(objId, "label");

logger.info("Resetting the title");
patchLiteralProperty(serverAddress + objId, "http://purl.org/dc/elements/1.1/title", "Third title");
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), "Third title");

final GraphStore versionResults = getContent(serverAddress + objId + "/fcr:versions/label");
logger.info("Got version profile:");
Expand Down Expand Up @@ -288,6 +286,60 @@ public void testRepositoryWideAutoVersioning() throws IOException {
postNodeTypeCNDSnippet(defaultResource);
}

@Test
public void testInvalidVersionReversion() throws Exception {
final String objId = UUID.randomUUID().toString();
createObject(objId);
addMixin(objId, MIX_NAMESPACE + "versionable");
final HttpPatch patch = new HttpPatch(serverAddress + objId + "/fcr:versions/invalid-version-label");
execute(patch);
assertEquals(NOT_FOUND.getStatusCode(), getStatus(patch));
}

@Test
public void testVersionReversion() throws Exception {
final String objId = UUID.randomUUID().toString();

final Resource subject = createResource(serverAddress + objId);

final String title1 = "foo";
final String firstVersionLabel = "v1";
final String title2 = "bar";
final String secondVersionLabel = "v2";

createObject(objId);
addMixin(objId, MIX_NAMESPACE + "versionable");
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), title1);
postObjectVersion(objId, firstVersionLabel);

patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), title2);
postObjectVersion(objId, secondVersionLabel);

final GraphStore preRollback = getGraphStore(new HttpGet(serverAddress + objId));
assertTrue("First title must be present!", preRollback.contains(Node.ANY, subject.asNode(), DC_TITLE.asNode(),
NodeFactory.createLiteral(title1)));
assertTrue("Second title must be present!", preRollback.contains(Node.ANY, subject.asNode(), DC_TITLE.asNode(),
NodeFactory.createLiteral(title2)));

revertToVersion(objId, firstVersionLabel);

final GraphStore postRollback = getGraphStore(new HttpGet(serverAddress + objId));
assertTrue("First title must be present!", postRollback.contains(Node.ANY, subject.asNode(), DC_TITLE.asNode(),
NodeFactory.createLiteral(title1)));
assertFalse("Second title must NOT be present!", postRollback.contains(Node.ANY, subject.asNode(), DC_TITLE.asNode(),
NodeFactory.createLiteral(title2)));

/*
* Make the sure the node is checked out and able to be updated.
*
* Because the JCR concept of checked-out is something we don't
* intend to expose through Fedora in the future, the following
* line is simply to test that writes can be completed after a
* reversion.
*/
patchLiteralProperty(serverAddress + objId, DC_TITLE.getURI(), "additional change");
}

private void testDatastreamContentUpdatesCreateNewVersions(final String objName, final String dsName) throws IOException {
final String firstVersionText = "foo";
final String secondVersionText = "bar";
Expand Down Expand Up @@ -392,4 +444,9 @@ public void postVersion(final String path, final String label) throws IOExceptio
assertEquals(NO_CONTENT.getStatusCode(), getStatus(postVersion));
}

private void revertToVersion(String objId, String versionLabel) throws IOException {
final HttpPatch patch = new HttpPatch(serverAddress + objId + "/fcr:versions/" + versionLabel);
execute(patch);
assertEquals(NO_CONTENT.getStatusCode(), getStatus(patch));
}
}
Expand Up @@ -86,6 +86,25 @@ public Iterator<Quad> getObjects(final DatasetGraph dataset,
return dataset.find(ANY, subject, predicate.asNode(), ANY);
}

/**
* Gets the URL of the node whose version is represented by the
* current node. The current implementation assumes the URI
* of that node will be the same as the breadcrumb entry that
* precedes one with the path "fcr:versions".
*/
public String getVersionSubjectUrl(final UriInfo uriInfo,
final Node subject) {
Map<String, String> breadcrumbs = getNodeBreadcrumbs(uriInfo, subject);
String lastUrl = null;
for (Map.Entry<String, String> entry : breadcrumbs.entrySet()) {
if (entry.getValue().equals("fcr:versions")) {
return lastUrl;
}
lastUrl = entry.getKey();
}
return null;
}

/**
* Gets a version label of a subject from the graph
*
Expand Down
Expand Up @@ -108,6 +108,16 @@ public void testIsNotFrozenNode() {
assertFalse("Node is not a frozen node.", testObj.isFrozenNode(mem, createURI("a/b/c")));
}

@Test
public void shouldFindVersionRoot() {

final UriInfo mockUriInfo = getUriInfoImpl();

final String nodeUri = testObj.getVersionSubjectUrl(mockUriInfo, createResource(
"http://localhost/fcrepo/a/b/fcr:versions/c").asNode());
assertEquals("http://localhost/fcrepo/a/b", nodeUri);
}

@Test
public void testGetLabeledVersion() {
final DatasetGraph mem = createMem();
Expand Down
Expand Up @@ -63,6 +63,19 @@ void nodeUpdated(Session session, String absPath)
void createVersion(Workspace workspace, Collection<String> paths)
throws RepositoryException;

/**
* Reverts the node to the version identified by the label. This method
* will throw a PathNotFoundException if no version with the given label is
* found.
*
* @param workspace the workspace in which the node resides
* @param absPath the path to the node whose version is to be reverted
* @param label identifies the historic version
* @throws RepositoryException
*/
void revertToVersion(Workspace workspace, String absPath, String label)
throws RepositoryException;

/**
* Creates a version checkpoint for the given node if versioning is enabled
* for that node type. When versioning is enabled this is the equivalent of
Expand Down

0 comments on commit 7df0138

Please sign in to comment.