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