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 RdfStreamParser}.
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 * </ul>
049 */
050@SuppressWarnings({
051   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
052   "java:S3776", // Cognitive complexity acceptable for RDF stream parser state machine methods
053   "java:S6541", // Brain Method complexity acceptable for core RDF stream parser dispatch logic
054   "rawtypes", // Raw types necessary for generic type handling
055   "unchecked" // Type erasure requires unchecked casts
056})
057public class RdfStreamParserSession extends InputStreamParserSession {
058
059   /**
060    * Builder class.
061    */
062   public static class Builder extends InputStreamParserSession.Builder<Builder> {
063
064      private final RdfStreamParser ctx;
065
066      /**
067       * Constructor
068       *
069       * @param ctx The context creating this session.
070       *    <br>Cannot be <jk>null</jk>.
071       */
072      protected Builder(RdfStreamParser ctx) {
073         super(reqnn("ctx", ctx));
074         this.ctx = ctx;
075      }
076
077      @Override
078      public RdfStreamParserSession build() {
079         return new RdfStreamParserSession(this);
080      }
081
082   }
083
084   /**
085    * Creates a new builder for this object.
086    *
087    * @param ctx The context creating this session.
088    *    <br>Cannot be <jk>null</jk>.
089    * @return A new builder.
090    */
091   public static Builder create(RdfStreamParser ctx) {
092      return new Builder(reqnn("ctx", ctx));
093   }
094
095   private final Model model;
096   private final org.apache.jena.rdf.model.Property pRoot;
097   private final org.apache.jena.rdf.model.Property pValue;
098   private final org.apache.jena.rdf.model.Property pType;
099   private final org.apache.jena.rdf.model.Property pRdfType;
100   private final Lang lang;
101   private final RdfStreamParser ctx;
102   private final Set<Resource> urisVisited = st();
103
104   /**
105    * Constructor.
106    *
107    * @param builder The builder for this object.
108    *    <br>Cannot be <jk>null</jk>.
109    */
110   protected RdfStreamParserSession(Builder builder) {
111      super(builder);
112      ctx = builder.ctx;
113      model = ModelFactory.createDefaultModel();
114      addModelPrefix(ctx.getJuneauNs());
115      addModelPrefix(ctx.getJuneauBpNs());
116      pRdfType = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
117      pRoot = model.createProperty(ctx.getJuneauNs().getUri(), "root");
118      pType = model.createProperty(ctx.getJuneauBpNs().getUri(), "_type");
119      pValue = model.createProperty(ctx.getJuneauNs().getUri(), "value");
120
121      // Map legacy language names to RIOT Lang
122      var langName = ctx.getLanguage();
123      lang = toLang(langName);
124      if (lang == null)
125         throw isex("Unknown RDF language: %s", langName);
126   }
127
128   @Override /* InputStreamParserSession */
129   public boolean hasNativeBytes() {
130      // The RDF graph layer has no native byte-array literal type — byte[] is surfaced either as an
131      // xsd:base64Binary typed literal (at BinaryFormat.NOT_SET) or as a plain string literal
132      // carrying the configured BinaryFormat's text wire form (at any other BinaryFormat; BinarySwap fires
133      // to reverse the wire form back to byte[]).
134      return false;
135   }
136
137   private static Lang toLang(String langName) {
138      var lang = RDFLanguages.nameToLang(langName);
139      if (lang != null)
140         return lang;
141      if (eq(langName, "RDF/PROTO")) // HTT - not registered in Jena's RDFLanguages
142         return Lang.RDFPROTO;
143      return null;
144   }
145
146   private void addModelPrefix(Namespace ns) {
147      model.setNsPrefix(ns.getName(), ns.getUri());
148   }
149
150   /**
151    * Decodes the specified string.
152    * If {@link RdfStreamParser#isTrimWhitespace} is <jk>true</jk>, the resulting string is trimmed before decoding.
153    * If {@link #isTrimStrings()} is <jk>true</jk>, the resulting string is trimmed after decoding.
154    */
155   String decodeString(Object o) {
156      if (o == null)
157         return null;
158      var s = o.toString();
159      if (s.isEmpty())
160         return s;
161      if (isTrimWhitespace()) // HTT: RdfStreamParser.Builder only exposes language(); trimWhitespace is always false for stream parsers.
162         s = s.trim(); // HTT: unreachable — see guard above.
163      s = XmlUtils.decode(s, null);
164      if (isTrimStrings())
165         s = s.trim();
166      return s;
167   }
168
169   /*
170    * Finds the roots in the model using either the "root" property to identify it,
171    *    or by resorting to scanning the model for all nodes with no incoming predicates.
172    */
173   List<Resource> getRoots(Model m) {
174      var l = new LinkedList<Resource>();
175
176      // First try to find the root using the "http://www.apache.org/juneau/root" property.
177      var root = m.createProperty(getJuneauNs().getUri(), "root");
178      for (ResIterator i = m.listResourcesWithProperty(root); i.hasNext();)
179         l.add(i.next());
180
181      if (! l.isEmpty())
182         return l;
183
184      // Otherwise, we need to find all resources that aren't objects.
185      // We want to explicitly ignore statements where the subject
186      // and object are the same node.
187      var objects = new HashSet<RDFNode>();
188      for (StmtIterator i = m.listStatements(); i.hasNext();) {
189         var st = i.next();
190         var subject = st.getSubject();
191         var object = st.getObject();
192         if (object.isResource() && ! object.equals(subject))
193            objects.add(object);
194      }
195      for (ResIterator i = m.listSubjects(); i.hasNext();) {
196         var r = i.next();
197         if (! objects.contains(r))
198            l.add(r);
199      }
200      return l;
201   }
202
203   Object getValue(RDFNode n, Object outer) throws ParseException {
204      if (n.isLiteral())
205         return n.asLiteral().getValue();
206      if (n.isResource()) { // HTT: RDFNode has only two concrete kinds (Literal, Resource); this is always true once isLiteral() is false.
207         var st = n.asResource().getProperty(pValue);
208         if (nn(st)) {
209            n = st.getObject();
210            if (n.isLiteral())
211               return n.asLiteral().getValue();
212            return readAnything(object(), st.getObject(), outer, null);
213         }
214      }
215      throw new ParseException(this, "Unknown value type for node '%s'", n);
216   }
217
218   boolean isBag(RDFNode n) {
219      if (n.isResource()) {
220         var st = n.asResource().getProperty(pRdfType);
221         if (nn(st))
222            return RDF_BAG.equals(st.getResource().getURI());
223      }
224      return false;
225   }
226
227   boolean isMultiValuedCollections(BeanPropertyMeta pMeta) {
228      var bpRdf = (pMeta == null ? RdfBeanPropertyMeta.DEFAULT : getRdfBeanPropertyMeta(pMeta));
229
230      if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
231         return bpRdf.getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED;
232
233      return getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED; // HTT: RdfStreamParser.Builder only exposes language(); the global collection format is always DEFAULT (never MULTI_VALUED) for stream parsers.
234   }
235
236   boolean isSeq(RDFNode n) {
237      if (n.isResource()) {
238         var st = n.asResource().getProperty(pRdfType);
239         if (nn(st))
240            return RDF_SEQ.equals(st.getResource().getURI());
241      }
242      return false;
243   }
244
245   <T> T readAnything(ClassMeta<?> eType, RDFNode n, Object outer, BeanPropertyMeta pMeta) throws ParseException, ExecutableException {
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 {
311            throw new ParseException(this, "Unrecognized node type '%s' for object", n); // HTT: unreachable — see guard above; n is always Literal or Resource.
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.isByteArray()) {
332         // Correctness gate: byte[] satisfies isArray()/isCollectionOrArray() too, but RDF emits
333         // byte[] as a typed base64 literal (not as a Seq/Bag/RDFList of byte resources).  This
334         // branch MUST stay above isCollectionOrArray so the literal path fires first.
335         var v = getValue(n, outer);
336         if (v instanceof byte[] b) {
337            o = b;
338         } else if (v != null) {
339            o = Base64.getDecoder().decode(v.toString());
340         }
341      } else if (sType.isCollectionOrArray() || sType.isArgs()) { // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API — always false here.
342         if (sType.isArray() || sType.isArgs()) // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API — always false here.
343            o = list();
344         else
345            o = (sType.canCreateNewInstance(outer) ? (Collection<?>)sType.newInstance(outer) : new JsonList(this));
346         var r = n.asResource();
347         if (! urisVisited.add(r))
348            return null;
349         if (isSeq(r)) {
350            readIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta);
351         } else if (isBag(r)) {
352            readIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta);
353         } else if (r.canAs(RDFList.class)) {
354            readIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta);
355         } else {
356            throw new ParseException(this, "Unrecognized node type '%s' for collection", n);
357         }
358         if (sType.isArray() || sType.isArgs()) // HTT: isArgs() requires method-argument list ClassMeta; not reachable through public parse() API — always false here.
359            o = toArray(sType, (Collection)o);
360      } else if (sType.isCharSequence()) {
361         o = decodeString(getValue(n, outer));
362      } else if (sType.isChar()) {
363         o = parseCharacter(decodeString(getValue(n, outer)));
364      } else if (sType.isNumber()) {
365         o = parseNumber(getValue(n, outer).toString(), (Class<? extends Number>)sType.inner());
366      } else if (sType.isBoolean()) {
367         o = convertToType(getValue(n, outer), boolean.class);
368      } else if (sType.isUri() && n.isResource()) {
369         // RDF URIs must be reconstructed from the resource's URI string (not from a literal),
370         // so this branch is kept here to fire before the temporal/scalar coercion fallbacks.
371         o = sType.newInstanceFromString(outer, decodeString(n.asResource().getURI()));
372      } else if (sType.isDate()) {
373         o = readDate(getValue(n, outer).toString(), sType);
374      } else if (sType.isCalendar()) {
375         o = readCalendar(getValue(n, outer).toString(), sType);
376      } else if (sType.isTemporal()) {
377         o = readTemporal(getValue(n, outer).toString(), sType);
378      } else if (sType.isDuration()) {
379         o = readDuration(getValue(n, outer).toString());
380      } else if (sType.isPeriod()) {
381         o = readPeriod(getValue(n, outer).toString());
382      } else if (sType.canCreateNewInstanceFromString(outer)) {
383         o = sType.newInstanceFromString(outer, decodeString(getValue(n, outer)));
384      } else if (n.isResource()) {
385         var r = n.asResource();
386         var m = newGenericMap(sType);
387         readIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
388         if (m.containsKey(getBeanTypePropertyName(eType)))
389            o = cast((MarshalledMap)m, pMeta, eType);
390         else if (nn(sType.getProxyInvocationHandler()))
391            o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
392         else
393            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
394      } else {
395         throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
396      }
397
398      if (nn(swap) && nn(o))
399         o = unswap(swap, o, eType);
400
401      if (nn(outer))
402         setParent(eType, o, outer);
403
404      return (T)o;
405   }
406
407   <T> BeanMap<T> readIntoBeanMap(Resource r2, BeanMap<T> m) throws ParseException, ExecutableException {
408      var bm = m.getMeta();
409      var rbm = getRdfBeanMeta(bm);
410      if (rbm.hasBeanUri() && nn(r2.getURI()))
411         rbm.getBeanUriProperty().set(m, null, r2.getURI());
412      for (StmtIterator i = r2.listProperties(); i.hasNext();) {
413         var st = i.next();
414         var p = st.getPredicate();
415         var key = decodeString(p.getLocalName());
416         var pMeta = m.getPropertyMeta(key);
417         setCurrentProperty(pMeta);
418         if (nn(pMeta)) {
419            var o = st.getObject();
420            var cm = (ClassMeta<?>) pMeta.getBeanInfo();
421            if (cm.isCollectionOrArray() && isMultiValuedCollections(pMeta)) {
422               var et = cm.getElementType();
423               var value = readAnything(et, o, m.getBean(false), pMeta);
424               setName(et, value, key);
425               try {
426                  pMeta.add(m, key, value);
427               } catch (BeanRuntimeException e) {
428                  onBeanSetterException(pMeta, e);
429                  throw e;
430               }
431            } else {
432               var value = readAnything(cm, o, m.getBean(false), pMeta);
433               setName(cm, value, key);
434               try {
435                  pMeta.set(m, key, value);
436               } catch (BeanRuntimeException e) {
437                  onBeanSetterException(pMeta, e);
438                  throw e;
439               }
440            }
441         } else if (! (p.equals(pRoot) || p.equals(pType))) {
442            var o = st.getObject();
443            var value = readAnything(object(), o, m.getBean(false), null);
444            onUnknownProperty(key, m, value);
445         }
446         setCurrentProperty(null);
447      }
448      return m;
449   }
450
451   <E> Collection<E> readIntoCollection(Container c, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws ParseException, ExecutableException {
452      int argIndex = 0;
453      for (NodeIterator ni = c.iterator(); ni.hasNext();) {
454         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.
455         l.add(e);
456      }
457      return l;
458   }
459
460   <E> Collection<E> readIntoCollection(RDFList list, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws ParseException, ExecutableException {
461      int argIndex = 0;
462      for (ExtendedIterator<RDFNode> ni = list.iterator(); ni.hasNext();) {
463         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.
464         l.add(e);
465      }
466      return l;
467   }
468
469   <K,V> Map<K,V> readIntoMap(Resource r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws ParseException, ExecutableException {
470      // Add URI as "uri" to generic maps.
471      if (nn(r.getURI())) {
472         var uri = convertAttrToType(m, "uri", keyType);
473         var value = convertAttrToType(m, r.getURI(), valueType);
474         m.put(uri, value);
475      }
476      for (StmtIterator i = r.listProperties(); i.hasNext();) {
477         var st = i.next();
478         var p = st.getPredicate();
479         var key = p.getLocalName();
480         if (! (eq(key, "root") && p.equals(pRoot))) {
481            key = decodeString(key);
482            var o = st.getObject();
483            var key2 = convertAttrToType(m, key, keyType);
484            V value = readAnything(valueType, o, m, pMeta);
485            setName(valueType, value, key);
486            m.put(key2, value);
487         }
488
489      }
490      return m;
491   }
492
493   @SuppressWarnings({
494      "resource" // Resource management handled by InputStreamParserSession
495   })
496   @Override /* Overridden from InputStreamParserSession */
497   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
498
499      RDFDataMgr.read(model, pipe.getInputStream(), lang);
500
501      var roots = getRoots(model);
502
503      // Special case where we're parsing a loose collection of resources.
504      if (isLooseCollections() && type.isCollectionOrArray()) { // HTT: RdfStreamParser.Builder only exposes language(); looseCollections is always false for stream parsers.
505         Collection c; // HTT: unreachable — see guard above.
506         if (type.isArray() || type.isArgs()) // HTT: unreachable — see guard above.
507            c = list(); // HTT: unreachable — see guard above.
508         else
509            c = (type.canCreateNewInstance(getOuter()) ? (Collection<?>)type.newInstance(getOuter()) : new JsonList(this)); // HTT: unreachable — see guard above.
510
511         var argIndex = new AtomicInteger(0); // HTT: unreachable — see guard above.
512         var c2 = c; // HTT: unreachable — see guard above.
513         roots.forEach(x -> c2.add(readAnything(type.isArgs() ? type.getArg(argIndex.getAndIncrement()) : type.getElementType(), x, getOuter(), null))); // HTT: unreachable — see guard above.
514
515         if (type.isArray() || type.isArgs()) // HTT: unreachable — see guard above.
516            return (T)toArray(type, c); // HTT: unreachable — see guard above.
517         return (T)c; // HTT: unreachable — see guard above.
518      }
519
520      if (roots.isEmpty())
521         return type.isOptional() ? (T)oe() : null;
522
523      if (roots.size() > 1)
524         throw new ParseException(this, "Too many root nodes found in model:  %s", roots.size());
525      var resource = roots.get(0);
526
527      return readAnything(type, resource, getOuter(), null);
528   }
529
530   /**
531    * RDF format for representing collections and arrays.
532    *
533    * @see RdfStreamParser#getCollectionFormat()
534    * @return
535    *    RDF format for representing collections and arrays.
536    */
537   protected final RdfCollectionFormat getCollectionFormat() { return ctx.getCollectionFormat(); }
538
539   /**
540    * Default XML namespace for bean properties.
541    *
542    * @see RdfStreamParser#getJuneauBpNs()
543    * @return
544    *    Default XML namespace for bean properties.
545    */
546   protected final Namespace getJuneauBpNs() { return ctx.getJuneauBpNs(); }
547
548   /**
549    * XML namespace for Juneau properties.
550    *
551    * @see RdfStreamParser#getJuneauNs()
552    * @return
553    *    XML namespace for Juneau properties.
554    */
555   protected final Namespace getJuneauNs() { return ctx.getJuneauNs(); }
556
557   /**
558    * RDF language.
559    *
560    * @see RdfStreamParser#getLanguage()
561    * @return
562    *    The RDF language to use.
563    */
564   protected final String getLanguage() { return ctx.getLanguage(); }
565
566   /**
567    * Returns the language-specific metadata on the specified bean.
568    *
569    * @param bm The bean to return the metadata on.
570    * @return The metadata.
571    */
572   protected RdfBeanMeta getRdfBeanMeta(BeanMeta<?> bm) {
573      return ctx.getRdfBeanMeta(bm);
574   }
575
576   /**
577    * Returns the language-specific metadata on the specified bean property.
578    *
579    * @param bpm The bean property to return the metadata on.
580    * @return The metadata.
581    */
582   protected RdfBeanPropertyMeta getRdfBeanPropertyMeta(BeanPropertyMeta bpm) {
583      return ctx.getRdfBeanPropertyMeta(bpm);
584   }
585
586   /**
587    * Returns the language-specific metadata on the specified class.
588    *
589    * @param cm The class to return the metadata on.
590    * @return The metadata.
591    */
592   protected RdfClassMeta getRdfClassMeta(ClassMeta<?> cm) {
593      return ctx.getRdfClassMeta(cm);
594   }
595
596   /**
597    * Returns the language-specific metadata on the specified bean property.
598    *
599    * @param bpm The bean property to return the metadata on.
600    * @return The metadata.
601    */
602   protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
603      return ctx.getXmlBeanPropertyMeta(bpm);
604   }
605
606   /**
607    * Collections should be serialized and parsed as loose collections.
608    *
609    * @see RdfStreamParser#isLooseCollections()
610    * @return
611    *    <jk>true</jk> if collections of resources are handled as loose collections of resources in RDF instead of
612    *    resources that are children of an RDF collection (e.g. Sequence, Bag).
613    */
614   protected final boolean isLooseCollections() { return ctx.isLooseCollections(); }
615
616   /**
617    * Trim whitespace from text elements.
618    *
619    * @see RdfStreamParser#isTrimWhitespace()
620    * @return
621    *    <jk>true</jk> if whitespace in text elements will be automatically trimmed.
622    */
623   protected final boolean isTrimWhitespace() { return ctx.isTrimWhitespace(); }
624}