001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.jena;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.IoUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023import static org.apache.juneau.marshall.jena.Constants.*;
024
025import java.io.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.jena.datatypes.xsd.*;
030import org.apache.jena.rdf.model.*;
031import org.apache.jena.riot.*;
032import org.apache.juneau.commons.bean.*;
033import org.apache.juneau.commons.http.*;
034import org.apache.juneau.commons.svl.*;
035import org.apache.juneau.commons.utils.*;
036import org.apache.juneau.marshall.*;
037import org.apache.juneau.marshall.httppart.*;
038import org.apache.juneau.marshall.serializer.*;
039import org.apache.juneau.marshall.xml.*;
040
041/**
042 * Session for stream-based RDF serialization (binary formats like RDF/THRIFT, RDF/PROTO).
043 *
044 * <h5 class='section'>Notes:</h5><ul>
045 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
046 * </ul>
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 *    <li class='link'>{doc jmr.RdfDetails}
050 * </ul>
051 */
052@SuppressWarnings({
053   "java:S110", // Inheritance depth acceptable for serializer session hierarchy
054   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
055   "java:S3776", // Cognitive complexity acceptable for RDF stream serialization
056   "java:S6541", // Brain method acceptable for RDF stream serialization dispatch logic
057   "rawtypes", // Raw Map/Collection/BeanMap casts in writeAnything(), writeMap() and the writeTo*() helpers, where element types are only known at runtime.
058   "resource", // The Jena Model created in the constructor is an in-memory session-lifetime field that is written out to the stream and never needs closing.
059   "unchecked" // Raw Map/Collection/ClassMeta casts in writeAnything() and the writeTo*() helpers are guarded by the sType.isMap()/isCollection() checks.
060})
061public class RdfStreamSerializerSession extends OutputStreamSerializerSession {
062
063   /**
064    * Builder class.
065    */
066   public static class Builder extends OutputStreamSerializerSession.Builder<Builder> {
067
068      private final RdfStreamSerializer ctx;
069
070      protected Builder(RdfStreamSerializer ctx) {
071         super(reqnn("ctx", ctx));
072         this.ctx = ctx;
073      }
074
075      @Override
076      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
077         super.apply(type, apply);
078         return this;
079      }
080
081      @Override
082      public RdfStreamSerializerSession build() {
083         return new RdfStreamSerializerSession(this);
084      }
085
086      @Override
087      public Builder debug(Boolean value) {
088         super.debug(value);
089         return this;
090      }
091
092      @Override
093      public Builder javaMethod(java.lang.reflect.Method value) {
094         super.javaMethod(value);
095         return this;
096      }
097
098      @Override
099      public Builder locale(Locale value) {
100         super.locale(value);
101         return this;
102      }
103
104      @Override
105      public Builder mediaType(MediaType value) {
106         super.mediaType(value);
107         return this;
108      }
109
110      @Override
111      public Builder mediaTypeDefault(MediaType value) {
112         super.mediaTypeDefault(value);
113         return this;
114      }
115
116      @Override
117      public Builder properties(Map<String,Object> value) {
118         super.properties(value);
119         return this;
120      }
121
122      @Override
123      public Builder property(String key, Object value) {
124         super.property(key, value);
125         return this;
126      }
127
128      @Override
129      public Builder resolver(VarResolverSession value) {
130         super.resolver(value);
131         return this;
132      }
133
134      @Override
135      public Builder schema(HttpPartSchema value) {
136         super.schema(value);
137         return this;
138      }
139
140      @Override
141      public Builder schemaDefault(HttpPartSchema value) {
142         super.schemaDefault(value);
143         return this;
144      }
145
146      @Override
147      public Builder timeZone(TimeZone value) {
148         super.timeZone(value);
149         return this;
150      }
151
152      @Override
153      public Builder timeZoneDefault(TimeZone value) {
154         super.timeZoneDefault(value);
155         return this;
156      }
157
158      @Override
159      public Builder unmodifiable() {
160         super.unmodifiable();
161         return this;
162      }
163
164      @Override
165      public Builder uriContext(UriContext value) {
166         super.uriContext(value);
167         return this;
168      }
169   }
170
171   /**
172    * Creates a new builder for this session.
173    *
174    * @param ctx The serializer creating this session.
175    *    <br>Cannot be <jk>null</jk>.
176    * @return A new builder.
177    */
178   public static Builder create(RdfStreamSerializer ctx) {
179      return new Builder(reqnn("ctx", ctx));
180   }
181
182   private final Model model;
183   private final Property pRoot;
184   private final Property pValue;
185   private final Lang lang;
186   private final RdfStreamSerializer ctx;
187   private final Namespace[] namespaces;
188
189   protected RdfStreamSerializerSession(Builder builder) {
190      super(builder);
191      ctx = builder.ctx;
192
193      model = ModelFactory.createDefaultModel();
194      namespaces = ctx.getNamespaces();
195      addModelPrefix(ctx.getJuneauNs());
196      addModelPrefix(ctx.getJuneauBpNs());
197      if (namespaces != null) // HTT: RdfStreamSerializer.Builder only exposes language(); the delegated RdfSerializer's namespaces() is never set, so getNamespaces() always returns a non-null (empty) array here.
198         for (var ns : namespaces) // HTT: unreachable — namespaces is always empty for stream serializers; see guard above.
199            addModelPrefix(ns); // HTT: unreachable — see guard above.
200      pRoot = model.createProperty(ctx.getJuneauNs().getUri(), "root");
201      pValue = model.createProperty(ctx.getJuneauNs().getUri(), "value");
202
203      var langName = ctx.getLanguage();
204      lang = toLang(langName);
205      if (lang == null)
206         throw isex("Unknown RDF language: %s", langName);
207   }
208
209   @Override /* OutputStreamSerializerSession */
210   public boolean hasNativeBytes() {
211      // The RDF graph layer has no native byte-array literal type. A byte array is surfaced either as a
212      // base64Binary typed literal, or as a plain string literal carrying the text wire form of the
213      // configured binary format (for any other binary format, after the binary swap fires).
214      return false;
215   }
216
217   private static Lang toLang(String langName) {
218      var l = org.apache.jena.riot.RDFLanguages.nameToLang(langName);
219      if (l != null)
220         return l;
221      if (eq(langName, "RDF/PROTO")) // HTT - not registered in Jena's RDFLanguages
222         return Lang.RDFPROTO;
223      return null;
224   }
225
226   private void addModelPrefix(Namespace ns) {
227      model.setNsPrefix(ns.getName(), ns.getUri());
228   }
229
230   /**
231    * Returns the language-specific metadata on the specified bean property.
232    *
233    * @param bpm The bean property to return the metadata on.
234    * @return The metadata.
235    */
236   protected RdfBeanPropertyMeta getRdfBeanPropertyMeta(BeanPropertyMeta bpm) {
237      return bpm == null ? RdfBeanPropertyMeta.DEFAULT : ctx.getRdfBeanPropertyMeta(bpm);
238   }
239
240   /**
241    * Returns the language-specific metadata on the specified bean property.
242    *
243    * @param bpm The bean property to return the metadata on.
244    * @return The metadata.
245    */
246   protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
247      return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm);
248   }
249
250   private String encodeElementName(Object o) {
251      return XmlUtils.encodeElementName(toString(o));
252   }
253
254   String encodeTextInvalidChars(Object o) {
255      if (o == null)
256         return null;
257      return XmlUtils.escapeText(toString(o));
258   }
259
260   private String getUri(Object uri) {
261      if (! nn(uri))
262         return null;
263      return getUriResolver().resolve(uri.toString());
264   }
265
266   @SuppressWarnings({
267      "null" // aType intentionally null before assignment in control flow
268   })
269   private RDFNode writeAnything(Object o, boolean isURI, ClassMeta<?> eType, String attrName, BeanPropertyMeta bpm, Resource parentResource) throws SerializeException {
270      var m = model;
271      ClassMeta<?> aType = push2(attrName, o, eType);
272      ClassMeta<?> sType;
273
274      if (eType == null)
275         eType = object();
276      if (aType == null) {
277         o = null;
278         aType = object();
279      }
280      if (isOptional(aType)) {
281         o = getOptionalValue(o);
282         eType = getOptionalType(eType);
283         aType = getClassMetaForObject(o, object());
284      }
285      if (nn(o)) {
286         if (aType.isDelegate())
287            aType = (ClassMeta)((Delegate)o).getBeanInfo();
288         sType = aType;
289         var swap = aType.getSwap(this);
290         if (nn(swap)) {
291            o = swap(swap, o);
292            sType = swap.getSwapClassMeta(this);
293            if (sType.isObject())
294               sType = getClassMetaForObject(o);
295         }
296      } else {
297         sType = eType.getSerializedClassMeta(this);
298      }
299
300      var typeName = getBeanTypeName(this, eType, aType, bpm);
301      RDFNode n = null;
302
303      if (o == null || sType.isChar() && ((Character)o).charValue() == 0) {
304         if (nn(bpm)) {
305            if (isKeepNullProperties()) // HTT: bpm!=null only reaches this branch via writeBeanMap's line ~418 call site, which is itself only reached after canIgnoreValue(bpMeta,...) let a null/NUL value through — and canIgnoreValue only lets a null value through when isKeepNullProperties() is already true. So whenever this line executes, isKeepNullProperties() is structurally guaranteed true; the false side (leaving n unset) is dead in this call path.
306               n = m.createResource(RDF_NIL);
307         } else {
308            n = m.createResource(RDF_NIL);
309         }
310      } else if (sType.isUri() || isURI) {
311         // RDF URI gate must come before isBean/isMap/isCharSequence: values annotated with Uri need Resource emission, not literal text.
312         var uri = getUri(o);
313         if (isAbsoluteUri(uri))
314            n = m.createResource(uri);
315         else
316            n = m.createLiteral(encodeTextInvalidChars(uri));
317      } else if (sType.isBean()) {
318         var bm = toBeanMap(o);
319         Object uri = null;
320         var rbm = ctx.getRdfBeanMeta(bm.getMeta());
321         if (rbm.hasBeanUri())
322            uri = rbm.getBeanUriProperty().get(bm, null);
323         var uri2 = getUri(uri);
324         n = m.createResource(uri2);
325         writeBeanMap(bm, (Resource)n, typeName);
326      } else if (sType.isMap()) {
327         n = m.createResource();
328         writeMap((Map)o, (Resource)n, sType);
329      } else if (sType.isByteArray()) {
330         // byte[] gate must come before isCollectionOrArray: a byte array also satisfies the array check but RDF emits it as a typed base64 literal, not as a Seq of bytes.
331         var b64 = Base64.getEncoder().encodeToString((byte[]) o);
332         n = m.createTypedLiteral(b64, XSDDatatype.XSDbase64Binary);
333      } else if (sType.isCollectionOrArray()) {
334         var c = sort(sType.isCollection() ? (Collection)o : toList(sType.inner(), o));
335         var f = ctx.getCollectionFormat();
336         var cRdf = ctx.getRdfClassMeta(sType);
337         var bpRdf = getRdfBeanPropertyMeta(bpm);
338         if (cRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
339            f = cRdf.getCollectionFormat();
340         if (bpRdf.getCollectionFormat() != RdfCollectionFormat.DEFAULT)
341            f = bpRdf.getCollectionFormat();
342         if (f == RdfCollectionFormat.MULTI_VALUED) {
343            writeToMultiProperties(c, eType, bpm, attrName, parentResource);
344         } else {
345            n = switch (f) {
346               case BAG -> writeToContainer(c, eType, m.createBag());
347               case LIST -> writeToList(c, eType);
348               default -> writeToContainer(c, eType, m.createSeq());
349            };
350         }
351      } else if (sType.isCharSequence() || sType.isChar()) {
352         n = m.createLiteral(encodeTextInvalidChars(o));
353      } else if (sType.isNumber() || sType.isBoolean()) {
354         if (!ctx.isAddLiteralTypes()) // HTT: RdfStreamSerializer.Builder only exposes language(); addLiteralTypes is always false for stream serializers.
355            n = m.createLiteral(o.toString());
356         else
357            n = m.createTypedLiteral(o); // HTT: unreachable — see guard above.
358      } else if (sType.isReader()) {
359         n = m.createLiteral(encodeTextInvalidChars(read((Reader)o, SerializerSession::handleThrown))); // HTT: the catch(IOException)->accept(e) path IS genuinely exercised (RdfSerializer_Test#o11_stream_serialize_reader_ioException_wrapped; a throwing Reader triggers this and yields a SerializeException wrapping the IOException — proven by a temporary System.err probe inside the lambda body during triage, which printed on every run) — but JaCoCo persistently reports the compiler-synthesized `lambda$writeAnything$0` method (the SerializerSession::handleThrown method-reference target) as 0% covered regardless, a known class of JaCoCo/javac-lambda-desugaring interaction where the synthetic method is invoked only via a functional-interface instance constructed in a different class (IoUtils.read) and its probe is never recorded. Waived as a tooling artifact, not a real gap. // NOSONAR java:S125 - HTT waiver marker, not code
360      } else if (sType.isInputStream()) {
361         n = m.createLiteral(encodeTextInvalidChars(read((InputStream)o, SerializerSession::handleThrown))); // HTT: same tooling artifact as the Reader branch above — RdfSerializer_Test#o12_stream_serialize_inputstream_ioException_wrapped exercises this (confirmed via the same temporary System.err probe technique), but JaCoCo reports the synthesized `lambda$writeAnything$1` method as 0% covered regardless. Waived as a tooling artifact, not a real gap.
362      } else {
363         n = m.createLiteral(encodeTextInvalidChars(toString(o)));
364      }
365
366      pop();
367      return n;
368   }
369
370   private void writeBeanMap(BeanMap<?> m, Resource r, String typeName) throws SerializeException {
371      var l = new ArrayList<BeanPropertyValue>();
372      if (nn(typeName)) {
373         var pm = m.getMeta().getTypeProperty();
374         l.add(new BeanPropertyValue(pm, pm.getName(), typeName, null));
375      }
376      var checkNull = (Predicate<Object>)(x -> isKeepNullProperties() || nn(x));
377      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> l.add(new BeanPropertyValue(pMeta, key, value, thrown)));
378      Collections.reverse(l);
379      l.forEach(x -> {
380         var bpMeta = x.getMeta();
381         var cMeta = (ClassMeta<?>) bpMeta.getBeanInfo();
382         var bpRdf = getRdfBeanPropertyMeta(bpMeta);
383         var bpXml = getXmlBeanPropertyMeta(bpMeta);
384         if (bpRdf.isBeanUri())
385            return;
386         var key = x.getName();
387         var value = x.getValue();
388         var t = x.getThrown();
389         if (nn(t))
390            onBeanGetterException(bpMeta, t); // HTT: genuinely exercised — RdfSerializer_Test#o13_stream_beanGetterException_surfaces_onBeanGetterException triggers a real getter exception, confirmed reaching this exact call (with a non-null BeanRuntimeException `t`) via a temporary System.err probe during triage — onBeanGetterException then rethrows as SerializeException since isIgnoreInvocationExceptionsOnGetters() is false. Same JaCoCo tooling artifact as lines 376/378: a conditional basic block whose only content is a call that always throws (no local catch, no fall-through instruction) is never marked covered by this JaCoCo version even when demonstrably executed. Waived as a tooling artifact, not a real gap.
391         if (canIgnoreValue(bpMeta, key, value))
392            return;
393         var ns = bpRdf.getNamespace();
394         if (ns == null && ctx.isUseXmlNamespaces()) // HTT: RdfStreamSerializer.Builder only exposes language(); the delegated RdfSerializer's useXmlNamespaces defaults to true and is never overridden, so this is always true when reached.
395            ns = bpXml.getNamespace();
396         if (ns == null)
397            ns = ctx.getJuneauBpNs();
398         else if (ctx.isAutoDetectNamespaces()) // HTT: RdfStreamSerializer.Builder only exposes language(); the delegated RdfSerializer's autoDetectNamespaces defaults to true and is never overridden, so this is always true when reached.
399            addModelPrefix(ns);
400         var p = model.createProperty(ns.getUri(), encodeElementName(key));
401         var n = writeAnything(value, bpMeta.isUri(), cMeta, key, bpMeta, r);
402         if (nn(n))
403            r.addProperty(p, n);
404      });
405   }
406
407   private void writeMap(Map m, Resource r, ClassMeta<?> type) throws SerializeException {
408      m = sort(m);
409      var keyType = type.getKeyType();
410      var valueType = type.getValueType();
411      var l = CollectionUtils.toList(m.entrySet());
412      Collections.reverse(l);
413      l.forEach(x -> {
414         var entry = (Map.Entry<Object,Object>)x;
415         Object value = entry.getValue();
416         Object key = generalize(entry.getKey(), keyType);
417         var ns = ctx.getJuneauBpNs();
418         var p = model.createProperty(ns.getUri(), encodeElementName(toString(key)));
419         var n = writeAnything(value, false, valueType, toString(key), null, r);
420         if (nn(n)) // HTT: writeMap always passes bpm=null to writeAnything, and writeAnything's null-value branch unconditionally assigns n=RDF_NIL when bpm is null (regardless of keepNullProperties) — so n is never null here; the false side is only reachable when bpm != null (see writeBeanMap's equivalent check at line ~419). Flagged as suspected structurally-dead code in this call path, not fixed.
421            r.addProperty(p, n);
422      });
423   }
424
425   private Container writeToContainer(Collection c, ClassMeta<?> type, Container list) throws SerializeException {
426      var elementType = type.getElementType();
427      c.forEach(x -> list.add(writeAnything(x, false, elementType, null, null, null)));
428      return list;
429   }
430
431   private RDFList writeToList(Collection c, ClassMeta<?> type) throws SerializeException {
432      var elementType = type.getElementType();
433      List<RDFNode> l = listOfSize(c.size());
434      c.forEach(x -> l.add(writeAnything(x, false, elementType, null, null, null)));
435      return model.createList(l.iterator());
436   }
437
438   private void writeToMultiProperties(Collection c, ClassMeta<?> sType, BeanPropertyMeta bpm, String attrName, Resource parentResource) throws SerializeException {
439      var elementType = sType.getElementType();
440      var bpRdf = getRdfBeanPropertyMeta(bpm);
441      var bpXml = getXmlBeanPropertyMeta(bpm);
442      c.forEach(x -> {
443         var ns = bpRdf.getNamespace();
444         if (ns == null && ctx.isUseXmlNamespaces()) // HTT: RdfStreamSerializer.Builder only exposes language(); the delegated RdfSerializer's useXmlNamespaces defaults to true and is never overridden, so this is always true when reached.
445            ns = bpXml.getNamespace();
446         if (ns == null)
447            ns = ctx.getJuneauBpNs();
448         else if (ctx.isAutoDetectNamespaces()) // HTT: RdfStreamSerializer.Builder only exposes language(); the delegated RdfSerializer's autoDetectNamespaces defaults to true and is never overridden, so this is always true when reached.
449            addModelPrefix(ns);
450         var n2 = writeAnything(x, false, elementType, null, null, null);
451         var p = model.createProperty(ns.getUri(), encodeElementName(attrName));
452         parentResource.addProperty(p, n2);
453      });
454   }
455
456   @Override
457   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
458      var cm = getClassMetaForObject(o);
459      if (ctx.isLooseCollections() && nn(cm) && cm.isCollectionOrArray()) { // HTT: RdfStreamSerializer.Builder only exposes language(); looseCollections is always false for stream serializers.
460         Collection c = cm.isCollection() ? (Collection)o : toList(cm.inner(), o); // HTT: unreachable — see guard above.
461         forEachEntry(c, x -> writeAnything(x, false, object(), "root", null, null)); // HTT: unreachable — see guard above.
462      } else { // HTT: unreachable false-jump target of the guard above (looseCollections is always false for stream serializers, so every write() takes this else branch, but jacoco still marks the residual jump).
463         var n = writeAnything(o, false, getExpectedRootType(o), "root", null, null);
464         Resource r;
465         if (n.isLiteral()) {
466            r = model.createResource();
467            r.addProperty(pValue, n);
468         } else {
469            r = n.asResource();
470         }
471         if (ctx.isAddRootProp()) // HTT: RdfStreamSerializer.Builder only exposes language(); addRootProp is always false for stream serializers.
472            r.addProperty(pRoot, "true"); // HTT: unreachable — see guard above.
473      }
474      RDFDataMgr.write(out.getOutputStream(), model, lang);
475   }
476
477}