Skip to content

Commit 3e8de96

Browse files
authored
Merge pull request #682 from trellis-ldp/feature/configurable-versioning
Make versioning more configurable
2 parents 76ebd9f + c8dcdf5 commit 3e8de96

File tree

11 files changed

+177
-55
lines changed

11 files changed

+177
-55
lines changed

components/file/src/main/java/org/trellisldp/file/FileMementoService.java

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static java.time.temporal.ChronoUnit.SECONDS;
1717
import static java.util.Collections.emptySortedSet;
1818
import static java.util.Collections.unmodifiableSortedSet;
19+
import static java.util.concurrent.CompletableFuture.completedFuture;
1920
import static java.util.concurrent.CompletableFuture.runAsync;
2021
import static java.util.concurrent.CompletableFuture.supplyAsync;
2122
import static org.slf4j.LoggerFactory.getLogger;
@@ -33,6 +34,7 @@
3334

3435
import org.apache.commons.io.FilenameUtils;
3536
import org.apache.commons.rdf.api.IRI;
37+
import org.eclipse.microprofile.config.Config;
3638
import org.eclipse.microprofile.config.ConfigProvider;
3739
import org.slf4j.Logger;
3840
import org.trellisldp.api.MementoService;
@@ -47,25 +49,38 @@ public class FileMementoService implements MementoService {
4749
/** The configuration key controlling the base filesystem path for memento storage. */
4850
public static final String CONFIG_FILE_MEMENTO_PATH = "trellis.file.memento-path";
4951

52+
/** The configuration key controlling whether Memento versioning is enabled. */
53+
public static final String CONFIG_FILE_MEMENTO = "trellis.file.memento";
54+
5055
private static final Logger LOGGER = getLogger(FileMementoService.class);
5156

5257
private final File directory;
58+
private final boolean enabled;
5359

5460
/**
5561
* Create a file-based memento service.
5662
*/
5763
public FileMementoService() {
58-
this(ConfigProvider.getConfig().getValue(CONFIG_FILE_MEMENTO_PATH, String.class));
64+
this(ConfigProvider.getConfig());
65+
}
66+
67+
private FileMementoService(final Config config) {
68+
this(config.getValue(CONFIG_FILE_MEMENTO_PATH, String.class),
69+
config.getOptionalValue(CONFIG_FILE_MEMENTO, Boolean.class).orElse(Boolean.TRUE));
5970
}
6071

6172
/**
6273
* Create a file-based memento service.
6374
* @param path the file path
75+
* @param enabled whether memento handling is enabled
6476
*/
65-
public FileMementoService(final String path) {
66-
LOGGER.info("Storing Mementos as files at {}", path);
77+
public FileMementoService(final String path, final boolean enabled) {
6778
this.directory = new File(path);
68-
init();
79+
this.enabled = enabled;
80+
if (enabled) {
81+
LOGGER.info("Storing Mementos as files at {}", path);
82+
init();
83+
}
6984
}
7085

7186
@Override
@@ -80,42 +95,51 @@ public CompletionStage<Void> put(final Resource resource) {
8095
* @return the completion stage representing that the operation has completed
8196
*/
8297
public CompletionStage<Void> put(final Resource resource, final Instant time) {
83-
return runAsync(() -> {
84-
final File resourceDir = FileUtils.getResourceDirectory(directory, resource.getIdentifier());
85-
if (!resourceDir.exists()) {
86-
resourceDir.mkdirs();
87-
}
88-
FileUtils.writeMemento(resourceDir, resource, time.truncatedTo(SECONDS));
89-
});
98+
if (enabled) {
99+
return runAsync(() -> {
100+
final File resourceDir = FileUtils.getResourceDirectory(directory, resource.getIdentifier());
101+
if (!resourceDir.exists()) {
102+
resourceDir.mkdirs();
103+
}
104+
FileUtils.writeMemento(resourceDir, resource, time.truncatedTo(SECONDS));
105+
});
106+
}
107+
return completedFuture(null);
90108
}
91109

92110
@Override
93111
public CompletionStage<Resource> get(final IRI identifier, final Instant time) {
94-
return supplyAsync(() -> {
95-
final Instant mementoTime = time.truncatedTo(SECONDS);
96-
final File resourceDir = FileUtils.getResourceDirectory(directory, identifier);
97-
final File file = FileUtils.getNquadsFile(resourceDir, mementoTime);
98-
if (file.exists()) {
99-
return new FileResource(identifier, file);
100-
}
101-
final SortedSet<Instant> allMementos = listMementos(identifier);
102-
if (allMementos.isEmpty()) {
103-
return MISSING_RESOURCE;
104-
}
105-
final SortedSet<Instant> possible = allMementos.headSet(mementoTime);
106-
if (possible.isEmpty()) {
107-
// In this case, the requested Memento is earlier than the set of all existing Mementos.
108-
// Based on RFC 7089, Section 4.5.3 https://tools.ietf.org/html/rfc7089#section-4.5.3
109-
// the first extant memento should therefore be returned.
110-
return new FileResource(identifier, FileUtils.getNquadsFile(resourceDir, allMementos.first()));
111-
}
112-
return new FileResource(identifier, FileUtils.getNquadsFile(resourceDir, possible.last()));
113-
});
112+
if (enabled) {
113+
return supplyAsync(() -> {
114+
final Instant mementoTime = time.truncatedTo(SECONDS);
115+
final File resourceDir = FileUtils.getResourceDirectory(directory, identifier);
116+
final File file = FileUtils.getNquadsFile(resourceDir, mementoTime);
117+
if (file.exists()) {
118+
return new FileResource(identifier, file);
119+
}
120+
final SortedSet<Instant> allMementos = listMementos(identifier);
121+
if (allMementos.isEmpty()) {
122+
return MISSING_RESOURCE;
123+
}
124+
final SortedSet<Instant> possible = allMementos.headSet(mementoTime);
125+
if (possible.isEmpty()) {
126+
// In this case, the requested Memento is earlier than the set of all existing Mementos.
127+
// Based on RFC 7089, Section 4.5.3 https://tools.ietf.org/html/rfc7089#section-4.5.3
128+
// the first extant memento should therefore be returned.
129+
return new FileResource(identifier, FileUtils.getNquadsFile(resourceDir, allMementos.first()));
130+
}
131+
return new FileResource(identifier, FileUtils.getNquadsFile(resourceDir, possible.last()));
132+
});
133+
}
134+
return completedFuture(MISSING_RESOURCE);
114135
}
115136

116137
@Override
117138
public CompletionStage<SortedSet<Instant>> mementos(final IRI identifier) {
118-
return supplyAsync(() -> listMementos(identifier));
139+
if (enabled) {
140+
return supplyAsync(() -> listMementos(identifier));
141+
}
142+
return completedFuture(emptySortedSet());
119143
}
120144

121145
/**
@@ -126,13 +150,16 @@ public CompletionStage<SortedSet<Instant>> mementos(final IRI identifier) {
126150
* @return the next stage of completion
127151
*/
128152
public CompletionStage<Void> delete(final IRI identifier, final Instant time) {
129-
return runAsync(() -> {
130-
final File resourceDir = FileUtils.getResourceDirectory(directory, identifier);
131-
final File file = FileUtils.getNquadsFile(resourceDir, time.truncatedTo(SECONDS));
132-
if (FileUtils.uncheckedDeleteIfExists(file.toPath())) {
133-
LOGGER.debug("Deleted Memento {} at {}", identifier, file);
134-
}
135-
});
153+
if (enabled) {
154+
return runAsync(() -> {
155+
final File resourceDir = FileUtils.getResourceDirectory(directory, identifier);
156+
final File file = FileUtils.getNquadsFile(resourceDir, time.truncatedTo(SECONDS));
157+
if (FileUtils.uncheckedDeleteIfExists(file.toPath())) {
158+
LOGGER.debug("Deleted Memento {} at {}", identifier, file);
159+
}
160+
});
161+
}
162+
return completedFuture(null);
136163
}
137164

138165
private void init() {

components/file/src/test/java/org/trellisldp/file/FileMementoServiceTest.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ static void cleanUp() throws IOException {
6262
@Test
6363
void testPutThenDelete() {
6464
final File dir = new File(getClass().getResource("/versions").getFile());
65-
final FileMementoService svc = new FileMementoService(dir.getAbsolutePath());
65+
final FileMementoService svc = new FileMementoService(dir.getAbsolutePath(), true);
6666
final IRI identifier = rdf.createIRI(TRELLIS_DATA_PREFIX + "another-resource");
6767
final IRI root = rdf.createIRI(TRELLIS_DATA_PREFIX);
6868
final Instant time = parse("2019-08-16T14:21:01Z");
@@ -96,6 +96,44 @@ void testPutThenDelete() {
9696
assertEquals(MISSING_RESOURCE, svc.get(identifier, time).toCompletableFuture().join());
9797
}
9898

99+
@Test
100+
void testPutDisabled() {
101+
final File dir = new File(getClass().getResource("/versions").getFile());
102+
final FileMementoService svc = new FileMementoService(dir.getAbsolutePath(), false);
103+
final IRI identifier = rdf.createIRI(TRELLIS_DATA_PREFIX + "another-resource");
104+
final Instant time = parse("2019-08-16T14:21:01Z");
105+
106+
final Resource mockResource = mock(Resource.class);
107+
108+
when(mockResource.getModified()).thenReturn(time);
109+
110+
svc.put(mockResource).toCompletableFuture().join();
111+
112+
final Resource res = svc.get(identifier, time).toCompletableFuture().join();
113+
assertEquals(MISSING_RESOURCE, res);
114+
assertDoesNotThrow(() -> svc.delete(identifier, time).toCompletableFuture().join());
115+
}
116+
117+
@Test
118+
void testListDisabled() {
119+
final IRI identifier = rdf.createIRI(TRELLIS_DATA_PREFIX + "resource");
120+
final File dir = new File(getClass().getResource("/versions").getFile());
121+
assertTrue(dir.exists(), "Resource directory doesn't exist!");
122+
assertTrue(dir.isDirectory(), "Resource directory isn't a valid directory");
123+
124+
try {
125+
System.setProperty(FileMementoService.CONFIG_FILE_MEMENTO_PATH, dir.getAbsolutePath());
126+
System.setProperty(FileMementoService.CONFIG_FILE_MEMENTO, "false");
127+
128+
final MementoService svc = new FileMementoService();
129+
130+
assertEquals(0L, svc.mementos(identifier).toCompletableFuture().join().size(),
131+
"Incorrect count of Mementos!");
132+
} finally {
133+
System.clearProperty(FileMementoService.CONFIG_FILE_MEMENTO_PATH);
134+
System.clearProperty(FileMementoService.CONFIG_FILE_MEMENTO);
135+
}
136+
}
99137

100138
@Test
101139
void testList() {
@@ -132,7 +170,7 @@ void testList() {
132170
@Test
133171
void testPutBinary() {
134172
final File dir = new File(getClass().getResource("/versions").getFile());
135-
final MementoService svc = new FileMementoService(dir.getAbsolutePath());
173+
final MementoService svc = new FileMementoService(dir.getAbsolutePath(), true);
136174
final IRI identifier = rdf.createIRI("trellis:data/a-binary");
137175
final IRI binaryId = rdf.createIRI("file:binary");
138176
final IRI root = rdf.createIRI("trellis:data/");
@@ -177,7 +215,7 @@ void testPutBinary() {
177215
@Test
178216
void testPutIndirectContainer() {
179217
final File dir = new File(getClass().getResource("/versions").getFile());
180-
final MementoService svc = new FileMementoService(dir.getAbsolutePath());
218+
final MementoService svc = new FileMementoService(dir.getAbsolutePath(), true);
181219
final IRI identifier = rdf.createIRI("trellis:data/a-resource");
182220
final IRI member = rdf.createIRI("trellis:data/membership-resource");
183221
final IRI root = rdf.createIRI("trellis:data/");

core/http/src/main/java/org/trellisldp/http/core/HttpConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public final class HttpConstants {
7777
/** Configuration key defining whether PUT-on-create generates contained or uncontained resources. */
7878
public static final String CONFIG_HTTP_PUT_UNCONTAINED = "trellis.http.put-uncontained";
7979

80+
/** Configuration key defining whether versions are created in the HTTP layer. */
81+
public static final String CONFIG_HTTP_VERSIONING = "trellis.http.versioning";
82+
8083
/** The Trellis query parameter for extended features of a given resource. */
8184
public static final String EXT = "ext";
8285

core/http/src/main/java/org/trellisldp/http/impl/MutatingLdpHandler.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import static java.util.stream.Collectors.toList;
2121
import static javax.ws.rs.core.Response.Status.CONFLICT;
2222
import static javax.ws.rs.core.Response.status;
23+
import static org.eclipse.microprofile.config.ConfigProvider.getConfig;
2324
import static org.slf4j.LoggerFactory.getLogger;
25+
import static org.trellisldp.http.core.HttpConstants.CONFIG_HTTP_VERSIONING;
2426
import static org.trellisldp.http.impl.HttpUtils.ldpResourceTypes;
2527
import static org.trellisldp.http.impl.HttpUtils.matchIdentifier;
2628
import static org.trellisldp.http.impl.HttpUtils.skolemizeQuads;
@@ -72,8 +74,8 @@ class MutatingLdpHandler extends BaseLdpHandler {
7274
private static final Logger LOGGER = getLogger(MutatingLdpHandler.class);
7375

7476
private final Session session;
75-
7677
private final InputStream entity;
78+
private final boolean versioningEnabled;
7779

7880
private Resource parent;
7981

@@ -104,6 +106,8 @@ protected MutatingLdpHandler(final TrellisRequest req, final ServiceBundler trel
104106
super(req, trellis, extensions, baseUrl);
105107
this.entity = entity;
106108
this.session = HttpSession.from(req.getSecurityContext());
109+
this.versioningEnabled = getConfig().getOptionalValue(CONFIG_HTTP_VERSIONING, Boolean.class)
110+
.orElse(Boolean.TRUE);
107111
}
108112

109113
protected void setParent(final Resource parent) {
@@ -131,12 +135,15 @@ protected IRI getParentModel() {
131135
* @return a response builder promise
132136
*/
133137
public CompletionStage<ResponseBuilder> updateMemento(final ResponseBuilder builder) {
134-
return getServices().getMementoService().put(getServices().getResourceService(), getInternalId())
135-
.exceptionally(ex -> {
136-
LOGGER.warn("Unable to store memento for {}: {}", getInternalId(), ex.getMessage());
137-
return null;
138-
})
139-
.thenApply(stage -> builder);
138+
if (versioningEnabled) {
139+
return getServices().getMementoService().put(getServices().getResourceService(), getInternalId())
140+
.exceptionally(ex -> {
141+
LOGGER.warn("Unable to store memento for {}: {}", getInternalId(), ex.getMessage());
142+
return null;
143+
})
144+
.thenApply(stage -> builder);
145+
}
146+
return completedFuture(builder);
140147
}
141148

142149
/**

core/http/src/test/java/org/trellisldp/http/impl/PutHandlerTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.trellisldp.api.ResourceService;
5353
import org.trellisldp.api.RuntimeTrellisException;
5454
import org.trellisldp.audit.DefaultAuditService;
55+
import org.trellisldp.http.core.HttpConstants;
5556
import org.trellisldp.vocabulary.LDP;
5657

5758
/**
@@ -95,6 +96,28 @@ void testBadAudit() {
9596
"No exception when the audit backend completes exceptionally!");
9697
}
9798

99+
@Test
100+
void testNoVersioning() {
101+
final MementoService mockMementoService = mock(MementoService.class);
102+
when(mockTrellisRequest.getPath()).thenReturn(RESOURCE_NAME);
103+
when(mockTrellisRequest.getContentType()).thenReturn(TEXT_TURTLE);
104+
when(mockBundler.getMementoService()).thenReturn(mockMementoService);
105+
106+
try {
107+
System.setProperty(HttpConstants.CONFIG_HTTP_VERSIONING, "false");
108+
final PutHandler handler = buildPutHandler(RESOURCE_TURTLE, null, false);
109+
try (final Response res = handler.setResource(handler.initialize(mockParent, MISSING_RESOURCE))
110+
.thenCompose(handler::updateMemento).toCompletableFuture().join().build()) {
111+
assertEquals(CREATED, res.getStatusInfo(), ERR_RESPONSE_CODE);
112+
113+
verify(mockMementoService, never()).put(any(ResourceService.class), any(IRI.class));
114+
}
115+
116+
} finally {
117+
System.clearProperty(HttpConstants.CONFIG_HTTP_VERSIONING);
118+
}
119+
}
120+
98121
@Test
99122
void testPutLdpResourceDefaultType() {
100123
when(mockTrellisRequest.getPath()).thenReturn(RESOURCE_NAME);

platform/dropwizard/src/main/java/org/trellisldp/app/triplestore/AppConfiguration.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class AppConfiguration extends TrellisConfiguration {
3131
@NotNull
3232
private String namespaces;
3333

34+
private boolean isVersioningEnabled = true;
35+
3436
private int levels = 3;
3537

3638
private int length = 2;
@@ -55,6 +57,24 @@ public void setMementos(final String config) {
5557
this.mementos = config;
5658
}
5759

60+
/**
61+
* Get whether versioning is enabled.
62+
* @return true if memento versioning is enabled; false otherwise
63+
*/
64+
@JsonProperty
65+
public boolean getIsVersioningEnabled() {
66+
return isVersioningEnabled;
67+
}
68+
69+
/**
70+
* Set whether mementos are enabled.
71+
* @param isVersioningEnabled whether versioning is enabled
72+
*/
73+
@JsonProperty
74+
public void setIsVersioningEnabled(final boolean isVersioningEnabled) {
75+
this.isVersioningEnabled = isVersioningEnabled;
76+
}
77+
5878
/**
5979
* Get the binary configuration.
6080
* @return the binary configuration

platform/dropwizard/src/main/java/org/trellisldp/app/triplestore/TrellisServiceBundler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public class TrellisServiceBundler extends BaseServiceBundler {
5656
*/
5757
public TrellisServiceBundler(final AppConfiguration config, final Environment environment) {
5858
auditService = new DefaultAuditService();
59-
mementoService = new FileMementoService(config.getMementos());
59+
mementoService = new FileMementoService(config.getMementos(), config.getIsVersioningEnabled());
6060
timemapGenerator = new DefaultTimemapGenerator();
6161
constraintServices = new DefaultConstraintServices(singletonList(new LdpConstraintService()));
6262
resourceService = buildResourceService(config, environment);

platform/dropwizard/src/test/java/org/trellisldp/app/triplestore/TrellisConfigurationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ void testConfigurationGeneral1() throws Exception {
4444
assertFalse(config.getCache().getNoCache(), "Incorrect cache/noCache value!");
4545
assertEquals(10L, config.getJsonld().getCacheSize(), "Incorrect jsonld/cacheSize value!");
4646
assertEquals(48L, config.getJsonld().getCacheExpireHours(), "Incorrect jsonld/cacheExpireHours value!");
47+
assertFalse(config.getIsVersioningEnabled(), "Incorrect versioning value!");
4748
assertTrue(config.getJsonld().getContextDomainWhitelist().isEmpty(), "Incorrect jsonld/contextDomainWhitelist");
4849
assertTrue(config.getJsonld().getContextWhitelist().contains("http://example.org/context.json"),
4950
"Incorrect jsonld/contextWhitelist value!");

platform/dropwizard/src/test/resources/config1.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ cors:
8686

8787
binaryHierarchyLevels: 2
8888
binaryHierarchyLength: 1
89+
isVersioningEnabled: false
8990

9091
cassandraAddress: my.cluster.node
9192
cassandraPort: 245993

0 commit comments

Comments
 (0)