001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.kernel.modeshape.rdf.converters;
017
018import com.google.common.base.Converter;
019import com.google.common.base.Splitter;
020import com.hp.hpl.jena.datatypes.BaseDatatype;
021import com.hp.hpl.jena.datatypes.RDFDatatype;
022import com.hp.hpl.jena.rdf.model.Literal;
023import com.hp.hpl.jena.rdf.model.RDFNode;
024import com.hp.hpl.jena.rdf.model.Resource;
025import org.fcrepo.kernel.api.models.FedoraResource;
026import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
027import org.slf4j.Logger;
028
029import javax.jcr.AccessDeniedException;
030import javax.jcr.Node;
031import javax.jcr.PropertyType;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.Value;
035import javax.jcr.ValueFactory;
036
037import java.util.Iterator;
038
039import static com.hp.hpl.jena.rdf.model.ResourceFactory.createLangLiteral;
040import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral;
041import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
042import static com.hp.hpl.jena.rdf.model.ResourceFactory.createTypedLiteral;
043import static javax.jcr.PropertyType.BOOLEAN;
044import static javax.jcr.PropertyType.DATE;
045import static javax.jcr.PropertyType.DECIMAL;
046import static javax.jcr.PropertyType.DOUBLE;
047import static javax.jcr.PropertyType.LONG;
048import static javax.jcr.PropertyType.PATH;
049import static javax.jcr.PropertyType.REFERENCE;
050import static javax.jcr.PropertyType.STRING;
051import static javax.jcr.PropertyType.UNDEFINED;
052import static javax.jcr.PropertyType.URI;
053import static javax.jcr.PropertyType.WEAKREFERENCE;
054import static org.fcrepo.kernel.api.RdfLexicon.INACCESSIBLE_RESOURCE;
055import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeToResource;
056import static org.slf4j.LoggerFactory.getLogger;
057
058/**
059 * @author cabeer
060 * @since 10/8/14
061 */
062public class ValueConverter extends Converter<Value, RDFNode> {
063
064    private static final Logger LOGGER = getLogger(ValueConverter.class);
065
066    private final Session session;
067    private final Converter<Node, Resource> graphSubjects;
068
069    /**
070     * Convert values between JCR values and RDF objects with the given session and subjects
071     * @param session the session
072     * @param graphSubjects the graph subjects
073     */
074    public ValueConverter(final Session session,
075                          final Converter<Resource, FedoraResource> graphSubjects) {
076        this.session = session;
077        this.graphSubjects = nodeToResource(graphSubjects);
078    }
079
080    @Override
081    protected RDFNode doForward(final Value value) {
082        try {
083            switch (value.getType()) {
084                case BOOLEAN:
085                    return literal2node(value.getBoolean());
086                case DATE:
087                    return literal2node(value.getDate());
088                case DECIMAL:
089                    return literal2node(value.getDecimal());
090                case DOUBLE:
091                    return literal2node(value.getDouble());
092                case LONG:
093                    return literal2node(value.getLong());
094                case URI:
095                    return createResource(value.getString());
096                case REFERENCE:
097                case WEAKREFERENCE:
098                case PATH:
099                    return traverseLink(value);
100                default:
101                    return stringliteral2node(value.getString());
102            }
103        } catch (final RepositoryException e) {
104            throw new RepositoryRuntimeException(e);
105        }
106    }
107
108    @Override
109    protected Value doBackward(final RDFNode resource) {
110
111        try {
112
113            final ValueFactory valueFactory = session.getValueFactory();
114
115            if (resource.isAnon()) {
116                // a non-URI resource (e.g. a blank node)
117                return valueFactory.createValue(resource.toString(), UNDEFINED);
118            }
119
120            final RdfLiteralJcrValueBuilder rdfLiteralJcrValueBuilder = new RdfLiteralJcrValueBuilder();
121
122            if (resource.isURIResource()) {
123                rdfLiteralJcrValueBuilder.value(resource.asResource().getURI()).datatype("URI");
124            } else {
125
126                final Literal literal = resource.asLiteral();
127                final RDFDatatype dataType = literal.getDatatype();
128
129                rdfLiteralJcrValueBuilder.value(literal.getString()).datatype(dataType).lang(literal.getLanguage());
130            }
131
132            return valueFactory.createValue(rdfLiteralJcrValueBuilder.toString(), STRING);
133        } catch (final RepositoryException e) {
134            throw new RepositoryRuntimeException(e);
135        }
136    }
137
138    private static Literal literal2node(final Object literal) {
139        final Literal result = createTypedLiteral(literal);
140        LOGGER.trace("Converting {} into {}", literal, result);
141        return result;
142    }
143
144
145    private static RDFNode stringliteral2node(final String literal) {
146        final RdfLiteralJcrValueBuilder rdfLiteralJcrValueBuilder = new RdfLiteralJcrValueBuilder(literal);
147
148        if (rdfLiteralJcrValueBuilder.hasLang()) {
149            return createLangLiteral(rdfLiteralJcrValueBuilder.value(), rdfLiteralJcrValueBuilder.lang());
150        } else if (rdfLiteralJcrValueBuilder.isResource()) {
151            return createResource(rdfLiteralJcrValueBuilder.value());
152        } else if (rdfLiteralJcrValueBuilder.hasDatatypeUri()) {
153            return createTypedLiteral(rdfLiteralJcrValueBuilder.value(), rdfLiteralJcrValueBuilder.datatype());
154        } else {
155            return createPlainLiteral(literal);
156        }
157    }
158
159    private RDFNode traverseLink(final Value v) throws RepositoryException {
160        try {
161            return getGraphSubject(nodeForValue(session, v));
162        } catch (final AccessDeniedException e) {
163            LOGGER.info("Link inaccessible by requesting user: {}, {}", v, session.getUserID());
164            return INACCESSIBLE_RESOURCE;
165        }
166    }
167
168    /**
169     * Get the node that a property value refers to.
170     * @param session Session to use to load the node.
171     * @param v Value that refers to a node.
172     * @return the JCR node
173     * @throws RepositoryException When there is an error accessing the node.
174     * @throws RepositoryRuntimeException When the value type is not PATH, REFERENCE or WEAKREFERENCE.
175    **/
176    public static javax.jcr.Node nodeForValue(final Session session, final Value v) throws RepositoryException {
177        if (v.getType() == PATH) {
178            return session.getNode(v.getString());
179        } else if (v.getType() == REFERENCE || v.getType() == WEAKREFERENCE) {
180            return session.getNodeByIdentifier(v.getString());
181        } else {
182            throw new RepositoryRuntimeException("Cannot convert value of type "
183                    + PropertyType.nameFromValue(v.getType()) + " to a node reference");
184        }
185    }
186
187    private RDFNode getGraphSubject(final javax.jcr.Node n) {
188        return graphSubjects.convert(n);
189    }
190
191    protected static class RdfLiteralJcrValueBuilder {
192        private static final String FIELD_DELIMITER = "\30^^\30";
193        public static final Splitter JCR_VALUE_SPLITTER = Splitter.on(FIELD_DELIMITER);
194
195        private String value;
196        private String datatypeUri;
197        private String lang;
198
199        RdfLiteralJcrValueBuilder() {
200
201        }
202
203        public RdfLiteralJcrValueBuilder(final String literal) {
204            this();
205
206            final Iterator<String> tokenizer = JCR_VALUE_SPLITTER.split(literal).iterator();
207
208            value = tokenizer.next();
209
210            if (tokenizer.hasNext()) {
211                datatypeUri = tokenizer.next();
212            }
213
214            if (tokenizer.hasNext()) {
215                lang = tokenizer.next();
216            }
217        }
218
219        @Override
220        public String toString() {
221            final StringBuilder b = new StringBuilder();
222
223            b.append(value);
224
225            if (hasDatatypeUri()) {
226                b.append(FIELD_DELIMITER);
227                b.append(datatypeUri);
228            } else if (hasLang()) {
229                // if it has a language, but not a datatype, add a placeholder.
230                b.append(FIELD_DELIMITER);
231            }
232
233            if (hasLang()) {
234                b.append(FIELD_DELIMITER);
235                b.append(lang);
236            }
237
238            return b.toString();
239
240        }
241
242        public String value() {
243            return value;
244        }
245
246        public RDFDatatype datatype() {
247            if (hasDatatypeUri()) {
248                return new BaseDatatype(datatypeUri);
249            }
250            return null;
251        }
252
253        public String lang() {
254            return lang;
255        }
256
257        public RdfLiteralJcrValueBuilder value(final String value) {
258            this.value = value;
259            return this;
260        }
261
262        public RdfLiteralJcrValueBuilder datatype(final String datatypeUri) {
263            this.datatypeUri = datatypeUri;
264            return this;
265        }
266
267        public RdfLiteralJcrValueBuilder datatype(final RDFDatatype datatypeUri) {
268            if (datatypeUri != null && !datatypeUri.getURI().isEmpty()) {
269                this.datatypeUri = datatypeUri.getURI();
270            }
271            return this;
272        }
273
274        public RdfLiteralJcrValueBuilder lang(final String lang) {
275            this.lang = lang;
276            return this;
277        }
278
279        public boolean hasLang() {
280            return lang != null && !lang.isEmpty();
281        }
282
283        public boolean hasDatatypeUri() {
284            return datatypeUri != null && !datatypeUri.isEmpty();
285        }
286
287        public boolean isResource() {
288            return hasDatatypeUri() && datatypeUri.equals("URI");
289        }
290    }
291}