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.RdfCommon.*;
017import static org.apache.juneau.jena.RdfSerializer.*;
018
019import java.util.*;
020
021import org.apache.juneau.*;
022import org.apache.juneau.internal.*;
023import org.apache.juneau.serializer.*;
024import org.apache.juneau.transform.*;
025import org.apache.juneau.xml.*;
026
027import com.hp.hpl.jena.rdf.model.*;
028
029/**
030 * Session object that lives for the duration of a single use of {@link RdfSerializer}.
031 * 
032 * <p>
033 * This class is NOT thread safe.  
034 * It is typically discarded after one-time use although it can be reused within the same thread.
035 */
036@SuppressWarnings({ "rawtypes", "unchecked" })
037public final class RdfSerializerSession extends WriterSerializerSession {
038
039   private final String rdfLanguage;
040   private final Namespace juneauNs, juneauBpNs;
041   private final boolean
042      addLiteralTypes,
043      addRootProperty,
044      useXmlNamespaces,
045      looseCollections,
046      autoDetectNamespaces,
047      addBeanTypeProperties;
048   private final Property pRoot, pValue;
049   private final Model model;
050   private final RDFWriter writer;
051   private final RdfCollectionFormat collectionFormat;
052   private final Namespace[] namespaces;
053
054   /**
055    * Create a new session using properties specified in the context.
056    * 
057    * @param ctx
058    *    The context creating this session object.
059    *    The context contains all the configuration settings for this object.
060    * @param args
061    *    Runtime arguments.
062    *    These specify session-level information such as locale and URI context.
063    *    It also include session-level properties that override the properties defined on the bean and
064    *    serializer contexts.
065    */
066   protected RdfSerializerSession(RdfSerializer ctx, SerializerSessionArgs args) {
067      super(ctx, args);
068            
069      rdfLanguage = getProperty(RDF_language, String.class, ctx.rdfLanguage);
070      juneauNs = getInstanceProperty(RDF_juneauNs, Namespace.class, ctx.juneauNs);
071      juneauBpNs = getInstanceProperty(RDF_juneauBpNs, Namespace.class, ctx.juneauBpNs);
072      addLiteralTypes = getProperty(RDF_addLiteralTypes, boolean.class, ctx.addLiteralTypes);
073      addRootProperty = getProperty(RDF_addRootProperty, boolean.class, ctx.addRootProperty);
074      collectionFormat = getProperty(RDF_collectionFormat, RdfCollectionFormat.class, ctx.collectionFormat);
075      looseCollections = getProperty(RDF_looseCollections, boolean.class, ctx.looseCollections);
076      useXmlNamespaces = getProperty(RDF_useXmlNamespaces, boolean.class, ctx.useXmlNamespaces);
077      autoDetectNamespaces = getProperty(RDF_autoDetectNamespaces, boolean.class, ctx.autoDetectNamespaces);
078      namespaces = getInstanceArrayProperty(RDF_namespaces, Namespace.class, ctx.namespaces);
079      addBeanTypeProperties = getProperty(RDF_addBeanTypeProperties, boolean.class, ctx.addBeanTypeProperties);
080      model = ModelFactory.createDefaultModel();
081      addModelPrefix(juneauNs);
082      addModelPrefix(juneauBpNs);
083      for (Namespace ns : this.namespaces)
084         addModelPrefix(ns);
085      pRoot = model.createProperty(juneauNs.getUri(), RDF_juneauNs_ROOT);
086      pValue = model.createProperty(juneauNs.getUri(), RDF_juneauNs_VALUE);
087      writer = model.getWriter(rdfLanguage);
088
089      // Only apply properties with this prefix!
090      String propPrefix = RdfCommon.LANG_PROP_MAP.get(rdfLanguage);
091      if (propPrefix == null)
092         throw new FormattedRuntimeException("Unknown RDF language encountered: ''{0}''", rdfLanguage);
093
094      // RDF/XML specific properties.
095      if (propPrefix.equals("rdfXml.")) {
096         writer.setProperty("tab", isUseWhitespace() ? 2 : 0);
097         writer.setProperty("attributeQuoteChar", Character.toString(getQuoteChar()));
098      }
099      
100      for (Map.Entry<String,Object> e : ctx.jenaSettings.entrySet())
101         if (e.getKey().startsWith(propPrefix, 5))
102            writer.setProperty(e.getKey().substring(5 + propPrefix.length()), e.getValue());
103      
104      for (String k : getPropertyKeys()) 
105         if (k.startsWith("RdfCommon.jena.") && k.startsWith(propPrefix, 15))
106            writer.setProperty(k.substring(15 + propPrefix.length()), getProperty(k));
107   }
108
109   @Override /* Session */
110   public ObjectMap asMap() {
111      return super.asMap()
112         .append("RdfSerializerSession", new ObjectMap()
113            .append("addBeanTypeProperties", addBeanTypeProperties)
114            .append("addLiteralTypes", addLiteralTypes)
115            .append("addRootProperty", addRootProperty)
116            .append("autoDetectNamespaces", autoDetectNamespaces)
117            .append("collectionFormat", collectionFormat)
118            .append("juneauNs", juneauNs)
119            .append("juneauBpNs", juneauBpNs)
120            .append("looseCollections", looseCollections)
121            .append("namespaces", namespaces)
122            .append("rdfLanguage", rdfLanguage)
123            .append("useXmlNamespaces", useXmlNamespaces)
124         );
125   }
126
127   /*
128    * Adds the specified namespace as a model prefix.
129    */
130   private void addModelPrefix(Namespace ns) {
131      model.setNsPrefix(ns.getName(), ns.getUri());
132   }
133
134   /**
135    * Returns the {@link Serializer#SERIALIZER_addBeanTypeProperties} setting value for this session.
136    * 
137    * @return The {@link Serializer#SERIALIZER_addBeanTypeProperties} setting value for this session.
138    */
139   @Override /* SerializerSession */
140   public final boolean isAddBeanTypeProperties() {
141      return addBeanTypeProperties;
142   }
143
144   /*
145    * XML-encodes the specified string using the {@link XmlUtils#escapeText(Object)} method.
146    */
147   private String encodeTextInvalidChars(Object o) {
148      if (o == null)
149         return null;
150      String s = toString(o);
151      return XmlUtils.escapeText(s);
152   }
153
154   /*
155    * XML-encoded the specified element name using the {@link XmlUtils#encodeElementName(Object)} method.
156    */
157   private String encodeElementName(Object o) {
158      return XmlUtils.encodeElementName(toString(o));
159   }
160   
161   @Override /* Serializer */
162   protected void doSerialize(SerializerPipe out, Object o) throws Exception {
163
164      Resource r = null;
165
166      ClassMeta<?> cm = getClassMetaForObject(o);
167      if (looseCollections && cm != null && cm.isCollectionOrArray()) {
168         Collection c = sort(cm.isCollection() ? (Collection)o : toList(cm.getInnerClass(), o));
169         for (Object o2 : c)
170            serializeAnything(o2, false, object(), "root", null, null);
171      } else {
172         RDFNode n = serializeAnything(o, false, getExpectedRootType(o), "root", null, null);
173         if (n.isLiteral()) {
174            r = model.createResource();
175            r.addProperty(pValue, n);
176         } else {
177            r = n.asResource();
178         }
179
180         if (addRootProperty)
181            r.addProperty(pRoot, "true");
182      }
183
184      writer.write(model, out.getWriter(), "http://unknown/");
185   }
186
187   private RDFNode serializeAnything(Object o, boolean isURI, ClassMeta<?> eType, 
188         String attrName, BeanPropertyMeta bpm, Resource parentResource) throws Exception {
189      Model m = model;
190
191      ClassMeta<?> aType = null;       // The actual type
192      ClassMeta<?> wType = null;       // The wrapped type
193      ClassMeta<?> sType = object();   // The serialized type
194
195      aType = push(attrName, o, eType);
196
197      if (eType == null)
198         eType = object();
199
200      // Handle recursion
201      if (aType == null) {
202         o = null;
203         aType = object();
204      }
205
206      if (o != null) {
207
208         if (aType.isDelegate()) {
209            wType = aType;
210            aType = ((Delegate)o).getClassMeta();
211         }
212
213         sType = aType;
214
215         // Swap if necessary
216         PojoSwap swap = aType.getPojoSwap(this);
217         if (swap != null) {
218            o = swap.swap(this, o);
219            sType = swap.getSwapClassMeta(this);
220
221            // If the getSwapClass() method returns Object, we need to figure out
222            // the actual type now.
223            if (sType.isObject())
224               sType = getClassMetaForObject(o);
225         }
226      } else {
227         sType = eType.getSerializedClassMeta(this);
228      }
229
230      String typeName = getBeanTypeName(eType, aType, bpm);
231
232      RDFNode n = null;
233
234      if (o == null || sType.isChar() && ((Character)o).charValue() == 0) {
235         if (bpm != null) {
236            if (! isTrimNulls()) {
237               n = m.createResource(RDF_NIL);
238            }
239         } else {
240            n = m.createResource(RDF_NIL);
241         }
242
243      } else if (sType.isUri() || isURI) {
244         // Note that RDF URIs must be absolute to be valid!
245         String uri = getUri(o, null);
246         if (StringUtils.isAbsoluteUri(uri))
247            n = m.createResource(uri);
248         else
249            n = m.createLiteral(encodeTextInvalidChars(uri));
250
251      } else if (sType.isCharSequence() || sType.isChar()) {
252         n = m.createLiteral(encodeTextInvalidChars(o));
253
254      } else if (sType.isNumber() || sType.isBoolean()) {
255         if (! addLiteralTypes)
256            n = m.createLiteral(o.toString());
257         else
258            n = m.createTypedLiteral(o);
259
260      } else if (sType.isMap() || (wType != null && wType.isMap())) {
261         if (o instanceof BeanMap) {
262            BeanMap bm = (BeanMap)o;
263            Object uri = null;
264            RdfBeanMeta rbm = (RdfBeanMeta)bm.getMeta().getExtendedMeta(RdfBeanMeta.class);
265            if (rbm.hasBeanUri())
266               uri = rbm.getBeanUriProperty().get(bm, null);
267            String uri2 = getUri(uri, null);
268            n = m.createResource(uri2);
269            serializeBeanMap(bm, (Resource)n, typeName);
270         } else {
271            Map m2 = (Map)o;
272            n = m.createResource();
273            serializeMap(m2, (Resource)n, sType);
274         }
275
276      } else if (sType.isBean()) {
277         BeanMap bm = toBeanMap(o);
278         Object uri = null;
279         RdfBeanMeta rbm = (RdfBeanMeta)bm.getMeta().getExtendedMeta(RdfBeanMeta.class);
280         if (rbm.hasBeanUri())
281            uri = rbm.getBeanUriProperty().get(bm, null);
282         String uri2 = getUri(uri, null);
283         n = m.createResource(uri2);
284         serializeBeanMap(bm, (Resource)n, typeName);
285
286      } else if (sType.isCollectionOrArray() || (wType != null && wType.isCollection())) {
287         Collection c = sort(sType.isCollection() ? (Collection)o : toList(sType.getInnerClass(), o));
288         RdfCollectionFormat f = collectionFormat;
289         RdfClassMeta rcm = sType.getExtendedMeta(RdfClassMeta.class);
290         if (rcm.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
291            f = rcm.getCollectionFormat();
292         if (bpm != null && bpm.getExtendedMeta(RdfBeanPropertyMeta.class).getCollectionFormat() != RdfCollectionFormat.DEFAULT)
293            f = bpm.getExtendedMeta(RdfBeanPropertyMeta.class).getCollectionFormat();
294         switch (f) {
295            case BAG: n = serializeToContainer(c, eType, m.createBag()); break;
296            case LIST: n = serializeToList(c, eType); break;
297            case MULTI_VALUED: serializeToMultiProperties(c, eType, bpm, attrName, parentResource); break;
298            default: n = serializeToContainer(c, eType, m.createSeq());
299         }
300      
301      } else if (sType.isReader() || sType.isInputStream()) {
302         n = m.createLiteral(encodeTextInvalidChars(IOUtils.read(o)));
303      
304      } else {
305         n = m.createLiteral(encodeTextInvalidChars(toString(o)));
306      }
307
308      pop();
309
310      return n;
311   }
312
313   private String getUri(Object uri, Object uri2) {
314      String s = null;
315      if (uri != null)
316         s = uri.toString();
317      if ((s == null || s.isEmpty()) && uri2 != null)
318         s = uri2.toString();
319      if (s == null)
320         return null;
321      return getUriResolver().resolve(s);
322   }
323
324   private void serializeMap(Map m, Resource r, ClassMeta<?> type) throws Exception {
325
326      m = sort(m);
327
328      ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType();
329
330      ArrayList<Map.Entry<Object,Object>> l = new ArrayList<>(m.entrySet());
331      Collections.reverse(l);
332      for (Map.Entry<Object,Object> me : l) {
333         Object value = me.getValue();
334
335         Object key = generalize(me.getKey(), keyType);
336
337         Namespace ns = juneauBpNs;
338         Property p = model.createProperty(ns.getUri(), encodeElementName(toString(key)));
339         RDFNode n = serializeAnything(value, false, valueType, toString(key), null, r);
340         if (n != null)
341            r.addProperty(p, n);
342      }
343   }
344
345   private void serializeBeanMap(BeanMap<?> m, Resource r, String typeName) throws Exception {
346      List<BeanPropertyValue> l = m.getValues(isTrimNulls(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null);
347      Collections.reverse(l);
348      for (BeanPropertyValue bpv : l) {
349         BeanPropertyMeta pMeta = bpv.getMeta();
350         ClassMeta<?> cMeta = pMeta.getClassMeta();
351
352         if (pMeta.getExtendedMeta(RdfBeanPropertyMeta.class).isBeanUri())
353            continue;
354
355         String key = bpv.getName();
356         Object value = bpv.getValue();
357         Throwable t = bpv.getThrown();
358         if (t != null)
359            onBeanGetterException(pMeta, t);
360
361         if (canIgnoreValue(cMeta, key, value))
362            continue;
363
364         BeanPropertyMeta bpm = bpv.getMeta();
365         Namespace ns = bpm.getExtendedMeta(RdfBeanPropertyMeta.class).getNamespace();
366         if (ns == null && useXmlNamespaces)
367            ns = bpm.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace();
368         if (ns == null)
369            ns = juneauBpNs;
370         else if (autoDetectNamespaces)
371            addModelPrefix(ns);
372
373         Property p = model.createProperty(ns.getUri(), encodeElementName(key));
374         RDFNode n = serializeAnything(value, pMeta.isUri(), cMeta, key, pMeta, r);
375         if (n != null)
376            r.addProperty(p, n);
377      }
378   }
379
380
381   private Container serializeToContainer(Collection c, ClassMeta<?> type, Container list) throws Exception {
382
383      ClassMeta<?> elementType = type.getElementType();
384      for (Object e : c) {
385         RDFNode n = serializeAnything(e, false, elementType, null, null, null);
386         list = list.add(n);
387      }
388      return list;
389   }
390
391   private RDFList serializeToList(Collection c, ClassMeta<?> type) throws Exception {
392      ClassMeta<?> elementType = type.getElementType();
393      List<RDFNode> l = new ArrayList<>(c.size());
394      for (Object e : c) {
395         l.add(serializeAnything(e, false, elementType, null, null, null));
396      }
397      return model.createList(l.iterator());
398   }
399
400   private void serializeToMultiProperties(Collection c, ClassMeta<?> sType, 
401         BeanPropertyMeta bpm, String attrName, Resource parentResource) throws Exception {
402      ClassMeta<?> elementType = sType.getElementType();
403      for (Object e : c) {
404         Namespace ns = null;
405         if (bpm != null) {
406            ns = bpm.getExtendedMeta(RdfBeanPropertyMeta.class).getNamespace();
407            if (ns == null && useXmlNamespaces)
408               ns = bpm.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace();
409         }
410         if (ns == null)
411            ns = juneauBpNs;
412         else if (autoDetectNamespaces)
413            addModelPrefix(ns);
414         RDFNode n2 = serializeAnything(e, false, elementType, null, null, null);
415         Property p = model.createProperty(ns.getUri(), encodeElementName(attrName));
416         parentResource.addProperty(p, n2);
417      }
418   }
419}