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