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