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 = bm.getExtendedMeta(RdfBeanMeta.class);
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 : pMeta.getExtendedMeta(RdfBeanPropertyMeta.class));
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   // Other methods
520   //-----------------------------------------------------------------------------------------------------------------
521
522   @Override /* Session */
523   public ObjectMap toMap() {
524      return super.toMap()
525         .append("RdfParserSession", new DefaultFilteringObjectMap()
526         );
527   }
528}