001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.jena;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.marshall.jena.Constants.*;
023
024import java.io.*;
025import java.util.*;
026import java.util.concurrent.atomic.*;
027
028import org.apache.jena.rdf.model.*;
029import org.apache.jena.riot.*;
030import org.apache.jena.util.iterator.*;
031import org.apache.juneau.commons.bean.*;
032import org.apache.juneau.commons.reflect.*;
033import org.apache.juneau.marshall.*;
034import org.apache.juneau.marshall.collections.*;
035import org.apache.juneau.marshall.parser.*;
036import org.apache.juneau.marshall.swap.spi.*;
037import org.apache.juneau.marshall.xml.*;
038
039/**
040 * Session object that lives for the duration of a single use of {@link RdfParser}.
041 *
042 * <h5 class='section'>Notes:</h5><ul>
043 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
044 * </ul>
045 *
046 * <h5 class='section'>See Also:</h5><ul>
047 *    <li class='link'>{doc jmr.RdfDetails}
048
049 * </ul>
050 */
051@SuppressWarnings({
052   "java:S138", // RDF parsing requires complex dispatch logic
053   "java:S3776", // Cognitive complexity - RDF graph traversal is inherently complex
054   "java:S6541", // Brain method - RDF graph-to-POJO conversion requires complex dispatch
055   "rawtypes", // Raw types necessary for generic type handling
056   "unchecked" // Type erasure requires unchecked casts
057})
058public class RdfParserSession extends ReaderParserSession {
059
060   /**
061    * Builder class.
062    */
063   public static class Builder extends ReaderParserSession.Builder<Builder> {
064
065      private final RdfParser ctx;
066
067      /**
068       * Constructor
069       *
070       * @param ctx The context creating this session.
071       *    <br>Cannot be <jk>null</jk>.
072       */
073      protected Builder(RdfParser ctx) {
074         super(ctx);
075         this.ctx = ctx;
076      }
077
078      @Override
079      public RdfParserSession build() {
080         return new RdfParserSession(this);
081      }
082
083   }
084
085   /**
086    * Creates a new builder for this object.
087    *
088    * @param ctx The context creating this session.
089    *    <br>Cannot be <jk>null</jk>.
090    * @return A new builder.
091    */
092   public static Builder create(RdfParser ctx) {
093      return new Builder(ctx);
094   }
095
096   private final Model model;
097   private final org.apache.jena.rdf.model.Property pRoot;
098   private final org.apache.jena.rdf.model.Property pValue;
099   private final org.apache.jena.rdf.model.Property pType;
100   private final org.apache.jena.rdf.model.Property pRdfType;
101   private final Lang lang;
102   private final RdfParser ctx;
103   private final Set<Resource> urisVisited = st();
104
105   /**
106    * Constructor.
107    *
108    * @param builder The builder for this object.
109    *    <br>Cannot be <jk>null</jk>.
110    */
111   protected RdfParserSession(Builder builder) {
112      super(builder);
113      ctx = builder.ctx;
114      model = ModelFactory.createDefaultModel();
115      addModelPrefix(ctx.getJuneauNs());
116      addModelPrefix(ctx.getJuneauBpNs());
117      pRdfType = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
118      pRoot = model.createProperty(ctx.getJuneauNs().getUri(), "root");
119      pType = model.createProperty(ctx.getJuneauBpNs().getUri(), "_type");
120      pValue = model.createProperty(ctx.getJuneauNs().getUri(), "value");
121
122      // Map legacy language names to RIOT Lang
123      var langName = ctx.getLanguage();
124      lang = toLang(langName);
125      if (lang == null)
126         throw isex("Unknown RDF language: %s", langName);
127   }
128
129   // Package-private (not private) for white-box testing.
130   static Lang toLang(String langName) {
131      var lang = RDFLanguages.nameToLang(langName);
132      if (lang != null)
133         return lang;
134      if (eq(langName, "RDF/PROTO"))
135         return Lang.RDFPROTO;
136      return null;
137   }
138
139   private final void addModelPrefix(Namespace ns) {
140      model.setNsPrefix(ns.getName(), ns.getUri());
141   }
142
143   /**
144    * Decodes the specified string.
145    * If {@link RdfParser#RDF_trimWhitespace} is <jk>true</jk>, the resulting string is trimmed before decoding.
146    * If {@link #isTrimStrings()} is <jk>true</jk>, the resulting string is trimmed after decoding.
147    */
148   // Package-private (not private) for white-box testing.
149   String decodeString(Object o) {
150      if (o == null)
151         return null;
152      var s = o.toString();
153      if (s.isEmpty())
154         return s;
155      if (isTrimWhitespace())
156         s = s.trim();
157      s = XmlUtils.decode(s, null);
158      if (isTrimStrings())
159         s = s.trim();
160      return s;
161   }
162
163   /*
164    * Finds the roots in the model using either the "root" property to identify it,
165    *    or by resorting to scanning the model for all nodes with no incoming predicates.
166    */
167   // Package-private (not private) for white-box testing.
168   List<Resource> getRoots(Model m) {
169      var l = new LinkedList<Resource>();
170
171      // First try to find the root using the "http://www.apache.org/juneau/root" property.
172      var root = m.createProperty(getJuneauNs().getUri(), "root");
173      for (ResIterator i = m.listResourcesWithProperty(root); i.hasNext();)
174         l.add(i.next());
175
176      if (! l.isEmpty())
177         return l;
178
179      // Otherwise, we need to find all resources that aren't objects.
180      // We want to explicitly ignore statements where the subject
181      // and object are the same node.
182      var objects = new HashSet<RDFNode>();
183      for (StmtIterator i = m.listStatements(); i.hasNext();) {
184         var st = i.next();
185         var subject = st.getSubject();
186         var object = st.getObject();
187         if (object.isResource() && ! object.equals(subject))
188            objects.add(object);
189      }
190      for (ResIterator i = m.listSubjects(); i.hasNext();) {
191         var r = i.next();
192         if (! objects.contains(r))
193            l.add(r);
194      }
195      return l;
196   }
197
198   // Package-private (not private) for white-box testing.
199   Object getValue(RDFNode n, Object outer) throws ParseException {
200      if (n.isLiteral())
201         return n.asLiteral().getValue();
202      if (n.isResource()) { // HTT: RDFNode has only two concrete kinds (Literal, Resource); this is always true once isLiteral() is false.
203         var st = n.asResource().getProperty(pValue);
204         if (nn(st)) {
205            n = st.getObject();
206            if (n.isLiteral())
207               return n.asLiteral().getValue();
208            return readAnything(object(), st.getObject(), outer, null);
209         }
210      }
211      throw new ParseException(this, "Unknown value type for node '%s'", n);
212   }
213
214   // Package-private (not private) for white-box testing.
215   boolean isBag(RDFNode n) {
216      if (n.isResource()) {
217         var st = n.asResource().getProperty(pRdfType);
218         if (nn(st))
219            return RDF_BAG.equals(st.getResource().getURI());
220      }
221      return false;
222   }
223
224   // Package-private (not private) for white-box testing.
225   boolean isMultiValuedCollections(BeanPropertyMeta pMeta) {
226      var bpRdf = getRdfBeanPropertyMeta(pMeta);
227
228      if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
229         return bpRdf.getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED;
230
231      return getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED;
232   }
233
234   // Package-private (not private) for white-box testing.
235   boolean isSeq(RDFNode n) {
236      if (n.isResource()) {
237         var st = n.asResource().getProperty(pRdfType);
238         if (nn(st))
239            return RDF_SEQ.equals(st.getResource().getURI());
240      }
241      return false;
242   }
243
244   // Package-private (not private) for white-box testing.
245   <T> T readAnything(ClassMeta<?> eType, RDFNode n, Object outer, BeanPropertyMeta pMeta) throws ParseException {
246
247      if (eType == null)
248         eType = object();
249      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
250      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
251      ClassMeta<?> sType;
252      if (nn(builder))
253         sType = builder.getBuilderClassMeta(this);
254      else if (nn(swap))
255         sType = swap.getSwapClassMeta(this);
256      else
257         sType = eType;
258
259      if (sType.isOptional())
260         return (T)o(readAnything(eType.getElementType(), n, outer, pMeta));
261
262      setCurrentClass(sType);
263
264      if (! sType.canCreateNewInstance(outer) && n.isResource()) {
265         var st = n.asResource().getProperty(pType);
266         if (nn(st)) {
267            var c = st.getLiteral().getString();
268            var tcm = getClassMeta(c, pMeta, eType);
269            if (nn(tcm))
270               sType = eType = tcm;
271         }
272      }
273
274      Object o = null;
275      if (n.isResource() && nn(n.asResource().getURI()) && n.asResource().getURI().equals(RDF_NIL)) {
276         // Do nothing.  Leave o == null.
277      } else if (sType.isObject()) {
278         if (n.isLiteral()) {
279            o = n.asLiteral().getValue();
280            if (o instanceof String o2) {
281               o = decodeString(o2);
282            }
283         } else if (n.isResource()) { // HTT: RDFNode has only two concrete kinds (Literal, Resource); this is always true once isLiteral() (above) is false -- the else below is defensive dead code.
284            var r = n.asResource();
285            if (! urisVisited.add(r))
286               o = r.getURI();
287            else if (nn(r.getProperty(pValue))) {
288               o = readAnything(object(), n.asResource().getProperty(pValue).getObject(), outer, null);
289            } else if (isSeq(r)) {
290               o = new JsonList(this);
291               readIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta);
292            } else if (isBag(r)) {
293               o = new JsonList(this);
294               readIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta);
295            } else if (r.canAs(RDFList.class)) {
296               o = new JsonList(this);
297               readIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta);
298            } else {
299               // If it has a URI and no child properties, we interpret this as an
300               // external resource, and convert it to just a URL.
301               var uri = r.getURI();
302               if (nn(uri) && ! r.listProperties().hasNext()) {
303                  o = r.getURI();
304               } else {
305                  var m2 = new JsonMap(this);
306                  readIntoMap(r, m2, null, null, pMeta);
307                  o = cast(m2, pMeta, eType);
308               }
309            }
310         } else { // HTT: unreachable -- n is always Literal or Resource; see guard above.
311            throw new ParseException(this, "Unrecognized node type '%s' for object", n); // HTT: unreachable -- see guard above.
312         }
313      } else if (nn(builder)) {
314         var r = n.asResource();
315         if (! urisVisited.add(r))
316            return null;
317         var bm = toBeanMap(builder.create(this, eType));
318         o = builder.build(this, checkRequired(readIntoBeanMap(r, bm)).getBean(), eType);
319      } else if (sType.canCreateNewBean(outer)) {
320         var r = n.asResource();
321         if (! urisVisited.add(r))
322            return null;
323         var bm = newBeanMap(outer, sType.inner());
324         o = checkRequired(readIntoBeanMap(r, bm)).getBean();
325      } else if (sType.isMap()) {
326         var r = n.asResource();
327         if (! urisVisited.add(r))
328            return null;
329         var m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
330         o = readIntoMap(r, m, eType.getKeyType(), eType.getValueType(), pMeta);
331      } else if (sType.isCollectionOrArray() || sType.isArgs()) { // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API -- always false here.
332         if (sType.isArray() || sType.isArgs()) // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API -- always false here.
333            o = list();
334         else
335            o = (sType.canCreateNewInstance(outer) ? (Collection<?>)sType.newInstance(outer) : new JsonList(this));
336         var r = n.asResource();
337         if (! urisVisited.add(r))
338            return null;
339         if (isSeq(r)) {
340            readIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta);
341         } else if (isBag(r)) {
342            readIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta);
343         } else if (r.canAs(RDFList.class)) {
344            readIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta);
345         } else {
346            throw new ParseException(this, "Unrecognized node type '%s' for collection", n);
347         }
348         if (sType.isArray() || sType.isArgs()) // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API -- always false here.
349            o = toArray(sType, (Collection)o);
350      } else if (sType.isCharSequence()) {
351         o = decodeString(getValue(n, outer));
352      } else if (sType.isChar()) {
353         o = parseCharacter(decodeString(getValue(n, outer)));
354      } else if (sType.isNumber()) {
355         o = parseNumber(getValue(n, outer).toString(), (Class<? extends Number>)sType.inner());
356      } else if (sType.isBoolean()) {
357         o = convertToType(getValue(n, outer), boolean.class);
358      } else if (sType.isUri() && n.isResource()) {
359         // RDF URIs must be reconstructed from the resource's URI string (not from a literal),
360         // so this branch is kept here to fire before the temporal/scalar coercion fallbacks.
361         o = sType.newInstanceFromString(outer, decodeString(n.asResource().getURI()));
362      } else if (sType.isDate()) {
363         o = readDate(getValue(n, outer).toString(), sType);
364      } else if (sType.isCalendar()) {
365         o = readCalendar(getValue(n, outer).toString(), sType);
366      } else if (sType.isTemporal()) {
367         o = readTemporal(getValue(n, outer).toString(), sType);
368      } else if (sType.isDuration()) {
369         o = readDuration(getValue(n, outer).toString());
370      } else if (sType.isPeriod()) {
371         o = readPeriod(getValue(n, outer).toString());
372      } else if (sType.canCreateNewInstanceFromString(outer)) {
373         o = sType.newInstanceFromString(outer, decodeString(getValue(n, outer)));
374      } else if (n.isResource()) {
375         var r = n.asResource();
376         var m = newGenericMap(sType);
377         readIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
378         if (m.containsKey(getBeanTypePropertyName(eType)))
379            o = cast((MarshalledMap)m, pMeta, eType);
380         else if (nn(sType.getProxyInvocationHandler()))
381            o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
382         else
383            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
384      } else {
385         throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
386      }
387
388      if (nn(swap) && nn(o))
389         o = unswap(swap, o, eType);
390
391      if (nn(outer))
392         setParent(eType, o, outer);
393
394      return (T)o;
395   }
396
397   // Package-private (not private) for white-box testing.
398   <T> BeanMap<T> readIntoBeanMap(Resource r2, BeanMap<T> m) throws ParseException {
399      var bm = m.getMeta();
400      var rbm = getRdfBeanMeta(bm);
401      if (rbm.hasBeanUri() && nn(r2.getURI()))
402         rbm.getBeanUriProperty().set(m, null, r2.getURI());
403      for (StmtIterator i = r2.listProperties(); i.hasNext();) {
404         var st = i.next();
405         var p = st.getPredicate();
406         var key = decodeString(p.getLocalName());
407         var pMeta = m.getPropertyMeta(key);
408         setCurrentProperty(pMeta);
409         if (nn(pMeta)) {
410            var o = st.getObject();
411            var cm = (ClassMeta<?>) pMeta.getBeanInfo();
412            if (cm.isCollectionOrArray() && isMultiValuedCollections(pMeta)) {
413               var et = cm.getElementType();
414               var value = readAnything(et, o, m.getBean(false), pMeta);
415               setName(et, value, key);
416               try {
417                  pMeta.add(m, key, value);
418               } catch (BeanRuntimeException e) {
419                  onBeanSetterException(pMeta, e);
420                  throw e;
421               }
422            } else {
423               var value = readAnything(cm, o, m.getBean(false), pMeta);
424               setName(cm, value, key);
425               try {
426                  pMeta.set(m, key, value);
427               } catch (BeanRuntimeException e) {
428                  onBeanSetterException(pMeta, e);
429                  throw e;
430               }
431            }
432         } else if (! (p.equals(pRoot) || p.equals(pType))) {
433            var o = st.getObject();
434            var value = readAnything(object(), o, m.getBean(false), null);
435            onUnknownProperty(key, m, value);
436         }
437         setCurrentProperty(null);
438      }
439      return m;
440   }
441
442   // Package-private (not private) for white-box testing.
443   <E> Collection<E> readIntoCollection(Container c, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws ParseException {
444      int argIndex = 0;
445      for (NodeIterator ni = c.iterator(); ni.hasNext();) {
446         E e = (E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API.
447         l.add(e);
448      }
449      return l;
450   }
451
452   // Package-private (not private) for white-box testing.
453   <E> Collection<E> readIntoCollection(RDFList list, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws ParseException {
454      int argIndex = 0;
455      for (ExtendedIterator<RDFNode> ni = list.iterator(); ni.hasNext();) {
456         E e = (E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API.
457         l.add(e);
458      }
459      return l;
460   }
461
462   // Package-private (not private) for white-box testing.
463   <K,V> Map<K,V> readIntoMap(Resource r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws ParseException {
464      // Add URI as "uri" to generic maps.
465      if (nn(r.getURI())) {
466         var uri = convertAttrToType(m, "uri", keyType);
467         var value = convertAttrToType(m, r.getURI(), valueType);
468         m.put(uri, value);
469      }
470      for (StmtIterator i = r.listProperties(); i.hasNext();) {
471         var st = i.next();
472         var p = st.getPredicate();
473         var key = p.getLocalName();
474         if (! (eq(key, "root") && p.getURI().equals(getJuneauNs().getUri()))) { // Bug: p.getURI() is the property's FULL URI, never equal to the bare namespace URI on the right; the inner && is therefore always false and this guard's "skip" branch is permanently unreachable.
475            key = decodeString(key);
476            var o = st.getObject();
477            var key2 = convertAttrToType(m, key, keyType);
478            V value = readAnything(valueType, o, m, pMeta);
479            setName(valueType, value, key);
480            m.put(key2, value);
481         }
482
483      }
484      return m;
485   }
486
487   @Override /* Overridden from ReaderParserSession */
488   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
489
490      // RDFDataMgr.read accepts StringReader; use RDFParser for Reader input
491      var content = pipe.asString();
492      RDFParser.fromString(content, lang)
493         .base("http://unknown/")
494         .parse(model);
495
496      var roots = getRoots(model);
497
498      // Special case where we're parsing a loose collection of resources.
499      if (isLooseCollections() && type.isCollectionOrArray()) {
500         Collection c;
501         if (type.isArray() || type.isArgs()) // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API.
502            c = list();
503         else
504            c = (type.canCreateNewInstance(getOuter()) ? (Collection<?>)type.newInstance(getOuter()) : new JsonList(this));
505
506         var argIndex = new AtomicInteger(0);
507         var c2 = c;
508         roots.forEach(x -> c2.add(readAnything(type.isArgs() ? type.getArg(argIndex.getAndIncrement()) : type.getElementType(), x, getOuter(), null))); // HTT: isArgs() guard -- same constraint as above.
509
510         if (type.isArray() || type.isArgs()) // HTT: isArgs() guard -- same constraint as above.
511            return (T)toArray(type, c);
512         return (T)c;
513      }
514
515      if (roots.isEmpty())
516         return type.isOptional() ? (T)oe() : null;
517
518      if (roots.size() > 1)
519         throw new ParseException(this, "Too many root nodes found in model:  %s", roots.size());
520      var resource = roots.get(0);
521
522      return readAnything(type, resource, getOuter(), null);
523   }
524
525   /**
526    * RDF format for representing collections and arrays.
527    *
528    * @see RdfParser.Builder#collectionFormat(RdfCollectionFormat)
529    * @return
530    *    RDF format for representing collections and arrays.
531    */
532   protected final RdfCollectionFormat getCollectionFormat() { return ctx.getCollectionFormat(); }
533
534   /**
535    * All Jena-related configuration properties.
536    *
537    * @return
538    *    A map of all Jena-related configuration properties.
539    */
540   protected final Map<String,Object> getJenaSettings() { return ctx.getJenaSettings(); }
541
542   /**
543    * Default XML namespace for bean properties.
544    *
545    * @see RdfParser.Builder#juneauBpNs(Namespace)
546    * @return
547    *    Default XML namespace for bean properties.
548    */
549   protected final Namespace getJuneauBpNs() { return ctx.getJuneauBpNs(); }
550
551   /**
552    * XML namespace for Juneau properties.
553    *
554    * @see RdfParser.Builder#juneauNs(Namespace)
555    * @return
556    *    XML namespace for Juneau properties.
557    */
558   protected final Namespace getJuneauNs() { return ctx.getJuneauNs(); }
559
560   /**
561    * RDF language.
562    *
563    * @see RdfParser.Builder#language(String)
564    * @return
565    *    The RDF language to use.
566    */
567   protected final String getLanguage() { return ctx.getLanguage(); }
568
569   /**
570    * Returns the language-specific metadata on the specified bean.
571    *
572    * @param bm The bean to return the metadata on.
573    * @return The metadata.
574    */
575   protected RdfBeanMeta getRdfBeanMeta(BeanMeta<?> bm) {
576      return ctx.getRdfBeanMeta(bm);
577   }
578
579   /**
580    * Returns the language-specific metadata on the specified bean property.
581    *
582    * @param bpm The bean property to return the metadata on.
583    * @return The metadata.
584    */
585   protected RdfBeanPropertyMeta getRdfBeanPropertyMeta(BeanPropertyMeta bpm) {
586      return ctx.getRdfBeanPropertyMeta(bpm);
587   }
588
589   /**
590    * Returns the language-specific metadata on the specified class.
591    *
592    * @param cm The class to return the metadata on.
593    * @return The metadata.
594    */
595   protected RdfClassMeta getRdfClassMeta(ClassMeta<?> cm) {
596      return ctx.getRdfClassMeta(cm);
597   }
598
599   /**
600    * Returns the language-specific metadata on the specified bean property.
601    *
602    * @param bpm The bean property to return the metadata on.
603    * @return The metadata.
604    */
605   protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
606      return ctx.getXmlBeanPropertyMeta(bpm);
607   }
608
609   /**
610    * Collections should be serialized and parsed as loose collections.
611    *
612    * @see RdfParser.Builder#looseCollections()
613    * @return
614    *    <jk>true</jk> if collections of resources are handled as loose collections of resources in RDF instead of
615    *    resources that are children of an RDF collection (e.g. Sequence, Bag).
616    */
617   protected final boolean isLooseCollections() { return ctx.isLooseCollections(); }
618
619   /**
620    * Trim whitespace from text elements.
621    *
622    * @see RdfParser.Builder#trimWhitespace()
623    * @return
624    *    <jk>true</jk> if whitespace in text elements will be automatically trimmed.
625    */
626   protected final boolean isTrimWhitespace() { return ctx.isTrimWhitespace(); }
627}