org.trellisldp.rosid.file.FileResourceService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trellis-rosid-file Show documentation
Show all versions of trellis-rosid-file Show documentation
A high performance implementation of a trellis repository
/*
* 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.trellisldp.rosid.file;
import static java.net.URI.create;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Files.lines;
import static java.nio.file.Files.newBufferedWriter;
import static java.nio.file.Files.walk;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.time.Instant.now;
import static java.util.Objects.isNull;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Stream.empty;
import static java.util.stream.Stream.of;
import static org.slf4j.LoggerFactory.getLogger;
import static org.trellisldp.api.RDFUtils.TRELLIS_PREFIX;
import static org.trellisldp.rosid.file.Constants.RESOURCE_CACHE;
import static org.trellisldp.rosid.file.Constants.RESOURCE_JOURNAL;
import static org.trellisldp.rosid.file.Constants.RESOURCE_QUADS;
import static org.trellisldp.rosid.file.FileUtils.resourceDirectory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.Quad;
import org.apache.commons.rdf.api.Triple;
import org.apache.curator.framework.CuratorFramework;
import org.apache.kafka.clients.producer.Producer;
import org.slf4j.Logger;
import org.trellisldp.api.EventService;
import org.trellisldp.api.Resource;
import org.trellisldp.rosid.common.AbstractResourceService;
import org.trellisldp.vocabulary.ACL;
import org.trellisldp.vocabulary.AS;
import org.trellisldp.vocabulary.DC;
import org.trellisldp.vocabulary.FOAF;
import org.trellisldp.vocabulary.LDP;
import org.trellisldp.vocabulary.PROV;
import org.trellisldp.vocabulary.RDF;
import org.trellisldp.vocabulary.Trellis;
import org.trellisldp.vocabulary.XSD;
/**
* @author acoburn
*/
public class FileResourceService extends AbstractResourceService {
private static final Logger LOGGER = getLogger(FileResourceService.class);
private final Map partitionData;
/**
* Create a File-based repository service
* @param partitionData the partition data configuration
* @param partitionUrls the partition URL configuration
* @param curator the curator framework
* @param producer the kafka producer
* @param notifications the notification service
* @param idSupplier an identifier supplier for new resources
* @param async generate cached resources asynchronously if true, synchonously if false
* @throws IOException if the directory is not writable
*/
public FileResourceService(final Map partitionData, final Map partitionUrls,
final CuratorFramework curator, final Producer producer, final EventService notifications,
final Supplier idSupplier, final Boolean async) throws IOException {
super(partitionUrls, producer, curator, notifications, idSupplier, async);
requireNonNull(partitionData, "partition data configuration may not be null!");
RESERVED_PARTITION_NAMES.stream().filter(partitionData::containsKey).findAny().ifPresent(name -> {
throw new IllegalArgumentException("Invalid partition name: " + name);
});
this.partitionData = partitionData;
init();
}
@Override
public Optional get(final IRI identifier) {
return ofNullable(resourceDirectory(partitionData, identifier)).filter(File::exists)
.flatMap(dir -> new File(dir, RESOURCE_CACHE).exists() ?
CachedResource.find(dir, identifier) : VersionedResource.find(dir, identifier, now()));
}
@Override
public Optional get(final IRI identifier, final Instant time) {
return ofNullable(resourceDirectory(partitionData, identifier)).filter(File::exists)
.flatMap(dir -> VersionedResource.find(dir, identifier, time));
}
@Override
protected Boolean write(final IRI identifier, final Stream extends Quad> remove,
final Stream extends Quad> add, final Instant time, final Boolean cacheAsync) {
final File dir = resourceDirectory(partitionData, identifier);
if (isNull(dir)) {
return false;
}
dir.mkdirs();
return RDFPatch.write(new File(dir, RESOURCE_JOURNAL), remove, add, time) &&
(async || cacheAsync || CachedResource.write(dir, identifier));
}
@Override
public Stream compact(final IRI identifier, final Instant from, final Instant until) {
throw new UnsupportedOperationException("compact is not implemented");
}
@Override
public Stream tryPurge(final IRI identifier) {
final List binaries = new ArrayList<>();
final File directory = resourceDirectory(partitionData, identifier);
try (final Stream lineStream = lines(new File(directory, RESOURCE_JOURNAL).toPath())) {
lineStream.flatMap(line -> {
final String[] parts = line.split(" ", 6);
if (parts.length == 6 && parts[0].equals("A") &&
parts[1].equals(identifier.toString()) &&
parts[2].equals(DC.hasPart.toString()) &&
parts[4].equals(Trellis.PreferServerManaged.toString())) {
return of(parts[3]);
}
return empty();
}).map(iri -> iri.substring(1, iri.length() - 1)).map(rdf::createIRI).forEach(binaries::add);
} catch (final IOException ex) {
LOGGER.error("Error processing journal file: {}", ex.getMessage());
throw new UncheckedIOException(ex);
}
try {
deleteIfExists(new File(directory, RESOURCE_CACHE).toPath());
deleteIfExists(new File(directory, RESOURCE_QUADS).toPath());
// Truncate history file, rather than actually deleting it
try (final BufferedWriter writer = newBufferedWriter(
new File(directory, RESOURCE_JOURNAL).toPath(), UTF_8, WRITE, TRUNCATE_EXISTING)) {
writer.write("");
}
} catch (final IOException ex) {
LOGGER.error("Error deleting files: {}", ex.getMessage());
throw new UncheckedIOException(ex);
}
return binaries.stream();
}
@Override
public Stream scan(final String partition) {
if (partitionData.containsKey(partition)) {
try {
return walk(new File(partitionData.get(partition)).toPath(), FileUtils.MAX + 2)
.filter(p -> p.endsWith(RESOURCE_CACHE)).map(Path::getParent).map(Path::toFile)
.map(CachedResource::read)
// TODO - JDK9 optional to stream
.flatMap(res -> res.map(Stream::of).orElseGet(Stream::empty)).map(data ->
rdf.createTriple(rdf.createIRI(data.getId()), RDF.type, rdf.createIRI(data.getLdpType())));
} catch (final IOException ex) {
LOGGER.error("Error reading partition root: {}", ex.getMessage());
}
}
return empty();
}
private void init() throws IOException {
for (final Map.Entry storage : partitionData.entrySet()) {
final File data = storage.getValue().startsWith("file:") ?
new File(create(storage.getValue())) : new File(storage.getValue());
LOGGER.info("Using resource data directory for '{}': {}", storage.getKey(), data.getAbsolutePath());
if (!data.exists()) {
data.mkdirs();
}
if (!data.canWrite()) {
throw new IOException("Cannot write to " + data.getAbsolutePath());
}
final IRI identifier = rdf.createIRI(TRELLIS_PREFIX + storage.getKey());
final IRI authIdentifier = rdf.createIRI(TRELLIS_PREFIX + storage.getKey() + "#auth");
final File root = resourceDirectory(partitionData, identifier);
final File rootData = new File(root, RESOURCE_JOURNAL);
if (!root.exists() || !rootData.exists()) {
LOGGER.info("Initializing root container for '{}'", identifier.getIRIString());
root.mkdirs();
final Instant time = now();
final IRI skolem = (IRI) skolemize(rdf.createBlankNode());
final Stream quads = of(
rdf.createQuad(Trellis.PreferServerManaged, identifier, RDF.type, LDP.Container),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, RDF.type, ACL.Authorization),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, ACL.mode, ACL.Read),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, ACL.mode, ACL.Write),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, ACL.mode, ACL.Control),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, ACL.accessTo, identifier),
rdf.createQuad(Trellis.PreferAccessControl, authIdentifier, ACL.agentClass, FOAF.Agent),
rdf.createQuad(Trellis.PreferAudit, identifier, PROV.wasGeneratedBy, skolem),
rdf.createQuad(Trellis.PreferAudit, skolem, RDF.type, PROV.Activity),
rdf.createQuad(Trellis.PreferAudit, skolem, RDF.type, AS.Create),
rdf.createQuad(Trellis.PreferAudit, skolem, PROV.wasAssociatedWith,
Trellis.RepositoryAdministrator),
rdf.createQuad(Trellis.PreferAudit, skolem, PROV.generatedAtTime,
rdf.createLiteral(time.toString(), XSD.dateTime)));
RDFPatch.write(rootData, empty(), quads, now());
CachedResource.write(root, identifier);
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy