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 com.hp.hpl.jena.datatypes.RDFDatatype;
023import com.hp.hpl.jena.rdf.model.Literal;
024import com.hp.hpl.jena.rdf.model.RDFNode;
025import com.hp.hpl.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 com.hp.hpl.jena.datatypes.TypeMapper.getInstance;
041import static com.hp.hpl.jena.rdf.model.ResourceFactory.createLangLiteral;
042import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral;
043import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
044import static com.hp.hpl.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.identifiers.NodeResourceConverter.nodeToResource;
058import static org.slf4j.LoggerFactory.getLogger;
059
060/**
061 * @author cabeer
062 * @since 10/8/14
063 */
064public class ValueConverter extends Converter<Value, RDFNode> {
065
066    private static final Logger LOGGER = getLogger(ValueConverter.class);
067
068    private final Session session;
069    private final Converter<Node, Resource> graphSubjects;
070
071    /**
072     * Convert values between JCR values and RDF objects with the given session and subjects
073     * @param session the session
074     * @param graphSubjects the graph subjects
075     */
076    public ValueConverter(final Session session,
077                          final Converter<Resource, FedoraResource> graphSubjects) {
078        this.session = session;
079        this.graphSubjects = nodeToResource(graphSubjects);
080    }
081
082    @Override
083    protected RDFNode doForward(final Value value) {
084        try {
085            switch (value.getType()) {
086                case BOOLEAN:
087                    return literal2node(value.getBoolean());
088                case DATE:
089                    return literal2node(value.getDate());
090                case DECIMAL:
091                    return literal2node(value.getDecimal());
092                case DOUBLE:
093                    return literal2node(value.getDouble());
094                case LONG:
095                    return literal2node(value.getLong());
096                case URI:
097                    return createResource(value.getString());
098                case REFERENCE:
099                case WEAKREFERENCE:
100                case PATH:
101                    return traverseLink(value);
102                default:
103                    return stringliteral2node(value.getString());
104            }
105        } catch (final RepositoryException e) {
106            throw new RepositoryRuntimeException(e);
107        }
108    }
109
110    @Override
111    protected Value doBackward(final RDFNode resource) {
112
113        try {
114
115            final ValueFactory valueFactory = session.getValueFactory();
116
117            if (resource.isAnon()) {
118                // a non-URI resource (e.g. a blank node)
119                return valueFactory.createValue(resource.toString(), UNDEFINED);
120            }
121
122            final RdfLiteralJcrValueBuilder rdfLiteralJcrValueBuilder = new RdfLiteralJcrValueBuilder();
123
124            if (resource.isURIResource()) {
125                rdfLiteralJcrValueBuilder.value(resource.asResource().getURI()).datatype("URI");
126            } else {
127
128                final Literal literal = resource.asLiteral();
129                final RDFDatatype dataType = literal.getDatatype();
130
131                rdfLiteralJcrValueBuilder.value(literal.getString()).datatype(dataType).lang(literal.getLanguage());
132            }
133
134            return valueFactory.createValue(rdfLiteralJcrValueBuilder.toString(), STRING);
135        } catch (final RepositoryException e) {
136            throw new RepositoryRuntimeException(e);
137        }
138    }
139
140    private static Literal literal2node(final Object literal) {
141        final Literal result = createTypedLiteral(literal);
142        LOGGER.trace("Converting {} into {}", literal, result);
143        return result;
144    }
145
146
147    private static RDFNode stringliteral2node(final String literal) {
148        final RdfLiteralJcrValueBuilder rdfLiteralJcrValueBuilder = new RdfLiteralJcrValueBuilder(literal);
149
150        if (rdfLiteralJcrValueBuilder.hasLang()) {
151            return createLangLiteral(rdfLiteralJcrValueBuilder.value(), rdfLiteralJcrValueBuilder.lang());
152        } else if (rdfLiteralJcrValueBuilder.isResource()) {
153            return createResource(rdfLiteralJcrValueBuilder.value());
154        } else if (rdfLiteralJcrValueBuilder.hasDatatypeUri()) {
155            return createTypedLiteral(rdfLiteralJcrValueBuilder.value(), rdfLiteralJcrValueBuilder.datatype());
156        } else {
157            return createPlainLiteral(literal);
158        }
159    }
160
161    private RDFNode traverseLink(final Value v) throws RepositoryException {
162        try {
163            return getGraphSubject(nodeForValue(session, v));
164        } catch (final AccessDeniedException e) {
165            LOGGER.info("Link inaccessible by requesting user: {}, {}", v, session.getUserID());
166            return INACCESSIBLE_RESOURCE;
167        }
168    }
169
170    /**
171     * Get the node that a property value refers to.
172     * @param session Session to use to load the node.
173     * @param v Value that refers to a node.
174     * @return the JCR node
175     * @throws RepositoryException When there is an error accessing the node.
176     * @throws RepositoryRuntimeException When the value type is not PATH, REFERENCE or WEAKREFERENCE.
177    **/
178    public static javax.jcr.Node nodeForValue(final Session session, final Value v) throws RepositoryException {
179        if (v.getType() == PATH) {
180            return session.getNode(v.getString());
181        } else if (v.getType() == REFERENCE || v.getType() == WEAKREFERENCE) {
182            return session.getNodeByIdentifier(v.getString());
183        } else {
184            throw new RepositoryRuntimeException("Cannot convert value of type "
185                    + PropertyType.nameFromValue(v.getType()) + " to a node reference");
186        }
187    }
188
189    private RDFNode getGraphSubject(final javax.jcr.Node n) {
190        return graphSubjects.convert(n);
191    }
192
193    protected static class RdfLiteralJcrValueBuilder {
194        private static final String FIELD_DELIMITER = "\30^^\30";
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}