All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.trellisldp.rosid.file.RDFPatch Maven / Gradle / Ivy

There is a newer version: 0.3.1
Show newest version
/*
 * 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.lang.String.join;
import static java.lang.System.lineSeparator;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.lines;
import static java.nio.file.Files.newBufferedWriter;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.time.Instant.parse;
import static java.time.temporal.ChronoUnit.MILLIS;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Optional.of;
import static java.util.Spliterator.IMMUTABLE;
import static java.util.Spliterator.NONNULL;
import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;
import static org.slf4j.LoggerFactory.getLogger;
import static org.trellisldp.rosid.file.FileUtils.stringToQuad;
import static org.trellisldp.vocabulary.RDF.type;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

import org.apache.commons.io.input.ReversedLinesFileReader;
import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.Quad;
import org.apache.commons.rdf.api.RDF;
import org.slf4j.Logger;
import org.trellisldp.api.VersionRange;
import org.trellisldp.vocabulary.DC;
import org.trellisldp.vocabulary.LDP;
import org.trellisldp.vocabulary.Trellis;
import org.trellisldp.vocabulary.XSD;

/**
 * @author acoburn
 */
final class RDFPatch {

    private static final Logger LOGGER = getLogger(RDFPatch.class);

    private static final String COMMENT_DELIM = " # ";
    private static final String BEGIN = "BEGIN" + COMMENT_DELIM;
    private static final String END = "END" + COMMENT_DELIM;

    /**
     * Read the triples from the journal that existed up to (and including) the specified time
     * @param rdf the rdf object
     * @param file the file
     * @param identifier the identifier
     * @param time the time
     * @return a stream of RDF triples
     */
    public static Stream asStream(final RDF rdf, final File file, final IRI identifier, final Instant time) {
        LOGGER.debug("Reading Journal for {} as quads", identifier);
        final StreamReader reader = new StreamReader(rdf, file, identifier, time);
        return stream(spliteratorUnknownSize(reader, IMMUTABLE | NONNULL | ORDERED), false).onClose(reader::close);
    }

    /**
     * Retrieve time values for the history of the resource
     * @param file the file
     * @return a list of VersionRange objects
     */
    public static List asTimeMap(final File file) {
        LOGGER.debug("Reading Journal for TimeMap data");
        final List ranges = new ArrayList<>();
        try (final TimeMapReader reader = new TimeMapReader(file)) {
            reader.forEachRemaining(ranges::add);
        }
        return unmodifiableList(ranges);
    }

    /**
     * Write RDF Patch statements to the specified file
     * @param file the file
     * @param delete the quads to delete
     * @param add the quads to add
     * @param time the time
     * @return true if the write succeeds; false otherwise
     */
    public static Boolean write(final File file, final Stream delete, final Stream add,
            final Instant time) {
        LOGGER.debug("Writing Journal at {}", file.getPath());
        try (final BufferedWriter writer = newBufferedWriter(file.toPath(), UTF_8, CREATE, APPEND)) {
            writer.write(BEGIN + time.truncatedTo(MILLIS) + lineSeparator());
            final Iterator delIter = delete.map(quadToString).iterator();
            while (delIter.hasNext()) {
                writer.write("D " + delIter.next() + lineSeparator());
            }
            final Iterator addIter = add.map(quadToString).iterator();
            while (addIter.hasNext()) {
                writer.write("A " + addIter.next() + lineSeparator());
            }
            writer.write(END + time.truncatedTo(MILLIS) + lineSeparator());
        } catch (final IOException ex) {
            LOGGER.error("Error writing data to resource {}: {}", file, ex.getMessage());
            return false;
        }
        return true;
    }

    public static final Function quadToString = quad ->
        join(" ",
                quad.getSubject().ntriplesString(), quad.getPredicate().ntriplesString(),
                quad.getObject().ntriplesString(),
                quad.getGraphName().orElse(Trellis.PreferUserManaged).ntriplesString(), ".");

    /**
     * A class for reading an RDF Patch file into a VersionRange Iterator
     */
    static class TimeMapReader implements Iterator, AutoCloseable {
        private final Stream lineStream;
        private final Iterator allLines;
        private Instant from = null;
        private Boolean hasUserTriples = false;
        private VersionRange buffer = null;

        /**
         * Create a time map reader
         * @param file the file
         */
        public TimeMapReader(final File file) {
            try {
                lineStream = lines(file.toPath());
            } catch (final IOException ex) {
                throw new UncheckedIOException(ex);
            }
            allLines = lineStream.iterator();
            tryAdvance();
        }

        @Override
        public boolean hasNext() {
            return nonNull(buffer);
        }

        @Override
        public VersionRange next() {
            final VersionRange range = buffer;
            tryAdvance();
            if (nonNull(range)) {
                return range;
            }
            throw new NoSuchElementException();
        }

        @Override
        public void close() {
            LOGGER.trace("Closing Journal from Timemap");
            lineStream.close();
        }

        private void tryAdvance() {
            while (allLines.hasNext()) {
                final String line = allLines.next();
                if (line.startsWith(BEGIN)) {
                    hasUserTriples = false;
                } else if (line.endsWith(Trellis.PreferUserManaged + " .") ||
                        line.endsWith(Trellis.PreferServerManaged + " .")) {
                    hasUserTriples = true;
                } else if (line.startsWith(END) && hasUserTriples) {
                    final Instant time = parse(line.split(COMMENT_DELIM)[1]);
                    if (nonNull(from)) {
                        if (time.isAfter(from.truncatedTo(MILLIS))) {
                            buffer = new VersionRange(from, time);
                            from = time;
                        }
                        return;
                    }
                    from = time;
                }
            }
            buffer = null;
        }
    }

    /**
     * A class for reading an RDFPatch file into a Quad Iterator.
     */
    static class StreamReader implements Iterator, AutoCloseable {

        private final Set deleted = new HashSet<>();
        private final ReversedLinesFileReader reader;
        private final Instant time;
        private final RDF rdf;
        private final IRI identifier;

        private Boolean inRegion = false;
        private Boolean hasModified = false;
        private Boolean hasModificationQuads = false;
        private Boolean hasContainerModificationQuads = false;
        private Quad buffer = null;
        private String line = null;
        private IRI interactionModel = null;
        private Instant momentIfContainer = null;
        private Instant momentIfNotContainer = null;

        /**
         * Create an iterator that reads a file line-by-line in reverse
         * @param rdf the RDF object
         * @param file the file
         * @param identifier the identifier
         * @param time the time
         */
        public StreamReader(final RDF rdf, final File file, final IRI identifier, final Instant time) {
            this.rdf = rdf;
            this.time = time;
            this.identifier = identifier;
            try {
                this.reader = new ReversedLinesFileReader(file, UTF_8);
                this.line = reader.readLine();
            } catch (final IOException ex) {
                throw new UncheckedIOException(ex);
            }
            tryAdvance();
        }

        @Override
        public boolean hasNext() {
            return nonNull(buffer);
        }

        @Override
        public Quad next() {
            final Quad quad = buffer;
            tryAdvance();
            if (nonNull(quad)) {
                return quad;
            }
            throw new NoSuchElementException();
        }

        @Override
        public void close() {
            LOGGER.trace("Closing stream reader");
            try {
                reader.close();
            } catch (final IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }

        private Boolean isRDFPatchLine(final String line) {
            return line.startsWith("A ") || line.startsWith("D ");
        }

        private Boolean shouldProceed(final String line, final Quad buffer) {
            return nonNull(line) && isNull(buffer);
        }

        private Consumer quadHandler(final String prefix) {
            return quad -> {
                if (quad.getGraphName().equals(of(LDP.PreferContainment)) ||
                        quad.getGraphName().equals(of(LDP.PreferMembership))) {
                    hasContainerModificationQuads = true;
                } else {
                    hasModificationQuads = true;
                }
                if (prefix.equals("D")) {
                    deleted.add(quad);
                } else if (prefix.equals("A") && !deleted.contains(quad)) {
                    if (quad.getGraphName().filter(Trellis.PreferServerManaged::equals).isPresent() &&
                            quad.getPredicate().equals(type)) {
                        interactionModel = (IRI) quad.getObject();
                    }
                    buffer = quad;
                }
            };
        }

        private Boolean shouldSetModificationForContainers() {
            return (hasContainerModificationQuads || hasModificationQuads) && isNull(momentIfContainer);
        }

        private Boolean shouldSetModificationForNonContainers() {
            return hasModificationQuads && isNull(momentIfNotContainer);
        }

        private void maybeEmitModifiedQuad(final String line) {
            final Instant moment = parse(line.split(COMMENT_DELIM, 2)[1]);
            if (!time.isBefore(moment.truncatedTo(MILLIS))) {
                if (shouldSetModificationForContainers()) {
                    momentIfContainer = moment;
                }
                if (shouldSetModificationForNonContainers()) {
                    momentIfNotContainer = moment;
                }
                if (LDP.RDFSource.equals(interactionModel) || LDP.NonRDFSource.equals(interactionModel)) {
                    if (nonNull(momentIfNotContainer)) {
                        buffer = rdf.createQuad(Trellis.PreferServerManaged, identifier, DC.modified,
                                rdf.createLiteral(momentIfNotContainer.toString(), XSD.dateTime));
                        hasModified = true;
                    }
                } else if (nonNull(interactionModel) && nonNull(momentIfContainer)) {
                    buffer = rdf.createQuad(Trellis.PreferServerManaged, identifier, DC.modified,
                            rdf.createLiteral(momentIfContainer.toString(), XSD.dateTime));
                    hasModified = true;
                }
            }
        }

        private void tryAdvance() {
            buffer = null;
            while (shouldProceed(line, buffer)) {
                // Determine if the reader is within the target region
                if (inRegion) {
                    // If the reader is in the target region, output any valid "A" quads and record any "D" quads
                    if (isRDFPatchLine(line)) {
                        final String[] parts = line.split(" ", 2);
                        stringToQuad(rdf, parts[1]).ifPresent(quadHandler(parts[0]));

                    // If the reader is in the target region and the modified triple hasn't yet been emitted
                    } else if (line.startsWith(BEGIN) && !hasModified) {
                        maybeEmitModifiedQuad(line);
                    }

                // Check if the reader has entered the target region
                } else if (line.startsWith(END) && !time.isBefore(parse(line.split(COMMENT_DELIM, 2)[1])
                            .truncatedTo(MILLIS))) {
                    inRegion = true;
                }

                // Advance the reader
                try {
                    line = reader.readLine();
                } catch (final IOException ex) {
                    throw new UncheckedIOException(ex);
                }
            }
        }
    }

    private RDFPatch() {
        // prevent instantiation
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy