Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #548 from fcrepo4/tombstones
Add Tombstone resource when a resource is deleted, preventing the re-use of URIs until the tombstone is also removed

Resolves: https://www.pivotaltracker.com/story/show/80853212
  • Loading branch information
Andrew Woods committed Oct 18, 2014
2 parents 816ffa6 + 82bb176 commit fd73096
Show file tree
Hide file tree
Showing 28 changed files with 655 additions and 34 deletions.
17 changes: 11 additions & 6 deletions fcrepo-http-api/pom.xml
Expand Up @@ -72,12 +72,6 @@
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring3</artifactId>
<scope>test</scope>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-common</artifactId>
Expand Down Expand Up @@ -118,10 +112,21 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring3</artifactId>
<scope>test</scope>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
Expand Down
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,16 +22,17 @@
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;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.UUID;

import com.hp.hpl.jena.update.GraphStore;

import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
Expand All @@ -43,18 +44,18 @@
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.Before;
Expand All @@ -63,6 +64,8 @@
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.hp.hpl.jena.update.GraphStore;

/**
* <p>Abstract AbstractResourceIT class.</p>
*
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 @@ -60,6 +60,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 comments on commit fd73096

Please sign in to comment.