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}