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