Skip to content

Commit

Permalink
Add Tombstone resource when a resource is deleted, preventing the re-…
Browse files Browse the repository at this point in the history
…use of URIs until the tombstone is also removed
  • Loading branch information
cbeer committed Oct 18, 2014
1 parent 58bdaa2 commit 82bb176
Show file tree
Hide file tree
Showing 23 changed files with 618 additions and 22 deletions.
Expand Up @@ -20,6 +20,8 @@
import org.fcrepo.http.commons.AbstractResource;
import org.fcrepo.http.commons.api.rdf.HttpResourceConverter;
import org.fcrepo.kernel.FedoraResource;
import org.fcrepo.kernel.Tombstone;
import org.fcrepo.kernel.exception.TombstoneException;
import org.fcrepo.kernel.identifiers.IdentifierConverter;
import org.slf4j.Logger;

Expand Down Expand Up @@ -59,9 +61,15 @@ protected IdentifierConverter<Resource, FedoraResource> translator() {
*/
@VisibleForTesting
public FedoraResource getResourceFromPath(final String externalPath) {
return translator().convert(translator().toDomain(externalPath));
}
final Resource resource = translator().toDomain(externalPath);
final FedoraResource fedoraResource = translator().convert(resource);

if (fedoraResource instanceof Tombstone) {
throw new TombstoneException(fedoraResource, resource.getURI() + "/fcr:tombstone");
}

return fedoraResource;
}

/**
* Set the baseURL for JMS events.
Expand Down
@@ -0,0 +1,91 @@
/**
* Copyright 2014 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.google.common.annotations.VisibleForTesting;
import org.fcrepo.kernel.FedoraResource;
import org.fcrepo.kernel.exception.RepositoryRuntimeException;
import org.springframework.context.annotation.Scope;

import javax.inject.Inject;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.ws.rs.DELETE;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;

import static javax.ws.rs.core.Response.noContent;

/**
* CRUD operations on Fedora tombstones
*
* @author cbeer
*/
@Scope("request")
@Path("/{path: .*}/fcr:tombstone")
public class FedoraTombstones extends FedoraBaseResource {

@Inject
protected Session session;

@PathParam("path") protected String externalPath;

/**
* Default JAX-RS entry point
*/
public FedoraTombstones() {
super();
}

/**
* Create a new FedoraNodes instance for a given path
* @param externalPath
*/
@VisibleForTesting
public FedoraTombstones(final String externalPath) {
this.externalPath = externalPath;
}


/**
* Delete a tombstone resource (freeing the original resource to be reused)
* @return
*/
@DELETE
public Response delete() {
resource().delete();

try {
session.save();
} catch (RepositoryException e) {
throw new RepositoryRuntimeException(e);
}

return noContent().build();
}

protected FedoraResource resource() {
return translator().convert(translator().toDomain(externalPath));
}


@Override
protected Session session() {
return session;
}

}
@@ -0,0 +1,68 @@
/**
* Copyright 2014 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 org.fcrepo.kernel.Tombstone;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import javax.jcr.Session;
import javax.ws.rs.core.Response;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.springframework.test.util.ReflectionTestUtils.setField;

/**
* @author cabeer
*/
public class FedoraTombstonesTest {

@Mock
private Tombstone mockResource;

private String path = "/test/object";

private FedoraTombstones testObj;

@Mock
private Session mockSession;

@Before
public void setUp() {
initMocks(this);
testObj = spy(new FedoraTombstones(path));
setField(testObj, "session", mockSession);
}

@Test
public void testDelete() throws Exception {
final Tombstone mockResource = mock(Tombstone.class);

doReturn(mockResource).when(testObj).resource();

final Response actual = testObj.delete();
assertEquals(NO_CONTENT.getStatusCode(), actual.getStatus());
verify(mockResource).delete();
verify(mockSession).save();
}
}
Expand Up @@ -22,7 +22,10 @@
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.Status.OK;
import static org.fcrepo.http.commons.test.util.TestHelpers.parseTriples;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -170,8 +173,7 @@ protected HttpResponse executeWithBasicAuth(final HttpUriRequest request,
}
}


protected int getStatus(final HttpUriRequest method)
protected static int getStatus(final HttpUriRequest method)
throws ClientProtocolException, IOException {
final HttpResponse response = execute(method);
final int result = response.getStatusLine().getStatusCode();
Expand Down Expand Up @@ -314,4 +316,7 @@ protected static String getRandomPropertyValue() {
return randomUUID().toString();
}

protected static void assertDeleted(final String location) throws IOException {
assertThat("Expected object to be deleted", getStatus(new HttpGet(location)), anyOf(is(404), is(410)));
}
}
Expand Up @@ -15,7 +15,6 @@
*/
package org.fcrepo.integration.http.api;


import static com.google.common.io.Files.createTempDir;
import static java.util.UUID.randomUUID;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -76,8 +75,7 @@ public void shouldRoundTripBackups() throws Exception {
assertEquals(204, response.getStatusLine().getStatusCode());

// Verify object removed
response = execute(new HttpGet(serverAddress + objName));
assertEquals(404, response.getStatusLine().getStatusCode());
assertDeleted(serverAddress + objName);

// try to restore it
final HttpPost restoreMethod =
Expand Down
Expand Up @@ -59,8 +59,9 @@ public void shouldRoundTripOneObject() throws IOException {

// delete it
execute(new HttpDelete(serverAddress + objName));
response = execute(new HttpGet(serverAddress + objName));
assertEquals(404, response.getStatusLine().getStatusCode());
assertDeleted(serverAddress + objName);
final HttpResponse tombstoneResponse = execute(new HttpDelete(serverAddress + objName + "/fcr:tombstone"));
assertEquals(204, tombstoneResponse.getStatusLine().getStatusCode());

// try to import it
final HttpPost importMethod = new HttpPost(serverAddress + "fcr:import");
Expand Down Expand Up @@ -96,8 +97,8 @@ public void shouldRoundTripOneObject() throws IOException {

// delete it
execute(new HttpDelete(serverAddress + objName));
response = execute(new HttpGet(serverAddress + objName));
assertEquals(404, response.getStatusLine().getStatusCode());
assertDeleted(serverAddress + objName);
execute(new HttpDelete(serverAddress + objName + "/fcr:tombstone"));

// try to import it
final HttpPost importMethod = new HttpPost(serverAddress + "fcr:import");
Expand Down
Expand Up @@ -59,6 +59,7 @@
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;

import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Variant;

Expand Down Expand Up @@ -408,11 +409,9 @@ public void testDeleteObject() throws Exception {

final String location = serverAddress + pid;
assertEquals(204, getStatus(new HttpDelete(location)));
assertEquals("Object wasn't really deleted!", 404,
getStatus(new HttpGet(location)));
assertDeleted(location);
}


@Test
public void testDeleteBinary() throws Exception {
final String pid = getRandomUniquePid();
Expand All @@ -421,10 +420,29 @@ public void testDeleteBinary() throws Exception {

final String location = serverAddress + pid + "/x";
assertEquals(204, getStatus(new HttpDelete(location)));
assertEquals("Object wasn't really deleted!", 404,
getStatus(new HttpGet(location)));
assertDeleted(location);
}

@Test
public void testDeleteObjectAndTombstone() throws Exception {
final String pid = getRandomUniquePid();

createObject(pid);

final String location = serverAddress + pid;
assertEquals(204, getStatus(new HttpDelete(location)));
assertDeleted(location);
final HttpGet httpGet = new HttpGet(location);
final HttpResponse response = execute(httpGet);
final Link tombstone = Link.valueOf(response.getFirstHeader("Link").getValue());

assertEquals("hasTombstone", tombstone.getRel());

final HttpResponse tombstoneResponse = execute(new HttpDelete(tombstone.getUri()));
assertEquals(204, tombstoneResponse.getStatusLine().getStatusCode());

assertEquals(404, getStatus(httpGet));
}

@Test
public void testEmptyPatch() throws Exception {
Expand Down Expand Up @@ -898,9 +916,7 @@ public void testDeleteDatastream() throws Exception {
new HttpDelete(serverAddress + pid + "/ds1");
assertEquals(204, getStatus(dmethod));

final HttpGet method_test_get =
new HttpGet(serverAddress + pid + "/ds1");
assertEquals(404, getStatus(method_test_get));
assertDeleted(serverAddress + pid + "/ds1");
}

@Test
Expand Down
@@ -0,0 +1,44 @@
/**
* Copyright 2014 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.commons.exceptionhandlers;

import org.fcrepo.kernel.exception.TombstoneException;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import static javax.ws.rs.core.Response.Status.GONE;
import static javax.ws.rs.core.Response.status;

/**
* @author cabeer
* @since 10/16/14
*/
@Provider
public class TombstoneExceptionMapper implements ExceptionMapper<TombstoneException> {
@Override
public Response toResponse(final TombstoneException e) {
final Response.ResponseBuilder response = status(GONE)
.entity(e.getMessage());

if (e.getURI() != null) {
response.link(e.getURI(), "hasTombstone");
}

return response.build();
}
}

0 comments on commit 82bb176

Please sign in to comment.