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