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.jena.Constants.*;
016import static org.apache.juneau.jena.RdfSerializer.*;
017
018import java.io.IOException;
019import java.util.*;
020
021import org.apache.jena.rdf.model.*;
022import org.apache.juneau.*;
023import org.apache.juneau.internal.*;
024import org.apache.juneau.jena.annotation.*;
025import org.apache.juneau.serializer.*;
026import org.apache.juneau.transform.*;
027import org.apache.juneau.xml.*;
028import org.apache.juneau.xml.annotation.*;
029
030/**
031 * Session object that lives for the duration of a single use of {@link RdfSerializer}.
032 *
033 * <p>
034 * This class is NOT thread safe.
035 * It is typically discarded after one-time use although it can be reused within the same thread.
036 */
037@SuppressWarnings({ "rawtypes", "unchecked" })
038public final class RdfSerializerSession extends WriterSerializerSession {
039
040   private final RdfSerializer ctx;
041   private final Property pRoot, pValue;
042   private final Model model;
043   private final RDFWriter writer;
044   private final Namespace[] namespaces;
045
046   /**
047    * Create a new session using properties specified in the context.
048    *
049    * @param ctx
050    *    The context creating this session object.
051    *    The context contains all the configuration settings for this object.
052    * @param args
053    *    Runtime arguments.
054    *    These specify session-level information such as locale and URI context.
055    *    It also include session-level properties that override the properties defined on the bean and
056    *    serializer contexts.
057    */
058   protected RdfSerializerSession(RdfSerializer ctx, SerializerSessionArgs args) {
059      super(ctx, args);
060      this.ctx = ctx;
061
062      namespaces = getInstanceArrayProperty(RDF_namespaces, Namespace.class, ctx.namespaces);
063      model = ModelFactory.createDefaultModel();
064      addModelPrefix(ctx.getJuneauNs());
065      addModelPrefix(ctx.getJuneauBpNs());
066      for (Namespace ns : this.namespaces)
067         addModelPrefix(ns);
068      pRoot = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_ROOT);
069      pValue = model.createProperty(ctx.getJuneauNs().getUri(), RDF_juneauNs_VALUE);
070      writer = model.getWriter(ctx.getLanguage());
071
072      // Only apply properties with this prefix!
073      String propPrefix = RdfCommon.LANG_PROP_MAP.get(ctx.getLanguage());
074      if (propPrefix == null)
075         throw new FormattedRuntimeException("Unknown RDF language encountered: ''{0}''", ctx.getLanguage());
076
077      // RDF/XML specific properties.
078      if (propPrefix.equals("rdfXml.")) {
079         writer.setProperty("tab", isUseWhitespace() ? 2 : 0);
080         writer.setProperty("attributeQuoteChar", Character.toString(getQuoteChar()));
081      }
082
083      for (Map.Entry<String,Object> e : ctx.jenaProperties.entrySet())
084         if (e.getKey().startsWith(propPrefix, 5))
085            writer.setProperty(e.getKey().substring(5 + propPrefix.length()), e.getValue());
086
087      for (String k : getPropertyKeys())
088         if (k.startsWith("RdfCommon.jena.") && k.startsWith(propPrefix, 15))
089            writer.setProperty(k.substring(15 + propPrefix.length()), getProperty(k));
090   }
091
092   /*
093    * Adds the specified namespace as a model prefix.
094    */
095   private void addModelPrefix(Namespace ns) {
096      model.setNsPrefix(ns.getName(), ns.getUri());
097   }
098
099   /*
100    * XML-encodes the specified string using the {@link XmlUtils#escapeText(Object)} method.
101    */
102   private String encodeTextInvalidChars(Object o) {
103      if (o == null)
104         return null;
105      String s = toString(o);
106      return XmlUtils.escapeText(s);
107   }
108
109   /*
110    * XML-encoded the specified element name using the {@link XmlUtils#encodeElementName(Object)} method.
111    */
112   private String encodeElementName(Object o) {
113      return XmlUtils.encodeElementName(toString(o));
114   }
115
116   @Override /* Serializer */
117   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
118
119      Resource r = null;
120
121      ClassMeta<?> cm = getClassMetaForObject(o);
122      if (isLooseCollections() && cm != null && cm.isCollectionOrArray()) {
123         Collection c = sort(cm.isCollection() ? (Collection)o : toList(cm.getInnerClass(), o));
124         for (Object o2 : c)
125            serializeAnything(o2, false, object(), "root", null, null);
126      } else {
127         RDFNode n = serializeAnything(o, false, getExpectedRootType(o), "root", null, null);
128         if (n.isLiteral()) {
129            r = model.createResource();
130            r.addProperty(pValue, n);
131         } else {
132            r = n.asResource();
133         }
134
135         if (isAddRootProp())
136            r.addProperty(pRoot, "true");
137      }
138
139      writer.write(model, out.getWriter(), "http://unknown/");
140   }
141
142   private RDFNode serializeAnything(Object o, boolean isURI, ClassMeta<?> eType,
143         String attrName, BeanPropertyMeta bpm, Resource parentResource) throws IOException, SerializeException {
144      Model m = model;
145
146      ClassMeta<?> aType = null;       // The actual type
147      ClassMeta<?> wType = null;       // The wrapped type
148      ClassMeta<?> sType = object();   // The serialized type
149
150      aType = push2(attrName, o, eType);
151
152      if (eType == null)
153         eType = object();
154
155      // Handle recursion
156      if (aType == null) {
157         o = null;
158         aType = object();
159      }
160
161      if (o != null) {
162
163         if (aType.isDelegate()) {
164            wType = aType;
165            aType = ((Delegate)o).getClassMeta();
166         }
167
168         sType = aType;
169
170         // Swap if necessary
171         PojoSwap swap = aType.getPojoSwap(this);
172         if (swap != null) {
173            o = swap(swap, o);
174            sType = swap.getSwapClassMeta(this);
175
176            // If the getSwapClass() method returns Object, we need to figure out
177            // the actual type now.
178            if (sType.isObject())
179               sType = getClassMetaForObject(o);
180         }
181      } else {
182         sType = eType.getSerializedClassMeta(this);
183      }
184
185      String typeName = getBeanTypeName(eType, aType, bpm);
186
187      RDFNode n = null;
188
189      if (o == null || sType.isChar() && ((Character)o).charValue() == 0) {
190         if (bpm != null) {
191            if (! isTrimNullProperties()) {
192               n = m.createResource(RDF_NIL);
193            }
194         } else {
195            n = m.createResource(RDF_NIL);
196         }
197
198      } else if (sType.isUri() || isURI) {
199         // Note that RDF URIs must be absolute to be valid!
200         String uri = getUri(o, null);
201         if (StringUtils.isAbsoluteUri(uri))
202            n = m.createResource(uri);
203         else
204            n = m.createLiteral(encodeTextInvalidChars(uri));
205
206      } else if (sType.isCharSequence() || sType.isChar()) {
207         n = m.createLiteral(encodeTextInvalidChars(o));
208
209      } else if (sType.isNumber() || sType.isBoolean()) {
210         if (! isAddLiteralTypes())
211            n = m.createLiteral(o.toString());
212         else
213            n = m.createTypedLiteral(o);
214
215      } else if (sType.isMap() || (wType != null && wType.isMap())) {
216         if (o instanceof BeanMap) {
217            BeanMap bm = (BeanMap)o;
218            Object uri = null;
219            RdfBeanMeta rbm = bRdf(bm.getMeta());
220            if (rbm.hasBeanUri())
221               uri = rbm.getBeanUriProperty().get(bm, null);
222            String uri2 = getUri(uri, null);
223            n = m.createResource(uri2);
224            serializeBeanMap(bm, (Resource)n, typeName);
225         } else {
226            Map m2 = (Map)o;
227            n = m.createResource();
228            serializeMap(m2, (Resource)n, sType);
229         }
230
231      } else if (sType.isBean()) {
232         BeanMap bm = toBeanMap(o);
233         Object uri = null;
234         RdfBeanMeta rbm = bRdf(bm.getMeta());
235         if (rbm.hasBeanUri())
236            uri = rbm.getBeanUriProperty().get(bm, null);
237         String uri2 = getUri(uri, null);
238         n = m.createResource(uri2);
239         serializeBeanMap(bm, (Resource)n, typeName);
240
241      } else if (sType.isCollectionOrArray() || (wType != null && wType.isCollection())) {
242
243         Collection c = sort(sType.isCollection() ? (Collection)o : toList(sType.getInnerClass(), o));
244         RdfCollectionFormat f = getCollectionFormat();
245         RdfClassMeta cRdf = cRdf(sType);
246         RdfBeanPropertyMeta bpRdf = bpRdf(bpm);
247
248         if (cRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
249            f = cRdf.getCollectionFormat();
250         if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
251            f = bpRdf.getCollectionFormat();
252
253         switch (f) {
254            case BAG: n = serializeToContainer(c, eType, m.createBag()); break;
255            case LIST: n = serializeToList(c, eType); break;
256            case MULTI_VALUED: serializeToMultiProperties(c, eType, bpm, attrName, parentResource); break;
257            default: n = serializeToContainer(c, eType, m.createSeq());
258         }
259
260      } else if (sType.isReader() || sType.isInputStream()) {
261         n = m.createLiteral(encodeTextInvalidChars(IOUtils.read(o)));
262
263      } else {
264         n = m.createLiteral(encodeTextInvalidChars(toString(o)));
265      }
266
267      pop();
268
269      return n;
270   }
271
272   private String getUri(Object uri, Object uri2) {
273      String s = null;
274      if (uri != null)
275         s = uri.toString();
276      if ((s == null || s.isEmpty()) && uri2 != null)
277         s = uri2.toString();
278      if (s == null)
279         return null;
280      return getUriResolver().resolve(s);
281   }
282
283   private void serializeMap(Map m, Resource r, ClassMeta<?> type) throws IOException, SerializeException {
284
285      m = sort(m);
286
287      ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType();
288
289      ArrayList<Map.Entry<Object,Object>> l = new ArrayList<>(m.entrySet());
290      Collections.reverse(l);
291      for (Map.Entry<Object,Object> me : l) {
292         Object value = me.getValue();
293
294         Object key = generalize(me.getKey(), keyType);
295
296         Namespace ns = getJuneauBpNs();
297         Property p = model.createProperty(ns.getUri(), encodeElementName(toString(key)));
298         RDFNode n = serializeAnything(value, false, valueType, toString(key), null, r);
299         if (n != null)
300            r.addProperty(p, n);
301      }
302   }
303
304   private void serializeBeanMap(BeanMap<?> m, Resource r, String typeName) throws IOException, SerializeException {
305      List<BeanPropertyValue> l = m.getValues(isTrimNullProperties(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null);
306      Collections.reverse(l);
307      for (BeanPropertyValue bpv : l) {
308
309         BeanPropertyMeta bpMeta = bpv.getMeta();
310         ClassMeta<?> cMeta = bpMeta.getClassMeta();
311         RdfBeanPropertyMeta bpRdf = bpRdf(bpMeta);
312         XmlBeanPropertyMeta bpXml = bpXml(bpMeta);
313
314         if (bpRdf.isBeanUri())
315            continue;
316
317         String key = bpv.getName();
318         Object value = bpv.getValue();
319         Throwable t = bpv.getThrown();
320         if (t != null)
321            onBeanGetterException(bpMeta, t);
322
323         if (canIgnoreValue(cMeta, key, value))
324            continue;
325
326         Namespace ns = bpRdf.getNamespace();
327         if (ns == null && isUseXmlNamespaces())
328            ns = bpXml.getNamespace();
329         if (ns == null)
330            ns = getJuneauBpNs();
331         else if (isAutoDetectNamespaces())
332            addModelPrefix(ns);
333
334         Property p = model.createProperty(ns.getUri(), encodeElementName(key));
335         RDFNode n = serializeAnything(value, bpMeta.isUri(), cMeta, key, bpMeta, r);
336         if (n != null)
337            r.addProperty(p, n);
338      }
339   }
340
341
342   private Container serializeToContainer(Collection c, ClassMeta<?> type, Container list) throws IOException, SerializeException {
343
344      ClassMeta<?> elementType = type.getElementType();
345      for (Object e : c) {
346         RDFNode n = serializeAnything(e, false, elementType, null, null, null);
347         list = list.add(n);
348      }
349      return list;
350   }
351
352   private RDFList serializeToList(Collection c, ClassMeta<?> type) throws IOException, SerializeException {
353      ClassMeta<?> elementType = type.getElementType();
354      List<RDFNode> l = new ArrayList<>(c.size());
355      for (Object e : c) {
356         l.add(serializeAnything(e, false, elementType, null, null, null));
357      }
358      return model.createList(l.iterator());
359   }
360
361   private void serializeToMultiProperties(Collection c, ClassMeta<?> sType,
362         BeanPropertyMeta bpm, String attrName, Resource parentResource) throws IOException, SerializeException {
363
364      ClassMeta<?> elementType = sType.getElementType();
365      RdfBeanPropertyMeta bpRdf = bpRdf(bpm);
366      XmlBeanPropertyMeta bpXml = bpXml(bpm);
367
368      for (Object e : c) {
369         Namespace ns = bpRdf.getNamespace();
370         if (ns == null && isUseXmlNamespaces())
371            ns = bpXml.getNamespace();
372         if (ns == null)
373            ns = getJuneauBpNs();
374         else if (isAutoDetectNamespaces())
375            addModelPrefix(ns);
376         RDFNode n2 = serializeAnything(e, false, elementType, null, null, null);
377         Property p = model.createProperty(ns.getUri(), encodeElementName(attrName));
378         parentResource.addProperty(p, n2);
379      }
380   }
381
382   private static RdfClassMeta cRdf(ClassMeta<?> cm) {
383      return cm.getExtendedMeta(RdfClassMeta.class);
384   }
385
386   private static XmlBeanPropertyMeta bpXml(BeanPropertyMeta pMeta) {
387      return pMeta == null ? XmlBeanPropertyMeta.DEFAULT : pMeta.getExtendedMeta(XmlBeanPropertyMeta.class);
388   }
389
390   private static RdfBeanPropertyMeta bpRdf(BeanPropertyMeta pMeta) {
391      return pMeta == null ? RdfBeanPropertyMeta.DEFAULT : pMeta.getExtendedMeta(RdfBeanPropertyMeta.class);
392   }
393
394   private static RdfBeanMeta bRdf(BeanMeta bm) {
395      return (RdfBeanMeta)bm.getExtendedMeta(RdfBeanMeta.class);
396   }
397
398   //-----------------------------------------------------------------------------------------------------------------
399   // Common properties
400   //-----------------------------------------------------------------------------------------------------------------
401
402   /**
403    * Configuration property:  RDF format for representing collections and arrays.
404    *
405    * @see RdfSerializer#RDF_collectionFormat
406    * @return
407    *    RDF format for representing collections and arrays.
408    */
409   protected final RdfCollectionFormat getCollectionFormat() {
410      return ctx.getCollectionFormat();
411   }
412
413   /**
414    * Configuration property:  Default XML namespace for bean properties.
415    *
416    * @see RdfSerializer#RDF_juneauBpNs
417    * @return
418    *    The XML namespace to use for bean properties.
419    */
420   protected final Namespace getJuneauBpNs() {
421      return ctx.getJuneauBpNs();
422   }
423
424   /**
425    * Configuration property:  XML namespace for Juneau properties.
426    *
427    * @see RdfSerializer#RDF_juneauNs
428    * @return
429    *    The XML namespace to use for Juneau properties.
430    */
431   protected final Namespace getJuneauNs() {
432      return ctx.getJuneauNs();
433   }
434
435   /**
436    * Configuration property:  RDF language.
437    *
438    * @see RdfSerializer#RDF_language
439    * @return
440    *    The RDF language to use.
441    */
442   protected final String getLanguage() {
443      return ctx.getLanguage();
444   }
445
446   /**
447    * Configuration property:  Collections should be serialized and parsed as loose collections.
448    *
449    * @see RdfSerializer#RDF_looseCollections
450    * @return
451    *    <jk>true</jk> if collections of resources are handled as loose collections of resources in RDF instead of
452    *    resources that are children of an RDF collection (e.g. Sequence, Bag).
453    */
454   protected final boolean isLooseCollections() {
455      return ctx.isLooseCollections();
456   }
457
458   //-----------------------------------------------------------------------------------------------------------------
459   // Jena properties
460   //-----------------------------------------------------------------------------------------------------------------
461
462   /**
463    * Configuration property:  All Jena-related configuration properties.
464    *
465    * @return
466    *    A map of all Jena-related configuration properties.
467    */
468   protected final Map<String,Object> getJenaProperties() {
469      return ctx.getJenaProperties();
470   }
471
472   //-----------------------------------------------------------------------------------------------------------------
473   // Properties
474   //-----------------------------------------------------------------------------------------------------------------
475
476   /**
477    * Configuration property:  Add <js>"_type"</js> properties when needed.
478    *
479    * @see RdfSerializer#RDF_addBeanTypes
480    * @return
481    *    <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
482    *    through reflection.
483    */
484   @Override
485   protected final boolean isAddBeanTypes() {
486      return ctx.isAddBeanTypes();
487   }
488
489   /**
490    * Configuration property:  Add XSI data types to non-<c>String</c> literals.
491    *
492    * @see RdfSerializer#RDF_addLiteralTypes
493    * @return
494    *    <jk>true</jk> if XSI data types should be added to string literals.
495    */
496   protected final boolean isAddLiteralTypes() {
497      return ctx.isAddLiteralTypes();
498   }
499
500   /**
501    * Configuration property:  Add RDF root identifier property to root node.
502    *
503    * @see RdfSerializer#RDF_addRootProperty
504    * @return
505    *    <jk>true</jk> if RDF property <c>http://www.apache.org/juneau/root</c> is added with a value of <js>"true"</js>
506    *    to identify the root node in the graph.
507    */
508   protected final boolean isAddRootProp() {
509      return ctx.isAddRootProp();
510   }
511
512   /**
513    * Configuration property:  Auto-detect namespace usage.
514    *
515    * @see RdfSerializer#RDF_autoDetectNamespaces
516    * @return
517    *    <jk>true</jk> if namespaces usage should be detected before serialization.
518    */
519   protected final boolean isAutoDetectNamespaces() {
520      return ctx.isAutoDetectNamespaces();
521   }
522
523   /**
524    * Configuration property:  Default namespaces.
525    *
526    * @see RdfSerializer#RDF_namespaces
527    * @return
528    *    The default list of namespaces associated with this serializer.
529    */
530   protected final Namespace[] getNamespaces() {
531      return ctx.getNamespaces();
532   }
533
534   /**
535    * Configuration property:  Reuse XML namespaces when RDF namespaces not specified.
536    *
537    * @see RdfSerializer#RDF_useXmlNamespaces
538    * @return
539    *    <jk>true</jk> if namespaces defined using {@link XmlNs @XmlNs} and {@link org.apache.juneau.xml.annotation.Xml @Xml} will be inherited by the RDF serializers.
540    *    <br>Otherwise, namespaces will be defined using {@link RdfNs @RdfNs} and {@link Rdf @Rdf}.
541    */
542   protected final boolean isUseXmlNamespaces() {
543      return ctx.isUseXmlNamespaces();
544   }
545
546   //-----------------------------------------------------------------------------------------------------------------
547   // Other methods
548   //-----------------------------------------------------------------------------------------------------------------
549
550   @Override /* Session */
551   public ObjectMap toMap() {
552      return super.toMap()
553         .append("RdfSerializerSession", new DefaultFilteringObjectMap()
554         );
555   }
556}