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