// *************************************************************************************************************************** | |
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * | |
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * | |
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * | |
// * with the License. You may obtain a copy of the License at * | |
// * * | |
// * http://www.apache.org/licenses/LICENSE-2.0 * | |
// * * | |
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * | |
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * | |
// * specific language governing permissions and limitations under the License. * | |
// *************************************************************************************************************************** | |
package org.apache.juneau.jena; | |
import static org.apache.juneau.internal.StringUtils.*; | |
import static org.apache.juneau.jena.Constants.*; | |
import java.io.IOException; | |
import java.util.*; | |
import org.apache.jena.rdf.model.*; | |
import org.apache.jena.util.iterator.*; | |
import org.apache.juneau.*; | |
import org.apache.juneau.parser.*; | |
import org.apache.juneau.transform.*; | |
import org.apache.juneau.xml.*; | |
/** | |
* Session object that lives for the duration of a single use of {@link RdfParser}. | |
* | |
* <p> | |
* This class is NOT thread safe. | |
* It is typically discarded after one-time use although it can be reused against multiple inputs. | |
*/ | |
@SuppressWarnings({"unchecked", "rawtypes"}) | |
public class RdfParserSession extends ReaderParserSession { | |
private final RdfParser ctx; | |
private final Property pRoot, pValue, pType, pRdfType; | |
private final Model model; | |
private final RDFReader rdfReader; | |
private final Set<Resource> urisVisited = new HashSet<>(); | |
/** | |
* Create a new session using properties specified in the context. | |
* | |
* @param ctx | |
* The context creating this session object. | |
* The context contains all the configuration settings for this object. | |
* @param args | |
* Runtime session arguments. | |
*/ | |
protected RdfParserSession(RdfParser ctx, ParserSessionArgs args) { | |
super(ctx, args); | |
this.ctx = ctx; | |
model = ModelFactory.createDefaultModel(); | |
addModelPrefix(ctx.getJuneauNs()); | |
addModelPrefix(ctx.getJuneauBpNs()); | |
pRoot = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_ROOT); | |
pValue = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_VALUE); | |
pType = model.createProperty(ctx.getJuneauBpNs().getUri(), RDF_juneauNs_TYPE); | |
pRdfType = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); | |
rdfReader = model.getReader(ctx.getLanguage()); | |
// Note: NTripleReader throws an exception if you try to set any properties on it. | |
if (! ctx.getLanguage().equals(LANG_NTRIPLE)) { | |
for (Map.Entry<String,Object> e : ctx.jenaProperties.entrySet()) | |
rdfReader.setProperty(e.getKey(), e.getValue()); | |
} | |
} | |
@Override /* ReaderParserSession */ | |
protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { | |
RDFReader r = rdfReader; | |
r.read(model, pipe.getBufferedReader(), null); | |
List<Resource> roots = getRoots(model); | |
// Special case where we're parsing a loose collection of resources. | |
if (isLooseCollections() && type.isCollectionOrArray()) { | |
Collection c = null; | |
if (type.isArray() || type.isArgs()) | |
c = new ArrayList(); | |
else | |
c = ( | |
type.canCreateNewInstance(getOuter()) | |
? (Collection<?>)type.newInstance(getOuter()) | |
: new ObjectList(this) | |
); | |
int argIndex = 0; | |
for (Resource resource : roots) | |
c.add(parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), resource, | |
getOuter(), null)); | |
if (type.isArray() || type.isArgs()) | |
return (T)toArray(type, c); | |
return (T)c; | |
} | |
if (roots.isEmpty()) | |
return type.isOptional() ? (T)Optional.empty() : null; | |
if (roots.size() > 1) | |
throw new ParseException(this, "Too many root nodes found in model: {0}", roots.size()); | |
Resource resource = roots.get(0); | |
return parseAnything(type, resource, getOuter(), null); | |
} | |
private final void addModelPrefix(Namespace ns) { | |
model.setNsPrefix(ns.getName(), ns.getUri()); | |
} | |
/* | |
* Decodes the specified string. | |
* If {@link RdfParser#RDF_trimWhitespace} is <jk>true</jk>, the resulting string is trimmed before decoding. | |
* If {@link #isTrimStrings()} is <jk>true</jk>, the resulting string is trimmed after decoding. | |
*/ | |
private String decodeString(Object o) { | |
if (o == null) | |
return null; | |
String s = o.toString(); | |
if (s.isEmpty()) | |
return s; | |
if (isTrimWhitespace()) | |
s = s.trim(); | |
s = XmlUtils.decode(s, null); | |
if (isTrimStrings()) | |
s = s.trim(); | |
return s; | |
} | |
/* | |
* Finds the roots in the model using either the "root" property to identify it, | |
* or by resorting to scanning the model for all nodes with no incoming predicates. | |
*/ | |
private List<Resource> getRoots(Model m) { | |
List<Resource> l = new LinkedList<>(); | |
// First try to find the root using the "http://www.apache.org/juneau/root" property. | |
Property root = m.createProperty(getJuneauNs().getUri(), RDF_juneauNs_ROOT); | |
for (ResIterator i = m.listResourcesWithProperty(root); i.hasNext();) | |
l.add(i.next()); | |
if (! l.isEmpty()) | |
return l; | |
// Otherwise, we need to find all resources that aren't objects. | |
// We want to explicitly ignore statements where the subject | |
// and object are the same node. | |
Set<RDFNode> objects = new HashSet<>(); | |
for (StmtIterator i = m.listStatements(); i.hasNext();) { | |
Statement st = i.next(); | |
RDFNode subject = st.getSubject(); | |
RDFNode object = st.getObject(); | |
if (object.isResource() && ! object.equals(subject)) | |
objects.add(object); | |
} | |
for (ResIterator i = m.listSubjects(); i.hasNext();) { | |
Resource r = i.next(); | |
if (! objects.contains(r)) | |
l.add(r); | |
} | |
return l; | |
} | |
private <T> BeanMap<T> parseIntoBeanMap(Resource r2, BeanMap<T> m) throws IOException, ParseException, ExecutableException { | |
BeanMeta<T> bm = m.getMeta(); | |
RdfBeanMeta rbm = getRdfBeanMeta(bm); | |
if (rbm.hasBeanUri() && r2.getURI() != null) | |
rbm.getBeanUriProperty().set(m, null, r2.getURI()); | |
for (StmtIterator i = r2.listProperties(); i.hasNext();) { | |
Statement st = i.next(); | |
Property p = st.getPredicate(); | |
String key = decodeString(p.getLocalName()); | |
BeanPropertyMeta pMeta = m.getPropertyMeta(key); | |
setCurrentProperty(pMeta); | |
if (pMeta != null) { | |
RDFNode o = st.getObject(); | |
ClassMeta<?> cm = pMeta.getClassMeta(); | |
if (cm.isCollectionOrArray() && isMultiValuedCollections(pMeta)) { | |
ClassMeta<?> et = cm.getElementType(); | |
Object value = parseAnything(et, o, m.getBean(false), pMeta); | |
setName(et, value, key); | |
pMeta.add(m, key, value); | |
} else { | |
Object value = parseAnything(cm, o, m.getBean(false), pMeta); | |
setName(cm, value, key); | |
pMeta.set(m, key, value); | |
} | |
} else if (! (p.equals(pRoot) || p.equals(pType))) { | |
onUnknownProperty(key, m); | |
} | |
setCurrentProperty(null); | |
} | |
return m; | |
} | |
private boolean isMultiValuedCollections(BeanPropertyMeta pMeta) { | |
RdfBeanPropertyMeta bpRdf = (pMeta == null ? RdfBeanPropertyMeta.DEFAULT : getRdfBeanPropertyMeta(pMeta)); | |
if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT) | |
return bpRdf.getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED; | |
return getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED; | |
} | |
private <T> T parseAnything(ClassMeta<?> eType, RDFNode n, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
if (eType == null) | |
eType = object(); | |
PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this); | |
BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); | |
ClassMeta<?> sType = null; | |
if (builder != null) | |
sType = builder.getBuilderClassMeta(this); | |
else if (swap != null) | |
sType = swap.getSwapClassMeta(this); | |
else | |
sType = eType; | |
if (sType.isOptional()) | |
return (T)Optional.ofNullable(parseAnything(eType.getElementType(), n, outer, pMeta)); | |
setCurrentClass(sType); | |
if (! sType.canCreateNewInstance(outer)) { | |
if (n.isResource()) { | |
Statement st = n.asResource().getProperty(pType); | |
if (st != null) { | |
String c = st.getLiteral().getString(); | |
ClassMeta tcm = getClassMeta(c, pMeta, eType); | |
if (tcm != null) | |
sType = eType = tcm; | |
} | |
} | |
} | |
Object o = null; | |
if (n.isResource() && n.asResource().getURI() != null && n.asResource().getURI().equals(RDF_NIL)) { | |
// Do nothing. Leave o == null. | |
} else if (sType.isObject()) { | |
if (n.isLiteral()) { | |
o = n.asLiteral().getValue(); | |
if (o instanceof String) { | |
o = decodeString(o); | |
} | |
} | |
else if (n.isResource()) { | |
Resource r = n.asResource(); | |
if (! urisVisited.add(r)) | |
o = r.getURI(); | |
else if (r.getProperty(pValue) != null) { | |
o = parseAnything(object(), n.asResource().getProperty(pValue).getObject(), outer, null); | |
} else if (isSeq(r)) { | |
o = new ObjectList(this); | |
parseIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta); | |
} else if (isBag(r)) { | |
o = new ObjectList(this); | |
parseIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta); | |
} else if (r.canAs(RDFList.class)) { | |
o = new ObjectList(this); | |
parseIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta); | |
} else { | |
// If it has a URI and no child properties, we interpret this as an | |
// external resource, and convert it to just a URL. | |
String uri = r.getURI(); | |
if (uri != null && ! r.listProperties().hasNext()) { | |
o = r.getURI(); | |
} else { | |
ObjectMap m2 = new ObjectMap(this); | |
parseIntoMap(r, m2, null, null, pMeta); | |
o = cast(m2, pMeta, eType); | |
} | |
} | |
} else { | |
throw new ParseException(this, "Unrecognized node type ''{0}'' for object", n); | |
} | |
} else if (sType.isBoolean()) { | |
o = convertToType(getValue(n, outer), boolean.class); | |
} else if (sType.isCharSequence()) { | |
o = decodeString(getValue(n, outer)); | |
} else if (sType.isChar()) { | |
o = parseCharacter(decodeString(getValue(n, outer))); | |
} else if (sType.isNumber()) { | |
o = parseNumber(getValue(n, outer).toString(), (Class<? extends Number>)sType.getInnerClass()); | |
} else if (sType.isMap()) { | |
Resource r = n.asResource(); | |
if (! urisVisited.add(r)) | |
return null; | |
Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this)); | |
o = parseIntoMap(r, m, eType.getKeyType(), eType.getValueType(), pMeta); | |
} else if (sType.isCollectionOrArray() || sType.isArgs()) { | |
if (sType.isArray() || sType.isArgs()) | |
o = new ArrayList(); | |
else | |
o = (sType.canCreateNewInstance(outer) ? (Collection<?>)sType.newInstance(outer) : new ObjectList(this)); | |
Resource r = n.asResource(); | |
if (! urisVisited.add(r)) | |
return null; | |
if (isSeq(r)) { | |
parseIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta); | |
} else if (isBag(r)) { | |
parseIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta); | |
} else if (r.canAs(RDFList.class)) { | |
parseIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta); | |
} else { | |
throw new ParseException(this, "Unrecognized node type ''{0}'' for collection", n); | |
} | |
if (sType.isArray() || sType.isArgs()) | |
o = toArray(sType, (Collection)o); | |
} else if (builder != null) { | |
Resource r = n.asResource(); | |
if (! urisVisited.add(r)) | |
return null; | |
BeanMap<?> bm = toBeanMap(builder.create(this, eType)); | |
o = builder.build(this, parseIntoBeanMap(r, bm).getBean(), eType); | |
} else if (sType.canCreateNewBean(outer)) { | |
Resource r = n.asResource(); | |
if (! urisVisited.add(r)) | |
return null; | |
BeanMap<?> bm = newBeanMap(outer, sType.getInnerClass()); | |
o = parseIntoBeanMap(r, bm).getBean(); | |
} else if (sType.isUri() && n.isResource()) { | |
o = sType.newInstanceFromString(outer, decodeString(n.asResource().getURI())); | |
} else if (sType.canCreateNewInstanceFromString(outer)) { | |
o = sType.newInstanceFromString(outer, decodeString(getValue(n, outer))); | |
} else if (n.isResource()) { | |
Resource r = n.asResource(); | |
Map m = new ObjectMap(this); | |
parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); | |
if (m.containsKey(getBeanTypePropertyName(eType))) | |
o = cast((ObjectMap)m, pMeta, eType); | |
else | |
throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); | |
} else { | |
throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); | |
} | |
if (swap != null && o != null) | |
o = unswap(swap, o, eType); | |
if (outer != null) | |
setParent(eType, o, outer); | |
return (T)o; | |
} | |
private boolean isSeq(RDFNode n) { | |
if (n.isResource()) { | |
Statement st = n.asResource().getProperty(pRdfType); | |
if (st != null) | |
return RDF_SEQ.equals(st.getResource().getURI()); | |
} | |
return false; | |
} | |
private boolean isBag(RDFNode n) { | |
if (n.isResource()) { | |
Statement st = n.asResource().getProperty(pRdfType); | |
if (st != null) | |
return RDF_BAG.equals(st.getResource().getURI()); | |
} | |
return false; | |
} | |
private Object getValue(RDFNode n, Object outer) throws IOException, ParseException, ExecutableException { | |
if (n.isLiteral()) | |
return n.asLiteral().getValue(); | |
if (n.isResource()) { | |
Statement st = n.asResource().getProperty(pValue); | |
if (st != null) { | |
n = st.getObject(); | |
if (n.isLiteral()) | |
return n.asLiteral().getValue(); | |
return parseAnything(object(), st.getObject(), outer, null); | |
} | |
} | |
throw new ParseException(this, "Unknown value type for node ''{0}''", n); | |
} | |
private <K,V> Map<K,V> parseIntoMap(Resource r, Map<K,V> m, ClassMeta<K> keyType, | |
ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
// Add URI as "uri" to generic maps. | |
if (r.getURI() != null) { | |
K uri = convertAttrToType(m, "uri", keyType); | |
V value = convertAttrToType(m, r.getURI(), valueType); | |
m.put(uri, value); | |
} | |
for (StmtIterator i = r.listProperties(); i.hasNext();) { | |
Statement st = i.next(); | |
Property p = st.getPredicate(); | |
String key = p.getLocalName(); | |
if (! (key.equals("root") && p.getURI().equals(getJuneauNs().getUri()))) { | |
key = decodeString(key); | |
RDFNode o = st.getObject(); | |
K key2 = convertAttrToType(m, key, keyType); | |
V value = parseAnything(valueType, o, m, pMeta); | |
setName(valueType, value, key); | |
m.put(key2, value); | |
} | |
} | |
return m; | |
} | |
private <E> Collection<E> parseIntoCollection(Container c, Collection<E> l, | |
ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
int argIndex = 0; | |
for (NodeIterator ni = c.iterator(); ni.hasNext();) { | |
E e = (E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); | |
l.add(e); | |
} | |
return l; | |
} | |
private <E> Collection<E> parseIntoCollection(RDFList list, Collection<E> l, | |
ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
int argIndex = 0; | |
for (ExtendedIterator<RDFNode> ni = list.iterator(); ni.hasNext();) { | |
E e = (E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); | |
l.add(e); | |
} | |
return l; | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Common properties | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Configuration property: RDF format for representing collections and arrays. | |
* | |
* @see RdfParser#RDF_collectionFormat | |
* @return | |
* RDF format for representing collections and arrays. | |
*/ | |
protected final RdfCollectionFormat getCollectionFormat() { | |
return ctx.getCollectionFormat(); | |
} | |
/** | |
* Configuration property: Default XML namespace for bean properties. | |
* | |
* @see RdfParser#RDF_juneauBpNs | |
* @return | |
* Default XML namespace for bean properties. | |
*/ | |
protected final Namespace getJuneauBpNs() { | |
return ctx.getJuneauBpNs(); | |
} | |
/** | |
* Configuration property: XML namespace for Juneau properties. | |
* | |
* @see RdfParser#RDF_juneauNs | |
* @return | |
* XML namespace for Juneau properties. | |
*/ | |
protected final Namespace getJuneauNs() { | |
return ctx.getJuneauNs(); | |
} | |
/** | |
* Configuration property: RDF language. | |
* | |
* @see RdfParser#RDF_language | |
* @return | |
* The RDF language to use. | |
*/ | |
protected final String getLanguage() { | |
return ctx.getLanguage(); | |
} | |
/** | |
* Configuration property: Collections should be serialized and parsed as loose collections. | |
* | |
* @see RdfParser#RDF_looseCollections | |
* @return | |
* <jk>true</jk> if collections of resources are handled as loose collections of resources in RDF instead of | |
* resources that are children of an RDF collection (e.g. Sequence, Bag). | |
*/ | |
protected final boolean isLooseCollections() { | |
return ctx.isLooseCollections(); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Jena properties | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Configuration property: All Jena-related configuration properties. | |
* | |
* @return | |
* A map of all Jena-related configuration properties. | |
*/ | |
protected final Map<String,Object> getJenaProperties() { | |
return ctx.getJenaProperties(); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Properties | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Configuration property: Trim whitespace from text elements. | |
* | |
* @see RdfParser#RDF_trimWhitespace | |
* @return | |
* <jk>true</jk> if whitespace in text elements will be automatically trimmed. | |
*/ | |
protected final boolean isTrimWhitespace() { | |
return ctx.isTrimWhitespace(); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Extended metadata | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Returns the language-specific metadata on the specified class. | |
* | |
* @param cm The class to return the metadata on. | |
* @return The metadata. | |
*/ | |
protected RdfClassMeta getRdfClassMeta(ClassMeta<?> cm) { | |
return ctx.getRdfClassMeta(cm); | |
} | |
/** | |
* Returns the language-specific metadata on the specified bean. | |
* | |
* @param bm The bean to return the metadata on. | |
* @return The metadata. | |
*/ | |
protected RdfBeanMeta getRdfBeanMeta(BeanMeta<?> bm) { | |
return ctx.getRdfBeanMeta(bm); | |
} | |
/** | |
* Returns the language-specific metadata on the specified bean property. | |
* | |
* @param bpm The bean property to return the metadata on. | |
* @return The metadata. | |
*/ | |
protected RdfBeanPropertyMeta getRdfBeanPropertyMeta(BeanPropertyMeta bpm) { | |
return ctx.getRdfBeanPropertyMeta(bpm); | |
} | |
/** | |
* Returns the language-specific metadata on the specified bean property. | |
* | |
* @param bpm The bean property to return the metadata on. | |
* @return The metadata. | |
*/ | |
protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) { | |
return ctx.getXmlBeanPropertyMeta(bpm); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Other methods | |
//----------------------------------------------------------------------------------------------------------------- | |
@Override /* Session */ | |
public ObjectMap toMap() { | |
return super.toMap() | |
.append("RdfParserSession", new DefaultFilteringObjectMap() | |
); | |
} | |
} |