001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.jena; 014 015import static org.apache.juneau.internal.StringUtils.*; 016import static org.apache.juneau.jena.Constants.*; 017 018import java.io.IOException; 019import java.util.*; 020 021import org.apache.jena.rdf.model.*; 022import org.apache.jena.util.iterator.*; 023import org.apache.juneau.*; 024import org.apache.juneau.collections.*; 025import org.apache.juneau.parser.*; 026import org.apache.juneau.transform.*; 027import org.apache.juneau.xml.*; 028 029/** 030 * Session object that lives for the duration of a single use of {@link RdfParser}. 031 * 032 * <p> 033 * This class is NOT thread safe. 034 * It is typically discarded after one-time use although it can be reused against multiple inputs. 035 */ 036@SuppressWarnings({"unchecked", "rawtypes"}) 037public class RdfParserSession extends ReaderParserSession { 038 039 private final RdfParser ctx; 040 private final Property pRoot, pValue, pType, pRdfType; 041 private final Model model; 042 private final RDFReader rdfReader; 043 private final Set<Resource> urisVisited = new HashSet<>(); 044 045 /** 046 * Create a new session using properties specified in the context. 047 * 048 * @param ctx 049 * The context creating this session object. 050 * The context contains all the configuration settings for this object. 051 * @param args 052 * Runtime session arguments. 053 */ 054 protected RdfParserSession(RdfParser ctx, ParserSessionArgs args) { 055 super(ctx, args); 056 this.ctx = ctx; 057 model = ModelFactory.createDefaultModel(); 058 addModelPrefix(ctx.getJuneauNs()); 059 addModelPrefix(ctx.getJuneauBpNs()); 060 pRoot = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_ROOT); 061 pValue = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_VALUE); 062 pType = model.createProperty(ctx.getJuneauBpNs().getUri(), RDF_juneauNs_TYPE); 063 pRdfType = model.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); 064 rdfReader = model.getReader(ctx.getLanguage()); 065 066 // Note: NTripleReader throws an exception if you try to set any properties on it. 067 if (! ctx.getLanguage().equals(LANG_NTRIPLE)) { 068 for (Map.Entry<String,Object> e : ctx.jenaProperties.entrySet()) 069 rdfReader.setProperty(e.getKey(), e.getValue()); 070 } 071 } 072 073 @Override /* ReaderParserSession */ 074 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 075 076 RDFReader r = rdfReader; 077 r.read(model, pipe.getBufferedReader(), null); 078 079 List<Resource> roots = getRoots(model); 080 081 // Special case where we're parsing a loose collection of resources. 082 if (isLooseCollections() && type.isCollectionOrArray()) { 083 Collection c = null; 084 if (type.isArray() || type.isArgs()) 085 c = new ArrayList(); 086 else 087 c = ( 088 type.canCreateNewInstance(getOuter()) 089 ? (Collection<?>)type.newInstance(getOuter()) 090 : new OList(this) 091 ); 092 093 int argIndex = 0; 094 for (Resource resource : roots) 095 c.add(parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), resource, 096 getOuter(), null)); 097 098 if (type.isArray() || type.isArgs()) 099 return (T)toArray(type, c); 100 return (T)c; 101 } 102 103 if (roots.isEmpty()) 104 return type.isOptional() ? (T)Optional.empty() : null; 105 106 if (roots.size() > 1) 107 throw new ParseException(this, "Too many root nodes found in model: {0}", roots.size()); 108 Resource resource = roots.get(0); 109 110 return parseAnything(type, resource, getOuter(), null); 111 } 112 113 private final void addModelPrefix(Namespace ns) { 114 model.setNsPrefix(ns.getName(), ns.getUri()); 115 } 116 117 /* 118 * Decodes the specified string. 119 * If {@link RdfParser#RDF_trimWhitespace} is <jk>true</jk>, the resulting string is trimmed before decoding. 120 * If {@link #isTrimStrings()} is <jk>true</jk>, the resulting string is trimmed after decoding. 121 */ 122 private String decodeString(Object o) { 123 if (o == null) 124 return null; 125 String s = o.toString(); 126 if (s.isEmpty()) 127 return s; 128 if (isTrimWhitespace()) 129 s = s.trim(); 130 s = XmlUtils.decode(s, null); 131 if (isTrimStrings()) 132 s = s.trim(); 133 return s; 134 } 135 136 /* 137 * Finds the roots in the model using either the "root" property to identify it, 138 * or by resorting to scanning the model for all nodes with no incoming predicates. 139 */ 140 private List<Resource> getRoots(Model m) { 141 List<Resource> l = new LinkedList<>(); 142 143 // First try to find the root using the "http://www.apache.org/juneau/root" property. 144 Property root = m.createProperty(getJuneauNs().getUri(), RDF_juneauNs_ROOT); 145 for (ResIterator i = m.listResourcesWithProperty(root); i.hasNext();) 146 l.add(i.next()); 147 148 if (! l.isEmpty()) 149 return l; 150 151 // Otherwise, we need to find all resources that aren't objects. 152 // We want to explicitly ignore statements where the subject 153 // and object are the same node. 154 Set<RDFNode> objects = new HashSet<>(); 155 for (StmtIterator i = m.listStatements(); i.hasNext();) { 156 Statement st = i.next(); 157 RDFNode subject = st.getSubject(); 158 RDFNode object = st.getObject(); 159 if (object.isResource() && ! object.equals(subject)) 160 objects.add(object); 161 } 162 for (ResIterator i = m.listSubjects(); i.hasNext();) { 163 Resource r = i.next(); 164 if (! objects.contains(r)) 165 l.add(r); 166 } 167 return l; 168 } 169 170 private <T> BeanMap<T> parseIntoBeanMap(Resource r2, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 171 BeanMeta<T> bm = m.getMeta(); 172 RdfBeanMeta rbm = getRdfBeanMeta(bm); 173 if (rbm.hasBeanUri() && r2.getURI() != null) 174 rbm.getBeanUriProperty().set(m, null, r2.getURI()); 175 for (StmtIterator i = r2.listProperties(); i.hasNext();) { 176 Statement st = i.next(); 177 Property p = st.getPredicate(); 178 String key = decodeString(p.getLocalName()); 179 BeanPropertyMeta pMeta = m.getPropertyMeta(key); 180 setCurrentProperty(pMeta); 181 if (pMeta != null) { 182 RDFNode o = st.getObject(); 183 ClassMeta<?> cm = pMeta.getClassMeta(); 184 if (cm.isCollectionOrArray() && isMultiValuedCollections(pMeta)) { 185 ClassMeta<?> et = cm.getElementType(); 186 Object value = parseAnything(et, o, m.getBean(false), pMeta); 187 setName(et, value, key); 188 try { 189 pMeta.add(m, key, value); 190 } catch (BeanRuntimeException e) { 191 onBeanSetterException(pMeta, e); 192 throw e; 193 } 194 } else { 195 Object value = parseAnything(cm, o, m.getBean(false), pMeta); 196 setName(cm, value, key); 197 try { 198 pMeta.set(m, key, value); 199 } catch (BeanRuntimeException e) { 200 onBeanSetterException(pMeta, e); 201 throw e; 202 } 203 } 204 } else if (! (p.equals(pRoot) || p.equals(pType))) { 205 RDFNode o = st.getObject(); 206 Object value = parseAnything(object(), o, m.getBean(false), null); 207 onUnknownProperty(key, m, value); 208 } 209 setCurrentProperty(null); 210 } 211 return m; 212 } 213 214 private boolean isMultiValuedCollections(BeanPropertyMeta pMeta) { 215 RdfBeanPropertyMeta bpRdf = (pMeta == null ? RdfBeanPropertyMeta.DEFAULT : getRdfBeanPropertyMeta(pMeta)); 216 217 if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT) 218 return bpRdf.getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED; 219 220 return getCollectionFormat() == RdfCollectionFormat.MULTI_VALUED; 221 } 222 223 private <T> T parseAnything(ClassMeta<?> eType, RDFNode n, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 224 225 if (eType == null) 226 eType = object(); 227 PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getSwap(this); 228 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 229 ClassMeta<?> sType = null; 230 if (builder != null) 231 sType = builder.getBuilderClassMeta(this); 232 else if (swap != null) 233 sType = swap.getSwapClassMeta(this); 234 else 235 sType = eType; 236 237 if (sType.isOptional()) 238 return (T)Optional.ofNullable(parseAnything(eType.getElementType(), n, outer, pMeta)); 239 240 setCurrentClass(sType); 241 242 if (! sType.canCreateNewInstance(outer)) { 243 if (n.isResource()) { 244 Statement st = n.asResource().getProperty(pType); 245 if (st != null) { 246 String c = st.getLiteral().getString(); 247 ClassMeta tcm = getClassMeta(c, pMeta, eType); 248 if (tcm != null) 249 sType = eType = tcm; 250 } 251 } 252 } 253 254 Object o = null; 255 if (n.isResource() && n.asResource().getURI() != null && n.asResource().getURI().equals(RDF_NIL)) { 256 // Do nothing. Leave o == null. 257 } else if (sType.isObject()) { 258 if (n.isLiteral()) { 259 o = n.asLiteral().getValue(); 260 if (o instanceof String) { 261 o = decodeString(o); 262 } 263 } 264 else if (n.isResource()) { 265 Resource r = n.asResource(); 266 if (! urisVisited.add(r)) 267 o = r.getURI(); 268 else if (r.getProperty(pValue) != null) { 269 o = parseAnything(object(), n.asResource().getProperty(pValue).getObject(), outer, null); 270 } else if (isSeq(r)) { 271 o = new OList(this); 272 parseIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta); 273 } else if (isBag(r)) { 274 o = new OList(this); 275 parseIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta); 276 } else if (r.canAs(RDFList.class)) { 277 o = new OList(this); 278 parseIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta); 279 } else { 280 // If it has a URI and no child properties, we interpret this as an 281 // external resource, and convert it to just a URL. 282 String uri = r.getURI(); 283 if (uri != null && ! r.listProperties().hasNext()) { 284 o = r.getURI(); 285 } else { 286 OMap m2 = new OMap(this); 287 parseIntoMap(r, m2, null, null, pMeta); 288 o = cast(m2, pMeta, eType); 289 } 290 } 291 } else { 292 throw new ParseException(this, "Unrecognized node type ''{0}'' for object", n); 293 } 294 } else if (sType.isBoolean()) { 295 o = convertToType(getValue(n, outer), boolean.class); 296 } else if (sType.isCharSequence()) { 297 o = decodeString(getValue(n, outer)); 298 } else if (sType.isChar()) { 299 o = parseCharacter(decodeString(getValue(n, outer))); 300 } else if (sType.isNumber()) { 301 o = parseNumber(getValue(n, outer).toString(), (Class<? extends Number>)sType.getInnerClass()); 302 } else if (sType.isMap()) { 303 Resource r = n.asResource(); 304 if (! urisVisited.add(r)) 305 return null; 306 Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new OMap(this)); 307 o = parseIntoMap(r, m, eType.getKeyType(), eType.getValueType(), pMeta); 308 } else if (sType.isCollectionOrArray() || sType.isArgs()) { 309 if (sType.isArray() || sType.isArgs()) 310 o = new ArrayList(); 311 else 312 o = (sType.canCreateNewInstance(outer) ? (Collection<?>)sType.newInstance(outer) : new OList(this)); 313 Resource r = n.asResource(); 314 if (! urisVisited.add(r)) 315 return null; 316 if (isSeq(r)) { 317 parseIntoCollection(r.as(Seq.class), (Collection)o, sType, pMeta); 318 } else if (isBag(r)) { 319 parseIntoCollection(r.as(Bag.class), (Collection)o, sType, pMeta); 320 } else if (r.canAs(RDFList.class)) { 321 parseIntoCollection(r.as(RDFList.class), (Collection)o, sType, pMeta); 322 } else { 323 throw new ParseException(this, "Unrecognized node type ''{0}'' for collection", n); 324 } 325 if (sType.isArray() || sType.isArgs()) 326 o = toArray(sType, (Collection)o); 327 } else if (builder != null) { 328 Resource r = n.asResource(); 329 if (! urisVisited.add(r)) 330 return null; 331 BeanMap<?> bm = toBeanMap(builder.create(this, eType)); 332 o = builder.build(this, parseIntoBeanMap(r, bm).getBean(), eType); 333 } else if (sType.canCreateNewBean(outer)) { 334 Resource r = n.asResource(); 335 if (! urisVisited.add(r)) 336 return null; 337 BeanMap<?> bm = newBeanMap(outer, sType.getInnerClass()); 338 o = parseIntoBeanMap(r, bm).getBean(); 339 } else if (sType.isUri() && n.isResource()) { 340 o = sType.newInstanceFromString(outer, decodeString(n.asResource().getURI())); 341 } else if (sType.canCreateNewInstanceFromString(outer)) { 342 o = sType.newInstanceFromString(outer, decodeString(getValue(n, outer))); 343 } else if (n.isResource()) { 344 Resource r = n.asResource(); 345 Map m = new OMap(this); 346 parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 347 if (m.containsKey(getBeanTypePropertyName(eType))) 348 o = cast((OMap)m, pMeta, eType); 349 else 350 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); 351 } else { 352 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); 353 } 354 355 if (swap != null && o != null) 356 o = unswap(swap, o, eType); 357 358 if (outer != null) 359 setParent(eType, o, outer); 360 361 return (T)o; 362 } 363 364 private boolean isSeq(RDFNode n) { 365 if (n.isResource()) { 366 Statement st = n.asResource().getProperty(pRdfType); 367 if (st != null) 368 return RDF_SEQ.equals(st.getResource().getURI()); 369 } 370 return false; 371 } 372 373 private boolean isBag(RDFNode n) { 374 if (n.isResource()) { 375 Statement st = n.asResource().getProperty(pRdfType); 376 if (st != null) 377 return RDF_BAG.equals(st.getResource().getURI()); 378 } 379 return false; 380 } 381 382 private Object getValue(RDFNode n, Object outer) throws IOException, ParseException, ExecutableException { 383 if (n.isLiteral()) 384 return n.asLiteral().getValue(); 385 if (n.isResource()) { 386 Statement st = n.asResource().getProperty(pValue); 387 if (st != null) { 388 n = st.getObject(); 389 if (n.isLiteral()) 390 return n.asLiteral().getValue(); 391 return parseAnything(object(), st.getObject(), outer, null); 392 } 393 } 394 throw new ParseException(this, "Unknown value type for node ''{0}''", n); 395 } 396 397 private <K,V> Map<K,V> parseIntoMap(Resource r, Map<K,V> m, ClassMeta<K> keyType, 398 ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 399 // Add URI as "uri" to generic maps. 400 if (r.getURI() != null) { 401 K uri = convertAttrToType(m, "uri", keyType); 402 V value = convertAttrToType(m, r.getURI(), valueType); 403 m.put(uri, value); 404 } 405 for (StmtIterator i = r.listProperties(); i.hasNext();) { 406 Statement st = i.next(); 407 Property p = st.getPredicate(); 408 String key = p.getLocalName(); 409 if (! (key.equals("root") && p.getURI().equals(getJuneauNs().getUri()))) { 410 key = decodeString(key); 411 RDFNode o = st.getObject(); 412 K key2 = convertAttrToType(m, key, keyType); 413 V value = parseAnything(valueType, o, m, pMeta); 414 setName(valueType, value, key); 415 m.put(key2, value); 416 } 417 418 } 419 return m; 420 } 421 422 private <E> Collection<E> parseIntoCollection(Container c, Collection<E> l, 423 ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 424 int argIndex = 0; 425 for (NodeIterator ni = c.iterator(); ni.hasNext();) { 426 E e = (E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); 427 l.add(e); 428 } 429 return l; 430 } 431 432 private <E> Collection<E> parseIntoCollection(RDFList list, Collection<E> l, 433 ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 434 int argIndex = 0; 435 for (ExtendedIterator<RDFNode> ni = list.iterator(); ni.hasNext();) { 436 E e = (E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), ni.next(), l, pMeta); 437 l.add(e); 438 } 439 return l; 440 } 441 442 //----------------------------------------------------------------------------------------------------------------- 443 // Common properties 444 //----------------------------------------------------------------------------------------------------------------- 445 446 /** 447 * Configuration property: RDF format for representing collections and arrays. 448 * 449 * @see RdfParser#RDF_collectionFormat 450 * @return 451 * RDF format for representing collections and arrays. 452 */ 453 protected final RdfCollectionFormat getCollectionFormat() { 454 return ctx.getCollectionFormat(); 455 } 456 457 /** 458 * Configuration property: Default XML namespace for bean properties. 459 * 460 * @see RdfParser#RDF_juneauBpNs 461 * @return 462 * Default XML namespace for bean properties. 463 */ 464 protected final Namespace getJuneauBpNs() { 465 return ctx.getJuneauBpNs(); 466 } 467 468 /** 469 * Configuration property: XML namespace for Juneau properties. 470 * 471 * @see RdfParser#RDF_juneauNs 472 * @return 473 * XML namespace for Juneau properties. 474 */ 475 protected final Namespace getJuneauNs() { 476 return ctx.getJuneauNs(); 477 } 478 479 /** 480 * Configuration property: RDF language. 481 * 482 * @see RdfParser#RDF_language 483 * @return 484 * The RDF language to use. 485 */ 486 protected final String getLanguage() { 487 return ctx.getLanguage(); 488 } 489 490 /** 491 * Configuration property: Collections should be serialized and parsed as loose collections. 492 * 493 * @see RdfParser#RDF_looseCollections 494 * @return 495 * <jk>true</jk> if collections of resources are handled as loose collections of resources in RDF instead of 496 * resources that are children of an RDF collection (e.g. Sequence, Bag). 497 */ 498 protected final boolean isLooseCollections() { 499 return ctx.isLooseCollections(); 500 } 501 502 //----------------------------------------------------------------------------------------------------------------- 503 // Jena properties 504 //----------------------------------------------------------------------------------------------------------------- 505 506 /** 507 * Configuration property: All Jena-related configuration properties. 508 * 509 * @return 510 * A map of all Jena-related configuration properties. 511 */ 512 protected final Map<String,Object> getJenaProperties() { 513 return ctx.getJenaProperties(); 514 } 515 516 //----------------------------------------------------------------------------------------------------------------- 517 // Properties 518 //----------------------------------------------------------------------------------------------------------------- 519 520 /** 521 * Configuration property: Trim whitespace from text elements. 522 * 523 * @see RdfParser#RDF_trimWhitespace 524 * @return 525 * <jk>true</jk> if whitespace in text elements will be automatically trimmed. 526 */ 527 protected final boolean isTrimWhitespace() { 528 return ctx.isTrimWhitespace(); 529 } 530 531 //----------------------------------------------------------------------------------------------------------------- 532 // Extended metadata 533 //----------------------------------------------------------------------------------------------------------------- 534 535 /** 536 * Returns the language-specific metadata on the specified class. 537 * 538 * @param cm The class to return the metadata on. 539 * @return The metadata. 540 */ 541 protected RdfClassMeta getRdfClassMeta(ClassMeta<?> cm) { 542 return ctx.getRdfClassMeta(cm); 543 } 544 545 /** 546 * Returns the language-specific metadata on the specified bean. 547 * 548 * @param bm The bean to return the metadata on. 549 * @return The metadata. 550 */ 551 protected RdfBeanMeta getRdfBeanMeta(BeanMeta<?> bm) { 552 return ctx.getRdfBeanMeta(bm); 553 } 554 555 /** 556 * Returns the language-specific metadata on the specified bean property. 557 * 558 * @param bpm The bean property to return the metadata on. 559 * @return The metadata. 560 */ 561 protected RdfBeanPropertyMeta getRdfBeanPropertyMeta(BeanPropertyMeta bpm) { 562 return ctx.getRdfBeanPropertyMeta(bpm); 563 } 564 565 /** 566 * Returns the language-specific metadata on the specified bean property. 567 * 568 * @param bpm The bean property to return the metadata on. 569 * @return The metadata. 570 */ 571 protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) { 572 return ctx.getXmlBeanPropertyMeta(bpm); 573 } 574 575 //----------------------------------------------------------------------------------------------------------------- 576 // Other methods 577 //----------------------------------------------------------------------------------------------------------------- 578 579 @Override /* Session */ 580 public OMap toMap() { 581 return super.toMap() 582 .a("RdfParserSession", new DefaultFilteringOMap() 583 ); 584 } 585}