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}