001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.kernel.api.utils;
007
008import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
009import static org.apache.jena.vocabulary.RDF.Init.type;
010import static org.fcrepo.kernel.api.RdfLexicon.CREATED_BY;
011import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
012import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_BY;
013import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
014import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
015import static org.fcrepo.kernel.api.RdfLexicon.isRelaxablePredicate;
016import static org.fcrepo.kernel.api.RdfLexicon.restrictedType;
017
018import java.util.Calendar;
019
020import com.google.common.net.MediaType;
021import org.fcrepo.kernel.api.RdfLexicon;
022import org.fcrepo.kernel.api.exception.MalformedRdfException;
023import org.fcrepo.kernel.api.exception.RelaxableServerManagedPropertyException;
024import org.fcrepo.kernel.api.exception.ServerManagedPropertyException;
025import org.fcrepo.kernel.api.exception.ServerManagedTypeException;
026
027import org.apache.jena.datatypes.xsd.XSDDateTime;
028import org.apache.jena.graph.Triple;
029import org.apache.jena.rdf.model.Property;
030import org.apache.jena.rdf.model.RDFNode;
031import org.apache.jena.rdf.model.Resource;
032import org.apache.jena.rdf.model.Statement;
033
034/**
035 * Some server managed triples can have the prohibition on user-management overridden.  While
036 * the server still updates them implicitly, it may be possible in some cases for a user
037 * request to override them.
038 *
039 * @author Mike Durbin
040 * @author whikloj
041 */
042public class RelaxedPropertiesHelper {
043
044    /**
045     * Gets the created date (if any) that was included in the statements.
046     * @param resource the resource we are looking for properties of
047     * @return the date that should be set for the CREATED_DATE or null if it should be
048     *         untouched
049     */
050    public static Calendar getCreatedDate(final Resource resource) {
051        final Iterable<Statement> iter = getIterable(resource, CREATED_DATE);
052        return extractSingleCalendarValue(iter, CREATED_DATE);
053    }
054
055    /**
056     * Gets the created by user (if any) that is included within the statements.
057     * @param resource the resource we are looking for properties of
058     * @return the string that should be set for the CREATED_BY or null if it should be
059     *         untouched
060     */
061    public static String getCreatedBy(final Resource resource) {
062        final Iterable<Statement> iter = getIterable(resource, CREATED_BY);
063        return extractSingleStringValue(iter, CREATED_BY);
064    }
065
066    /**
067     * Gets the modified date (if any) that was included within the statements.
068     * @param resource the resource we are looking for properties of
069     * @return the date that should be set for the LAST_MODIFIED_DATE or null if it should be
070     *         untouched
071     */
072    public static Calendar getModifiedDate(final Resource resource) {
073        final Iterable<Statement> iter = getIterable(resource, LAST_MODIFIED_DATE);
074        return extractSingleCalendarValue(iter, LAST_MODIFIED_DATE);
075    }
076
077    /**
078     * Gets the modified by user (if any) that was included within the statements.
079     * @param resource the resource we are looking for properties of
080     * @return the string that should be set for the MODIFIED_BY or null if it should be
081     *         untouched
082     */
083    public static String getModifiedBy(final Resource resource) {
084        final Iterable<Statement> iter = getIterable(resource, LAST_MODIFIED_BY);
085        return extractSingleStringValue(iter, LAST_MODIFIED_BY);
086    }
087
088    private static String extractSingleStringValue(final Iterable<Statement> statements,
089                                                   final Property predicate) {
090        String textValue = null;
091        for (final Statement added : statements) {
092            if (textValue == null) {
093                textValue = added.getObject().asLiteral().getString();
094            } else {
095                throw new MalformedRdfException(predicate + " may only appear once!");
096            }
097        }
098        return textValue;
099    }
100
101    private static Calendar extractSingleCalendarValue(final Iterable<Statement> statements, final Property predicate) {
102        Calendar cal = null;
103        for (final Statement added : statements) {
104            if (cal == null) {
105                try {
106                    cal = RelaxedPropertiesHelper.parseExpectedXsdDateTimeValue(added.getObject());
107                } catch (final IllegalArgumentException e) {
108                    throw new MalformedRdfException("Expected a xsd:datetime for " + predicate, e);
109                }
110            } else {
111                throw new MalformedRdfException(predicate + " may only appear once!");
112            }
113        }
114        return cal;
115    }
116
117    /**
118     * Parses an RDFNode that is expected to be a literal of type xsd:dateTime into a Java Calendar
119     * object.
120     * @param node a node representing an xsd:dateTime literal
121     * @return a Calendar representation of the expressed dateTime
122     */
123    private static Calendar parseExpectedXsdDateTimeValue(final RDFNode node) {
124        final Object value = node.asLiteral().getValue();
125        if (value instanceof XSDDateTime) {
126            return ((XSDDateTime) value).asCalendar();
127        } else {
128            throw new IllegalArgumentException("Expected an xsd:dateTime!");
129        }
130    }
131
132    private static Iterable<Statement> getIterable(final Resource resource, final Property predicate) {
133        final var iterator = resource.listProperties(predicate);
134        return () -> iterator;
135    }
136
137    /**
138     * Several tests for invalid or disallowed RDF statements.
139     * @param triple the triple to check.
140     */
141    public static void checkTripleForDisallowed(final Triple triple) {
142        if (triple.getPredicate().equals(type().asNode()) && !triple.getObject().isVariable() &&
143                !triple.getObject().isURI()) {
144            // The object of a rdf:type triple is not a variable and not a URI.
145            throw new MalformedRdfException(
146                    String.format("Invalid rdf:type: %s", triple.getObject()));
147        } else if (restrictedType.test(triple)) {
148            // The object of a rdf:type triple has a restricted namespace.
149            throw new ServerManagedTypeException(
150                    String.format("The server managed type (%s) cannot be modified by the client.",
151                            triple.getObject()));
152        } else if (isManagedPredicate.test(createProperty(triple.getPredicate().getURI()))) {
153            // The predicate is server managed.
154            final var message = String.format("The server managed predicate (%s) cannot be modified by the client.",
155                    triple.getPredicate());
156            if (isRelaxablePredicate.test(createProperty(triple.getPredicate().getURI()))) {
157                // It is a relaxable predicate so throw the appropriate exception.
158                throw new RelaxableServerManagedPropertyException(message);
159            }
160            throw new ServerManagedPropertyException(message);
161        } else if (triple.getPredicate().toString().equals(RdfLexicon.HAS_MIME_TYPE.getURI())) {
162            final String mimetype = triple.getObject().toString(false);
163            if (!mimetype.startsWith("?")) {
164                try {
165                    MediaType.parse(mimetype);
166                } catch (final Exception ex) {
167                    throw new MalformedRdfException("Invalid value for '" + RdfLexicon.HAS_MIME_TYPE +
168                            "' encountered : " + mimetype, ex);
169                }
170
171            }
172        }
173    }
174
175    // Prevent instantiation
176    private RelaxedPropertiesHelper() {
177
178    }
179}