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}