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.xml; 018 019import static org.apache.juneau.commons.utils.AssertionUtils.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.IoUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*; 024import static org.apache.juneau.xml.XmlSerializerSession.JsonType.*; 025import static org.apache.juneau.xml.annotation.XmlFormat.*; 026 027import java.io.*; 028import java.lang.reflect.*; 029import java.nio.charset.*; 030import java.util.*; 031import java.util.function.*; 032 033import org.apache.juneau.*; 034import org.apache.juneau.commons.lang.*; 035import org.apache.juneau.httppart.*; 036import org.apache.juneau.serializer.*; 037import org.apache.juneau.svl.*; 038import org.apache.juneau.xml.annotation.*; 039 040/** 041 * Session object that lives for the duration of a single use of {@link XmlSerializer}. 042 * 043 * <h5 class='section'>Notes:</h5><ul> 044 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 045 * </ul> 046 * 047 * <h5 class='section'>See Also:</h5><ul> 048 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/XmlBasics">XML Basics</a> 049 050 * </ul> 051 */ 052@SuppressWarnings({ "unchecked", "rawtypes", "resource" }) 053public class XmlSerializerSession extends WriterSerializerSession { 054 055 /** 056 * Builder class. 057 */ 058 public static class Builder extends WriterSerializerSession.Builder { 059 060 private XmlSerializer ctx; 061 062 /** 063 * Constructor 064 * 065 * @param ctx The context creating this session. 066 * <br>Cannot be <jk>null</jk>. 067 */ 068 protected Builder(XmlSerializer ctx) { 069 super(assertArgNotNull("ctx", ctx)); 070 this.ctx = ctx; 071 } 072 073 @Override /* Overridden from Builder */ 074 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 075 super.apply(type, apply); 076 return this; 077 } 078 079 @Override 080 public XmlSerializerSession build() { 081 return new XmlSerializerSession(this); 082 } 083 084 @Override /* Overridden from Builder */ 085 public Builder debug(Boolean value) { 086 super.debug(value); 087 return this; 088 } 089 090 @Override /* Overridden from Builder */ 091 public Builder fileCharset(Charset value) { 092 super.fileCharset(value); 093 return this; 094 } 095 096 @Override /* Overridden from Builder */ 097 public Builder javaMethod(Method value) { 098 super.javaMethod(value); 099 return this; 100 } 101 102 @Override /* Overridden from Builder */ 103 public Builder locale(Locale value) { 104 super.locale(value); 105 return this; 106 } 107 108 @Override /* Overridden from Builder */ 109 public Builder mediaType(MediaType value) { 110 super.mediaType(value); 111 return this; 112 } 113 114 @Override /* Overridden from Builder */ 115 public Builder mediaTypeDefault(MediaType value) { 116 super.mediaTypeDefault(value); 117 return this; 118 } 119 120 @Override /* Overridden from Builder */ 121 public Builder properties(Map<String,Object> value) { 122 super.properties(value); 123 return this; 124 } 125 126 @Override /* Overridden from Builder */ 127 public Builder property(String key, Object value) { 128 super.property(key, value); 129 return this; 130 } 131 132 @Override /* Overridden from Builder */ 133 public Builder resolver(VarResolverSession value) { 134 super.resolver(value); 135 return this; 136 } 137 138 @Override /* Overridden from Builder */ 139 public Builder schema(HttpPartSchema value) { 140 super.schema(value); 141 return this; 142 } 143 144 @Override /* Overridden from Builder */ 145 public Builder schemaDefault(HttpPartSchema value) { 146 super.schemaDefault(value); 147 return this; 148 } 149 150 @Override /* Overridden from Builder */ 151 public Builder streamCharset(Charset value) { 152 super.streamCharset(value); 153 return this; 154 } 155 156 @Override /* Overridden from Builder */ 157 public Builder timeZone(TimeZone value) { 158 super.timeZone(value); 159 return this; 160 } 161 162 @Override /* Overridden from Builder */ 163 public Builder timeZoneDefault(TimeZone value) { 164 super.timeZoneDefault(value); 165 return this; 166 } 167 168 @Override /* Overridden from Builder */ 169 public Builder unmodifiable() { 170 super.unmodifiable(); 171 return this; 172 } 173 174 @Override /* Overridden from Builder */ 175 public Builder uriContext(UriContext value) { 176 super.uriContext(value); 177 return this; 178 } 179 180 @Override /* Overridden from Builder */ 181 public Builder useWhitespace(Boolean value) { 182 super.useWhitespace(value); 183 return this; 184 } 185 } 186 187 /** 188 * Identifies what the contents were of a serialized bean. 189 */ 190 @SuppressWarnings("javadoc") 191 public enum ContentResult { 192 CR_VOID, // No content...append "/>" to the start tag. 193 CR_EMPTY, // No content...append "/>" to the start tag if XML, "/></end>" if HTML. 194 CR_MIXED, // Mixed content...don't add whitespace. 195 CR_ELEMENTS // Elements...use normal whitespace rules. 196 } 197 198 enum JsonType { 199 STRING("string"), BOOLEAN("boolean"), NUMBER("number"), ARRAY("array"), OBJECT("object"), NULL("null"); 200 201 private final String value; 202 203 JsonType(String value) { 204 this.value = value; 205 } 206 207 @Override 208 public String toString() { 209 return value; 210 } 211 212 boolean isOneOf(JsonType...types) { 213 for (var type : types) 214 if (type == this) 215 return true; 216 return false; 217 } 218 } 219 220 /** 221 * Creates a new builder for this object. 222 * 223 * @param ctx The context creating this session. 224 * @return A new builder. 225 */ 226 /** 227 * Creates a new builder for this object. 228 * 229 * @param ctx The context creating this session. 230 * <br>Cannot be <jk>null</jk>. 231 * @return A new builder. 232 */ 233 public static Builder create(XmlSerializer ctx) { 234 return new Builder(assertArgNotNull("ctx", ctx)); 235 } 236 237 private final String textNodeDelimiter; 238 private final XmlSerializer ctx; 239 private Namespace defaultNamespace; 240 private List<Namespace> namespaces = new ArrayList<>(); 241 242 /** 243 * Constructor. 244 * 245 * @param builder The builder for this object. 246 */ 247 protected XmlSerializerSession(Builder builder) { 248 super(builder); 249 ctx = builder.ctx; 250 defaultNamespace = findDefaultNamespace(ctx.getDefaultNamespace()); 251 var ctxNamespaces = ctx.getNamespaces(); 252 namespaces = ctxNamespaces == null ? new ArrayList<>() : new ArrayList<>(ctxNamespaces); 253 textNodeDelimiter = ctx.getTextNodeDelimiter(); 254 } 255 256 /** 257 * Returns the language-specific metadata on the specified bean. 258 * 259 * @param bm The bean to return the metadata on. 260 * @return The metadata. 261 */ 262 public XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) { 263 return ctx.getXmlBeanMeta(bm); 264 } 265 266 /** 267 * Returns the language-specific metadata on the specified bean property. 268 * 269 * @param bpm The bean property to return the metadata on. 270 * @return The metadata. 271 */ 272 public XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) { 273 return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm); 274 } 275 276 /** 277 * Returns the language-specific metadata on the specified class. 278 * 279 * @param cm The class to return the metadata on. 280 * @return The metadata. 281 */ 282 public XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) { 283 return ctx.getXmlClassMeta(cm); 284 } 285 286 /** 287 * Converts the specified output target object to an {@link XmlWriter}. 288 * 289 * @param out The output target object. 290 * @return The output target object wrapped in an {@link XmlWriter}. 291 * @throws IOException Thrown by underlying stream. 292 */ 293 public final XmlWriter getXmlWriter(SerializerPipe out) throws IOException { 294 var output = out.getRawOutput(); 295 if (output instanceof XmlWriter output2) 296 return output2; 297 var w = new XmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), getUriResolver(), isEnableNamespaces(), defaultNamespace); 298 out.setWriter(w); 299 return w; 300 } 301 302 /* 303 * Add a namespace to this session. 304 * 305 * @param ns The namespace being added. 306 */ 307 private void addNamespace(Namespace ns) { 308 if (ns == defaultNamespace) 309 return; 310 311 for (var n : namespaces) 312 if (n == ns) 313 return; 314 315 if (nn(defaultNamespace) && (ns.uri.equals(defaultNamespace.uri) || ns.name.equals(defaultNamespace.name))) 316 defaultNamespace = ns; 317 else 318 namespaces = addAll(namespaces, ns); 319 } 320 321 private Namespace findDefaultNamespace(Namespace n) { 322 if (n == null) 323 return null; 324 if (nn(n.name) && nn(n.uri)) 325 return n; 326 if (n.uri == null) { 327 for (var n2 : getNamespaces()) 328 if (n2.name.equals(n.name)) 329 return n2; 330 } 331 if (n.name == null) { 332 for (var n2 : getNamespaces()) 333 if (n2.uri.equals(n.uri)) 334 return n2; 335 } 336 return n; 337 } 338 339 /** 340 * Checks if an object is a text node (String or primitive type). 341 */ 342 private static boolean isTextNode(Object o) { 343 if (o == null) 344 return false; 345 var c = o.getClass(); 346 // Text nodes are strings and primitives (not beans, collections, arrays, or other complex types) 347 return CharSequence.class.isAssignableFrom(c) || Number.class.isAssignableFrom(c) || Boolean.class.isAssignableFrom(c) || c.isPrimitive(); 348 } 349 350 private boolean isXmlText(XmlFormat format, ClassMeta<?> sType) { 351 if (format == XMLTEXT) 352 return true; 353 var xcm = getXmlClassMeta(sType); 354 if (xcm == null) 355 return false; 356 return xcm.getFormat() == XMLTEXT; 357 } 358 359 @SuppressWarnings("null") 360 private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m, Namespace elementNs, boolean isCollapsed, boolean isMixedOrText) throws SerializeException { 361 boolean hasChildren = false; 362 var bm = m.getMeta(); 363 364 var lp = new ArrayList<BeanPropertyValue>(); 365 366 var checkNull = (Predicate<Object>)(x -> isKeepNullProperties() || nn(x)); 367 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 368 lp.add(new BeanPropertyValue(pMeta, key, value, thrown)); 369 }); 370 371 var xbm = getXmlBeanMeta(bm); 372 373 // @formatter:off 374 Set<String> 375 attrs = xbm.getAttrPropertyNames(), 376 elements = xbm.getElementPropertyNames(), 377 collapsedElements = xbm.getCollapsedPropertyNames(); 378 var attrsProperty = xbm.getAttrsPropertyName(); 379 var contentProperty = xbm.getContentPropertyName(); 380 // @formatter:on 381 382 var cf = (XmlFormat)null; 383 384 var content = (Object)null; 385 var contentType = (ClassMeta<?>)null; 386 for (var p : lp) { 387 var n = p.getName(); 388 if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) { 389 var pMeta = p.getMeta(); 390 if (pMeta.canRead()) { 391 var cMeta = p.getClassMeta(); 392 393 var key = p.getName(); 394 var value = p.getValue(); 395 var t = p.getThrown(); 396 if (nn(t)) 397 onBeanGetterException(pMeta, t); 398 399 if (canIgnoreValue(cMeta, key, value)) 400 continue; 401 402 var bpXml = getXmlBeanPropertyMeta(pMeta); 403 var ns = (isEnableNamespaces() && bpXml.getNamespace() != elementNs ? bpXml.getNamespace() : null); 404 405 if (pMeta.isUri()) { 406 out.attrUri(ns, key, value); 407 } else if (n.equals(attrsProperty)) { 408 if (value instanceof BeanMap<?> bm2) { 409 bm2.forEachValue(x -> true, (pMeta2, key2, value2, thrown2) -> { 410 if (nn(thrown2)) 411 onBeanGetterException(pMeta, thrown2); 412 out.attr(ns, key2, value2); 413 }); 414 } else /* Map */ { 415 var m2 = (Map)value; 416 if (nn(m2)) 417 m2.forEach((k, v) -> out.attr(ns, s(k), v)); 418 } 419 } else { 420 out.attr(ns, key, value); 421 } 422 } 423 } 424 } 425 426 boolean hasContent = false, preserveWhitespace = false, isVoidElement = xbm.getContentFormat() == VOID; 427 428 for (var p : lp) { 429 BeanPropertyMeta pMeta = p.getMeta(); 430 if (pMeta.canRead()) { 431 var cMeta = p.getClassMeta(); 432 433 var n = p.getName(); 434 if (n.equals(contentProperty)) { 435 content = p.getValue(); 436 contentType = p.getClassMeta(); 437 hasContent = true; 438 cf = xbm.getContentFormat(); 439 if (cf.isOneOf(MIXED, MIXED_PWS, TEXT, TEXT_PWS, XMLTEXT)) 440 isMixedOrText = true; 441 if (cf.isOneOf(MIXED_PWS, TEXT_PWS)) 442 preserveWhitespace = true; 443 if (contentType.isCollection() && ((Collection)content).isEmpty()) 444 hasContent = false; 445 else if (contentType.isArray() && Array.getLength(content) == 0) 446 hasContent = false; 447 } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*")) { 448 var key = p.getName(); 449 var value = p.getValue(); 450 var t = p.getThrown(); 451 if (nn(t)) 452 onBeanGetterException(pMeta, t); 453 454 if (canIgnoreValue(cMeta, key, value)) 455 continue; 456 457 if (! hasChildren) { 458 hasChildren = true; 459 out.appendIf(! isCollapsed, '>').nlIf(! isMixedOrText, indent); 460 } 461 462 var bpXml = getXmlBeanPropertyMeta(pMeta); 463 serializeAnything(out, value, cMeta, key, null, bpXml.getNamespace(), false, bpXml.getXmlFormat(), isMixedOrText, false, pMeta); 464 } 465 } 466 } 467 if (contentProperty == null && ! hasContent) 468 return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY); 469 470 // Serialize XML content. 471 if (nn(content)) { 472 out.w('>').nlIf(! isMixedOrText, indent); 473 if (contentType == null) {} else if (contentType.isCollection()) { 474 var c = (Collection)content; 475 boolean previousWasTextNode = false; 476 for (var value : c) { 477 boolean currentIsTextNode = isTextNode(value); 478 // Insert delimiter between consecutive text nodes 479 if (previousWasTextNode && currentIsTextNode && nn(textNodeDelimiter) && ! textNodeDelimiter.isEmpty()) { 480 out.append(textNodeDelimiter); 481 } 482 serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 483 previousWasTextNode = currentIsTextNode; 484 } 485 } else if (contentType.isArray()) { 486 var c = toList(Object[].class, content); 487 boolean previousWasTextNode = false; 488 for (var value : c) { 489 boolean currentIsTextNode = isTextNode(value); 490 // Insert delimiter between consecutive text nodes 491 if (previousWasTextNode && currentIsTextNode && nn(textNodeDelimiter) && ! textNodeDelimiter.isEmpty()) { 492 out.append(textNodeDelimiter); 493 } 494 serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 495 previousWasTextNode = currentIsTextNode; 496 } 497 } else { 498 serializeAnything(out, content, contentType, null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 499 } 500 } else { 501 if (isAddJsonTags()) 502 out.attr("nil", "true"); 503 out.w('>').nlIf(! isMixedOrText, indent); 504 } 505 return isMixedOrText ? CR_MIXED : CR_ELEMENTS; 506 } 507 508 private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws SerializeException { 509 510 var eeType = eType.getElementType(); 511 512 var c = (sType.isCollection() ? (Collection)in : toList(sType.inner(), in)); 513 514 var type2 = (String)null; 515 516 var eName = Value.of(type2); 517 var eNs = Value.<Namespace>empty(); 518 519 if (nn(ppMeta)) { 520 XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(ppMeta); 521 eName.set(bpXml.getChildName()); 522 eNs.set(bpXml.getNamespace()); 523 } 524 525 // Track if previous element was a text node for delimiter insertion 526 var previousWasTextNode = Value.of(false); 527 528 forEachEntry(c, x -> { 529 var currentIsTextNode = isTextNode(x); 530 531 // Insert delimiter between consecutive text nodes 532 if (previousWasTextNode.get() && currentIsTextNode && nn(textNodeDelimiter) && ! textNodeDelimiter.isEmpty()) { 533 out.append(textNodeDelimiter); 534 } 535 536 serializeAnything(out, x, eeType, null, eName.get(), eNs.get(), false, XmlFormat.DEFAULT, isMixed, false, null); 537 previousWasTextNode.set(currentIsTextNode); 538 }); 539 540 return out; 541 } 542 543 private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType, ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws SerializeException { 544 545 var keyType = eKeyType == null ? sType.getKeyType() : eKeyType; 546 var valueType = eValueType == null ? sType.getValueType() : eValueType; 547 548 var hasChildren = Flag.create(); 549 forEachEntry(m, e -> { 550 551 var k = e.getKey(); 552 if (k == null) { 553 k = "\u0000"; 554 } else { 555 k = generalize(k, keyType); 556 if (isTrimStrings() && k instanceof String) 557 k = k.toString().trim(); 558 } 559 560 var value = e.getValue(); 561 562 hasChildren.ifNotSet(() -> out.w('>').nlIf(! isMixed, indent)).set(); 563 serializeAnything(out, value, valueType, toString(k), null, null, false, XmlFormat.DEFAULT, isMixed, false, null); 564 }); 565 566 return hasChildren.isSet() ? CR_ELEMENTS : CR_EMPTY; 567 } 568 569 @Override /* Overridden from Serializer */ 570 protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { 571 if (isEnableNamespaces() && isAutoDetectNamespaces()) 572 findNsfMappings(o); 573 serializeAnything(getXmlWriter(out), o, getExpectedRootType(o), null, null, null, isEnableNamespaces() && isAddNamespaceUrisToRoot(), XmlFormat.DEFAULT, false, false, null); 574 } 575 576 /** 577 * Recursively searches for the XML namespaces on the specified POJO and adds them to the serializer context object. 578 * 579 * @param o The POJO to check. 580 * @throws SerializeException Thrown if bean recursion occurred. 581 */ 582 @SuppressWarnings("null") 583 protected final void findNsfMappings(Object o) throws SerializeException { 584 ClassMeta<?> aType = null; // The actual type 585 586 try { 587 aType = push(null, o, null); 588 } catch (BeanRecursionException e) { 589 throw new SerializeException(e); 590 } 591 592 if (nn(aType)) { 593 var ns = getXmlClassMeta(aType).getNamespace(); 594 if (nn(ns)) { 595 if (nn(ns.uri)) 596 addNamespace(ns); 597 else 598 ns = null; 599 } 600 } 601 602 // Handle recursion 603 if (nn(aType) && ! aType.isPrimitive()) { 604 605 var bm = (BeanMap<?>)null; 606 if (aType.isBeanMap()) { 607 bm = (BeanMap<?>)o; 608 } else if (aType.isBean()) { 609 bm = toBeanMap(o); 610 } else if (aType.isDelegate()) { 611 var innerType = ((Delegate<?>)o).getClassMeta(); 612 var ns = Value.of(getXmlClassMeta(innerType).getNamespace()); 613 if (ns.isPresent()) { 614 if (nn(ns.get().uri)) 615 addNamespace(ns.get()); 616 else 617 ns.getAndUnset(); 618 } 619 620 if (innerType.isBean()) { 621 innerType.getBeanMeta().getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(x -> { 622 ns.set(getXmlBeanPropertyMeta(x).getNamespace()); 623 if (ns.isPresent() && nn(ns.get().uri)) 624 addNamespace(ns.get()); 625 }); 626 } else if (innerType.isMap()) { 627 ((Map<?,?>)o).forEach((k, v) -> findNsfMappings(v)); 628 } else if (innerType.isCollection()) { 629 ((Collection<?>)o).forEach(this::findNsfMappings); 630 } 631 632 } else if (aType.isMap()) { 633 ((Map<?,?>)o).forEach((k, v) -> findNsfMappings(v)); 634 } else if (aType.isCollection()) { 635 ((Collection<?>)o).forEach(this::findNsfMappings); 636 } else if (aType.isArray() && ! aType.getElementType().isPrimitive()) { 637 for (var o2 : ((Object[])o)) 638 findNsfMappings(o2); 639 } 640 if (nn(bm)) { 641 var checkNull = (Predicate<Object>)(x -> isKeepNullProperties() || nn(x)); 642 bm.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 643 var ns = getXmlBeanPropertyMeta(pMeta).getNamespace(); 644 if (nn(ns) && nn(ns.uri)) 645 addNamespace(ns); 646 647 try { 648 findNsfMappings(value); 649 } catch (@SuppressWarnings("unused") Throwable x) { 650 // Ignore 651 } 652 }); 653 } 654 } 655 656 pop(); 657 } 658 659 /** 660 * Default namespace. 661 * 662 * @see XmlSerializer.Builder#defaultNamespace(Namespace) 663 * @return 664 * The default namespace URI for this document. 665 */ 666 protected final Namespace getDefaultNamespace() { return defaultNamespace; } 667 668 /** 669 * Default namespaces. 670 * 671 * @see XmlSerializer.Builder#namespaces(Namespace...) 672 * @return 673 * The default list of namespaces associated with this serializer. 674 * <br>Never <jk>null</jk>. 675 * <br>List is modifiable (namespaces can be added during serialization). 676 */ 677 protected final List<Namespace> getNamespaces() { return namespaces == null ? l() : namespaces; } 678 679 /** 680 * Add <js>"_type"</js> properties when needed. 681 * 682 * @see XmlSerializer.Builder#addBeanTypesXml() 683 * @return 684 * <jk>true</jk> if<js>"_type"</js> properties will be added to beans if their type cannot be inferred 685 * through reflection. 686 */ 687 @Override 688 protected boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); } 689 690 /** 691 * Add JSON type tags. 692 * 693 * @see XmlSerializer.Builder#disableJsonTags() 694 * @return 695 * <jk>true</jk> if plain strings will be wrapped in <js><string></js> tags when serialized as root elements. 696 */ 697 protected final boolean isAddJsonTags() { return ctx.addJsonTags; } 698 699 /** 700 * Add namespace URLs to the root element. 701 * 702 * @see XmlSerializer.Builder#addNamespaceUrisToRoot() 703 * @return 704 * <jk>true</jk> if {@code xmlns:x} attributes are added to the root element for the default and all mapped namespaces. 705 */ 706 protected final boolean isAddNamespaceUrisToRoot() { return ctx.isAddNamespaceUrlsToRoot(); } 707 708 /** 709 * Auto-detect namespace usage. 710 * 711 * @see XmlSerializer.Builder#disableAutoDetectNamespaces() 712 * @return 713 * <jk>true</jk> if namespace usage is detected before serialization. 714 */ 715 protected final boolean isAutoDetectNamespaces() { return ctx.isAutoDetectNamespaces(); } 716 717 /** 718 * Enable support for XML namespaces. 719 * 720 * @see XmlSerializer.Builder#enableNamespaces() 721 * @return 722 * <jk>false</jk> if XML output will not contain any namespaces regardless of any other settings. 723 */ 724 protected final boolean isEnableNamespaces() { return ctx.isEnableNamespaces(); } 725 726 /** 727 * Returns <jk>true</jk> if we're serializing HTML. 728 * 729 * <p> 730 * The difference in behavior is how empty non-void elements are handled. 731 * The XML serializer will produce a collapsed tag, whereas the HTML serializer will produce a start and end tag. 732 * 733 * @return <jk>true</jk> if we're generating HTML. 734 */ 735 protected boolean isHtmlMode() { return false; } 736 737 /** 738 * Workhorse method. 739 * 740 * @param out The writer to send the output to. 741 * @param o The object to serialize. 742 * @param eType The expected type if this is a bean property value being serialized. 743 * @param keyName The property name or map key name. 744 * @param elementName The root element name. 745 * @param elementNamespace The namespace of the element. 746 * @param addNamespaceUris Flag indicating that namespace URIs need to be added. 747 * @param format The format to serialize the output to. 748 * @param isMixedOrText We're serializing mixed content, so don't use whitespace. 749 * @param preserveWhitespace 750 * <jk>true</jk> if we're serializing {@link XmlFormat#MIXED_PWS} or {@link XmlFormat#TEXT_PWS}. 751 * @param pMeta The bean property metadata if this is a bean property being serialized. 752 * @return The same writer passed in so that calls to the writer can be chained. 753 * @throws SerializeException General serialization error occurred. 754 */ 755 @SuppressWarnings("null") 756 protected ContentResult serializeAnything(XmlWriter out, Object o, ClassMeta<?> eType, String keyName, String elementName, Namespace elementNamespace, boolean addNamespaceUris, XmlFormat format, 757 boolean isMixedOrText, boolean preserveWhitespace, BeanPropertyMeta pMeta) throws SerializeException { 758 759 JsonType type = null; // The type string (e.g. <type> or <x x='type'> 760 int i = isMixedOrText ? 0 : indent; // Current indentation 761 ClassMeta<?> aType = null; // The actual type 762 ClassMeta<?> wType = null; // The wrapped type (delegate) 763 ClassMeta<?> sType = object(); // The serialized type 764 765 aType = push2(keyName, o, eType); 766 767 if (eType == null) 768 eType = object(); 769 770 // Handle recursion 771 if (aType == null) { 772 o = null; 773 aType = object(); 774 } 775 776 // Handle Optional<X> 777 if (isOptional(aType)) { 778 o = getOptionalValue(o); 779 eType = getOptionalType(eType); 780 aType = getClassMetaForObject(o, object()); 781 } 782 783 if (nn(o)) { 784 785 if (aType.isDelegate()) { 786 wType = aType; 787 eType = aType = ((Delegate<?>)o).getClassMeta(); 788 } 789 790 sType = aType; 791 792 // Swap if necessary 793 var swap = aType.getSwap(this); 794 if (nn(swap)) { 795 o = swap(swap, o); 796 sType = swap.getSwapClassMeta(this); 797 798 // If the getSwapClass() method returns Object, we need to figure out 799 // the actual type now. 800 if (sType.isObject()) 801 sType = getClassMetaForObject(o); 802 } 803 } else { 804 sType = eType.getSerializedClassMeta(this); 805 } 806 807 // Does the actual type match the expected type? 808 boolean isExpectedType = true; 809 if (o == null || ! eType.same(aType)) { 810 if (eType.isNumber()) 811 isExpectedType = aType.isNumber(); 812 else if (eType.isMap()) 813 isExpectedType = aType.isMap(); 814 else if (eType.isCollectionOrArray()) 815 isExpectedType = aType.isCollectionOrArray(); 816 else 817 isExpectedType = false; 818 } 819 820 var resolvedDictionaryName = isExpectedType ? null : aType.getBeanDictionaryName(); 821 822 // Note that the dictionary name may be specified on the actual type or the serialized type. 823 // HTML templates will have them defined on the serialized type. 824 var dictionaryName = aType.getBeanDictionaryName(); 825 if (dictionaryName == null) 826 dictionaryName = sType.getBeanDictionaryName(); 827 828 // char '\0' is interpreted as null. 829 if (nn(o) && sType.isChar() && ((Character)o).charValue() == 0) 830 o = null; 831 832 boolean isCollapsed = false; // If 'true', this is a collection and we're not rendering the outer element. 833 boolean isRaw = (sType.isReader() || sType.isInputStream()) && nn(o); 834 835 // Get the JSON type string. 836 if (o == null) { 837 type = NULL; 838 } else if (sType.isCharSequence() || sType.isChar()) { 839 type = STRING; 840 } else if (sType.isNumber()) { 841 type = NUMBER; 842 } else if (sType.isBoolean()) { 843 type = BOOLEAN; 844 } else if (sType.isMapOrBean()) { 845 isCollapsed = getXmlClassMeta(sType).getFormat() == COLLAPSED; 846 type = OBJECT; 847 } else if (sType.isCollectionOrArray()) { 848 isCollapsed = (format == COLLAPSED && ! addNamespaceUris); 849 type = ARRAY; 850 } else { 851 type = STRING; 852 } 853 854 if (format.isOneOf(MIXED, MIXED_PWS, TEXT, TEXT_PWS, XMLTEXT) && type.isOneOf(NULL, STRING, NUMBER, BOOLEAN)) 855 isCollapsed = true; 856 857 // Is there a name associated with this bean? 858 859 var name = keyName; 860 if (elementName == null && nn(dictionaryName)) { 861 elementName = dictionaryName; 862 isExpectedType = nn(o); // preserve type='null' when it's null. 863 } 864 865 if (elementName == null) { 866 elementName = name; 867 name = null; 868 } 869 870 if (eq(name, elementName)) 871 name = null; 872 873 if (isEnableNamespaces()) { 874 if (elementNamespace == null) 875 elementNamespace = getXmlClassMeta(sType).getNamespace(); 876 if (elementNamespace == null) 877 elementNamespace = getXmlClassMeta(aType).getNamespace(); 878 if (nn(elementNamespace) && elementNamespace.uri == null) 879 elementNamespace = null; 880 if (elementNamespace == null) 881 elementNamespace = defaultNamespace; 882 } else { 883 elementNamespace = null; 884 } 885 886 // Do we need a carriage return after the start tag? 887 boolean cr = nn(o) && (sType.isMapOrBean() || sType.isCollectionOrArray()) && ! isMixedOrText; 888 889 var en = elementName; 890 if (en == null && ! isRaw) { 891 if (isAddJsonTags()) { 892 en = type.toString(); 893 type = null; 894 } 895 } 896 897 boolean encodeEn = nn(elementName); 898 var ns = (elementNamespace == null ? null : elementNamespace.name); 899 var dns = (String)null; 900 var elementNs = (String)null; 901 if (isEnableNamespaces()) { 902 dns = elementName == null && nn(defaultNamespace) ? defaultNamespace.name : null; 903 elementNs = elementName == null ? dns : ns; 904 if (elementName == null) 905 elementNamespace = null; 906 } 907 908 // Render the start tag. 909 if (! isCollapsed) { 910 if (nn(en)) { 911 out.oTag(i, elementNs, en, encodeEn); 912 if (addNamespaceUris) { 913 out.attr((String)null, "xmlns", defaultNamespace.getUri()); 914 915 for (var n : namespaces) 916 out.attr("xmlns", n.getName(), n.getUri()); 917 } 918 if (! isExpectedType) { 919 if (nn(resolvedDictionaryName)) 920 out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName); 921 else if (nn(type) && type != STRING) 922 out.attr(dns, getBeanTypePropertyName(eType), type); 923 } 924 if (nn(name)) 925 out.attr(getNamePropertyName(), name); 926 } else { 927 out.i(i); 928 } 929 if (o == null) { 930 if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable()) 931 o = sType.getPrimitiveDefault(); 932 } 933 934 if (nn(o) && ! (sType.isMapOrBean() || en == null)) 935 out.w('>'); 936 937 if (cr && ! (sType.isMapOrBean())) 938 out.nl(i + 1); 939 } 940 941 var rc = CR_ELEMENTS; 942 943 // Render the tag contents. 944 if (nn(o)) { 945 if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 946 out.textUri(o); 947 } else if (sType.isCharSequence() || sType.isChar()) { 948 if (isXmlText(format, sType)) 949 out.append(o); 950 else 951 out.text(o, preserveWhitespace); 952 } else if (sType.isNumber() || sType.isBoolean()) { 953 out.append(o); 954 } else if (sType.isMap() || (nn(wType) && wType.isMap())) { 955 if (o instanceof BeanMap) 956 rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixedOrText); 957 else 958 rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixedOrText); 959 } else if (sType.isBean()) { 960 rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixedOrText); 961 } else if (sType.isCollection() || (nn(wType) && wType.isCollection())) { 962 if (isCollapsed) 963 indent--; 964 serializeCollection(out, o, sType, eType, pMeta, isMixedOrText); 965 if (isCollapsed) 966 indent++; 967 } else if (sType.isArray()) { 968 if (isCollapsed) 969 indent--; 970 serializeCollection(out, o, sType, eType, pMeta, isMixedOrText); 971 if (isCollapsed) 972 indent++; 973 } else if (sType.isReader()) { 974 pipe((Reader)o, out, SerializerSession::handleThrown); 975 } else if (sType.isInputStream()) { 976 pipe((InputStream)o, out, SerializerSession::handleThrown); 977 } else { 978 if (isXmlText(format, sType)) 979 out.append(toString(o)); 980 else 981 out.text(toString(o)); 982 } 983 } 984 985 pop(); 986 987 // Render the end tag. 988 if (! isCollapsed) { 989 if (nn(en)) { 990 if (rc == CR_EMPTY) { 991 if (isHtmlMode()) 992 out.w('>').eTag(elementNs, en, encodeEn); 993 else 994 out.w('/').w('>'); 995 } else if (rc == CR_VOID || o == null) { 996 out.w('/').w('>'); 997 } else 998 out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn); 999 } 1000 if (! isMixedOrText) 1001 out.nl(i); 1002 } 1003 1004 return rc; 1005 } 1006}