001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.xml; 014 015import static org.apache.juneau.common.internal.IOUtils.*; 016import static org.apache.juneau.common.internal.StringUtils.*; 017import static org.apache.juneau.internal.ArrayUtils.*; 018import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*; 019import static org.apache.juneau.xml.XmlSerializerSession.JsonType.*; 020import static org.apache.juneau.xml.annotation.XmlFormat.*; 021 022import java.io.*; 023import java.lang.reflect.*; 024import java.nio.charset.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.*; 029import org.apache.juneau.httppart.*; 030import org.apache.juneau.internal.*; 031import org.apache.juneau.serializer.*; 032import org.apache.juneau.svl.*; 033import org.apache.juneau.swap.*; 034import org.apache.juneau.xml.annotation.*; 035 036/** 037 * Session object that lives for the duration of a single use of {@link XmlSerializer}. 038 * 039 * <h5 class='section'>Notes:</h5><ul> 040 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 041 * </ul> 042 * 043 * <h5 class='section'>See Also:</h5><ul> 044 * <li class='link'><a class="doclink" href="../../../../index.html#jm.XmlDetails">XML Details</a> 045 046 * </ul> 047 */ 048@SuppressWarnings({"unchecked","rawtypes"}) 049public class XmlSerializerSession extends WriterSerializerSession { 050 051 //----------------------------------------------------------------------------------------------------------------- 052 // Static 053 //----------------------------------------------------------------------------------------------------------------- 054 055 /** 056 * Creates a new builder for this object. 057 * 058 * @param ctx The context creating this session. 059 * @return A new builder. 060 */ 061 public static Builder create(XmlSerializer ctx) { 062 return new Builder(ctx); 063 } 064 065 //----------------------------------------------------------------------------------------------------------------- 066 // Builder 067 //----------------------------------------------------------------------------------------------------------------- 068 069 /** 070 * Builder class. 071 */ 072 @FluentSetters 073 public static class Builder extends WriterSerializerSession.Builder { 074 075 XmlSerializer ctx; 076 077 /** 078 * Constructor 079 * 080 * @param ctx The context creating this session. 081 */ 082 protected Builder(XmlSerializer ctx) { 083 super(ctx); 084 this.ctx = ctx; 085 } 086 087 @Override 088 public XmlSerializerSession build() { 089 return new XmlSerializerSession(this); 090 } 091 092 // <FluentSetters> 093 094 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 095 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 096 super.apply(type, apply); 097 return this; 098 } 099 100 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 101 public Builder debug(Boolean value) { 102 super.debug(value); 103 return this; 104 } 105 106 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 107 public Builder properties(Map<String,Object> value) { 108 super.properties(value); 109 return this; 110 } 111 112 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 113 public Builder property(String key, Object value) { 114 super.property(key, value); 115 return this; 116 } 117 118 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 119 public Builder unmodifiable() { 120 super.unmodifiable(); 121 return this; 122 } 123 124 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 125 public Builder locale(Locale value) { 126 super.locale(value); 127 return this; 128 } 129 130 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 131 public Builder localeDefault(Locale value) { 132 super.localeDefault(value); 133 return this; 134 } 135 136 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 137 public Builder mediaType(MediaType value) { 138 super.mediaType(value); 139 return this; 140 } 141 142 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 143 public Builder mediaTypeDefault(MediaType value) { 144 super.mediaTypeDefault(value); 145 return this; 146 } 147 148 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 149 public Builder timeZone(TimeZone value) { 150 super.timeZone(value); 151 return this; 152 } 153 154 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 155 public Builder timeZoneDefault(TimeZone value) { 156 super.timeZoneDefault(value); 157 return this; 158 } 159 160 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 161 public Builder javaMethod(Method value) { 162 super.javaMethod(value); 163 return this; 164 } 165 166 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 167 public Builder resolver(VarResolverSession value) { 168 super.resolver(value); 169 return this; 170 } 171 172 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 173 public Builder schema(HttpPartSchema value) { 174 super.schema(value); 175 return this; 176 } 177 178 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 179 public Builder schemaDefault(HttpPartSchema value) { 180 super.schemaDefault(value); 181 return this; 182 } 183 184 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 185 public Builder uriContext(UriContext value) { 186 super.uriContext(value); 187 return this; 188 } 189 190 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 191 public Builder fileCharset(Charset value) { 192 super.fileCharset(value); 193 return this; 194 } 195 196 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 197 public Builder streamCharset(Charset value) { 198 super.streamCharset(value); 199 return this; 200 } 201 202 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 203 public Builder useWhitespace(Boolean value) { 204 super.useWhitespace(value); 205 return this; 206 } 207 208 // </FluentSetters> 209 } 210 211 //----------------------------------------------------------------------------------------------------------------- 212 // Instance 213 //----------------------------------------------------------------------------------------------------------------- 214 215 private final XmlSerializer ctx; 216 private Namespace 217 defaultNamespace; 218 private Namespace[] namespaces = new Namespace[0]; 219 220 /** 221 * Constructor. 222 * 223 * @param builder The builder for this object. 224 */ 225 protected XmlSerializerSession(Builder builder) { 226 super(builder); 227 ctx = builder.ctx; 228 namespaces = ctx.getNamespaces(); 229 defaultNamespace = findDefaultNamespace(ctx.getDefaultNamespace()); 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(x -> x.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(x -> findNsfMappings(x)); 358 } 359 360 } else if (aType.isMap()) { 361 ((Map<?,?>)o).forEach((k,v) -> findNsfMappings(v)); 362 } else if (aType.isCollection()) { 363 ((Collection<?>)o).forEach(x -> findNsfMappings(x)); 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 (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 en = type.toString(); 551 type = null; 552 } 553 554 boolean encodeEn = elementName != null; 555 String ns = (elementNamespace == null ? null : elementNamespace.name); 556 String dns = null, elementNs = null; 557 if (isEnableNamespaces()) { 558 dns = elementName == null && defaultNamespace != null ? defaultNamespace.name : null; 559 elementNs = elementName == null ? dns : ns; 560 if (elementName == null) 561 elementNamespace = null; 562 } 563 564 // Render the start tag. 565 if (! isCollapsed) { 566 if (en != null) { 567 out.oTag(i, elementNs, en, encodeEn); 568 if (addNamespaceUris) { 569 out.attr((String)null, "xmlns", defaultNamespace.getUri()); 570 571 for (Namespace n : namespaces) 572 out.attr("xmlns", n.getName(), n.getUri()); 573 } 574 if (! isExpectedType) { 575 if (resolvedDictionaryName != null) 576 out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName); 577 else if (type != null && type != STRING) 578 out.attr(dns, getBeanTypePropertyName(eType), type); 579 } 580 if (name != null) 581 out.attr(getNamePropertyName(), name); 582 } else { 583 out.i(i); 584 } 585 if (o == null) { 586 if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable()) 587 o = sType.getPrimitiveDefault(); 588 } 589 590 if (o != null && ! (sType.isMapOrBean() || en == null)) 591 out.w('>'); 592 593 if (cr && ! (sType.isMapOrBean())) 594 out.nl(i+1); 595 } 596 597 ContentResult rc = CR_ELEMENTS; 598 599 // Render the tag contents. 600 if (o != null) { 601 if (sType.isUri() || (pMeta != null && pMeta.isUri())) { 602 out.textUri(o); 603 } else if (sType.isCharSequence() || sType.isChar()) { 604 if (isXmlText(format, sType)) 605 out.append(o); 606 else 607 out.text(o, preserveWhitespace); 608 } else if (sType.isNumber() || sType.isBoolean()) { 609 out.append(o); 610 } else if (sType.isMap() || (wType != null && wType.isMap())) { 611 if (o instanceof BeanMap) 612 rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixedOrText); 613 else 614 rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixedOrText); 615 } else if (sType.isBean()) { 616 rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixedOrText); 617 } else if (sType.isCollection() || (wType != null && wType.isCollection())) { 618 if (isCollapsed) 619 this.indent--; 620 serializeCollection(out, o, sType, eType, pMeta, isMixedOrText); 621 if (isCollapsed) 622 this.indent++; 623 } else if (sType.isArray()) { 624 if (isCollapsed) 625 this.indent--; 626 serializeCollection(out, o, sType, eType, pMeta, isMixedOrText); 627 if (isCollapsed) 628 this.indent++; 629 } else if (sType.isReader()) { 630 pipe((Reader)o, out, SerializerSession::handleThrown); 631 } else if (sType.isInputStream()) { 632 pipe((InputStream)o, out, SerializerSession::handleThrown); 633 } else { 634 if (isXmlText(format, sType)) 635 out.append(toString(o)); 636 else 637 out.text(toString(o)); 638 } 639 } 640 641 pop(); 642 643 // Render the end tag. 644 if (! isCollapsed) { 645 if (en != null) { 646 if (rc == CR_EMPTY) { 647 if (isHtmlMode()) 648 out.w('>').eTag(elementNs, en, encodeEn); 649 else 650 out.w('/').w('>'); 651 } else if (rc == CR_VOID || o == null) { 652 out.w('/').w('>'); 653 } 654 else 655 out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn); 656 } 657 if (! isMixedOrText) 658 out.nl(i); 659 } 660 661 return rc; 662 } 663 664 private boolean isXmlText(XmlFormat format, ClassMeta<?> sType) { 665 if (format == XMLTEXT) 666 return true; 667 XmlClassMeta xcm = getXmlClassMeta(sType); 668 if (xcm == null) 669 return false; 670 return xcm.getFormat() == XMLTEXT; 671 } 672 673 private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType, 674 ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws SerializeException { 675 676 ClassMeta<?> keyType = eKeyType == null ? sType.getKeyType() : eKeyType; 677 ClassMeta<?> valueType = eValueType == null ? sType.getValueType() : eValueType; 678 679 Flag hasChildren = Flag.create(); 680 forEachEntry(m, e -> { 681 682 Object k = e.getKey(); 683 if (k == null) { 684 k = "\u0000"; 685 } else { 686 k = generalize(k, keyType); 687 if (isTrimStrings() && k instanceof String) 688 k = k.toString().trim(); 689 } 690 691 Object value = e.getValue(); 692 693 hasChildren.ifNotSet(()->out.w('>').nlIf(! isMixed, indent)).set(); 694 serializeAnything(out, value, valueType, toString(k), null, null, false, XmlFormat.DEFAULT, isMixed, false, null); 695 }); 696 697 return hasChildren.isSet() ? CR_ELEMENTS : CR_EMPTY; 698 } 699 700 private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m, 701 Namespace elementNs, boolean isCollapsed, boolean isMixedOrText) throws SerializeException { 702 boolean hasChildren = false; 703 BeanMeta<?> bm = m.getMeta(); 704 705 List<BeanPropertyValue> lp = new ArrayList<>(); 706 707 Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null; 708 m.forEachValue(checkNull, (pMeta,key,value,thrown) -> { 709 lp.add(new BeanPropertyValue(pMeta, key, value, thrown)); 710 }); 711 712 XmlBeanMeta xbm = getXmlBeanMeta(bm); 713 714 Set<String> 715 attrs = xbm.getAttrPropertyNames(), 716 elements = xbm.getElementPropertyNames(), 717 collapsedElements = xbm.getCollapsedPropertyNames(); 718 String 719 attrsProperty = xbm.getAttrsPropertyName(), 720 contentProperty = xbm.getContentPropertyName(); 721 722 XmlFormat cf = null; 723 724 Object content = null; 725 ClassMeta<?> contentType = null; 726 for (BeanPropertyValue p : lp) { 727 String n = p.getName(); 728 if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) { 729 BeanPropertyMeta pMeta = p.getMeta(); 730 if (pMeta.canRead()) { 731 ClassMeta<?> cMeta = p.getClassMeta(); 732 733 String key = p.getName(); 734 Object value = p.getValue(); 735 Throwable t = p.getThrown(); 736 if (t != null) 737 onBeanGetterException(pMeta, t); 738 739 if (canIgnoreValue(cMeta, key, value)) 740 continue; 741 742 XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta); 743 Namespace ns = (isEnableNamespaces() && bpXml.getNamespace() != elementNs ? bpXml.getNamespace() : null); 744 745 if (pMeta.isUri() ) { 746 out.attrUri(ns, key, value); 747 } else if (n.equals(attrsProperty)) { 748 if (value instanceof BeanMap) { 749 BeanMap<?> bm2 = (BeanMap)value; 750 bm2.forEachValue(x -> true, (pMeta2,key2,value2,thrown2) -> { 751 if (thrown2 != null) 752 onBeanGetterException(pMeta, thrown2); 753 out.attr(ns, key2, value2); 754 }); 755 } else /* Map */ { 756 Map m2 = (Map)value; 757 if (m2 != null) 758 m2.forEach((k,v) -> out.attr(ns, stringify(k), v)); 759 } 760 } else { 761 out.attr(ns, key, value); 762 } 763 } 764 } 765 } 766 767 boolean 768 hasContent = false, 769 preserveWhitespace = false, 770 isVoidElement = xbm.getContentFormat() == VOID; 771 772 for (BeanPropertyValue p : lp) { 773 BeanPropertyMeta pMeta = p.getMeta(); 774 if (pMeta.canRead()) { 775 ClassMeta<?> cMeta = p.getClassMeta(); 776 777 String n = p.getName(); 778 if (n.equals(contentProperty)) { 779 content = p.getValue(); 780 contentType = p.getClassMeta(); 781 hasContent = true; 782 cf = xbm.getContentFormat(); 783 if (cf.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT)) 784 isMixedOrText = true; 785 if (cf.isOneOf(MIXED_PWS, TEXT_PWS)) 786 preserveWhitespace = true; 787 if (contentType.isCollection() && ((Collection)content).isEmpty()) 788 hasContent = false; 789 else if (contentType.isArray() && Array.getLength(content) == 0) 790 hasContent = false; 791 } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*") ) { 792 String key = p.getName(); 793 Object value = p.getValue(); 794 Throwable t = p.getThrown(); 795 if (t != null) 796 onBeanGetterException(pMeta, t); 797 798 if (canIgnoreValue(cMeta, key, value)) 799 continue; 800 801 if (! hasChildren) { 802 hasChildren = true; 803 out.appendIf(! isCollapsed, '>').nlIf(! isMixedOrText, indent); 804 } 805 806 XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta); 807 serializeAnything(out, value, cMeta, key, null, bpXml.getNamespace(), false, bpXml.getXmlFormat(), isMixedOrText, false, pMeta); 808 } 809 } 810 } 811 if (contentProperty == null && ! hasContent) 812 return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY); 813 814 // Serialize XML content. 815 if (content != null) { 816 out.w('>').nlIf(! isMixedOrText, indent); 817 if (contentType == null) { 818 } else if (contentType.isCollection()) { 819 Collection c = (Collection)content; 820 for (Iterator i = c.iterator(); i.hasNext();) { 821 Object value = i.next(); 822 serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 823 } 824 } else if (contentType.isArray()) { 825 Collection c = toList(Object[].class, content); 826 for (Iterator i = c.iterator(); i.hasNext();) { 827 Object value = i.next(); 828 serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 829 } 830 } else { 831 serializeAnything(out, content, contentType, null, null, null, false, cf, isMixedOrText, preserveWhitespace, null); 832 } 833 } else { 834 out.attr("nil", "true").w('>').nlIf(! isMixedOrText, indent); 835 } 836 return isMixedOrText ? CR_MIXED : CR_ELEMENTS; 837 } 838 839 private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, 840 ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws SerializeException { 841 842 ClassMeta<?> eeType = eType.getElementType(); 843 844 Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in)); 845 846 String type2 = null; 847 848 Value<String> eName = Value.of(type2); 849 Value<Namespace> eNs = Value.empty(); 850 851 if (ppMeta != null) { 852 XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(ppMeta); 853 eName.set(bpXml.getChildName()); 854 eNs.set(bpXml.getNamespace()); 855 } 856 857 forEachEntry(c, x -> serializeAnything(out, x, eeType, null, eName.get(), eNs.get(), false, XmlFormat.DEFAULT, isMixed, false, null)); 858 859 return out; 860 } 861 862 static enum JsonType { 863 STRING("string"),BOOLEAN("boolean"),NUMBER("number"),ARRAY("array"),OBJECT("object"),NULL("null"); 864 865 private final String value; 866 private JsonType(String value) { 867 this.value = value; 868 } 869 870 @Override 871 public String toString() { 872 return value; 873 } 874 875 boolean isOneOf(JsonType...types) { 876 for (JsonType type : types) 877 if (type == this) 878 return true; 879 return false; 880 } 881 } 882 883 /** 884 * Identifies what the contents were of a serialized bean. 885 */ 886 @SuppressWarnings("javadoc") 887 public static enum ContentResult { 888 CR_VOID, // No content...append "/>" to the start tag. 889 CR_EMPTY, // No content...append "/>" to the start tag if XML, "/></end>" if HTML. 890 CR_MIXED, // Mixed content...don't add whitespace. 891 CR_ELEMENTS // Elements...use normal whitespace rules. 892 } 893 894 //----------------------------------------------------------------------------------------------------------------- 895 // Properties 896 //----------------------------------------------------------------------------------------------------------------- 897 898 /** 899 * Add <js>"_type"</js> properties when needed. 900 * 901 * @see XmlSerializer.Builder#addBeanTypesXml() 902 * @return 903 * <jk>true</jk> if<js>"_type"</js> properties will be added to beans if their type cannot be inferred 904 * through reflection. 905 */ 906 @Override 907 protected boolean isAddBeanTypes() { 908 return ctx.isAddBeanTypes(); 909 } 910 911 /** 912 * Add namespace URLs to the root element. 913 * 914 * @see XmlSerializer.Builder#addNamespaceUrisToRoot() 915 * @return 916 * <jk>true</jk> if {@code xmlns:x} attributes are added to the root element for the default and all mapped namespaces. 917 */ 918 protected final boolean isAddNamespaceUrisToRoot() { 919 return ctx.isAddNamespaceUrlsToRoot(); 920 } 921 922 /** 923 * Auto-detect namespace usage. 924 * 925 * @see XmlSerializer.Builder#disableAutoDetectNamespaces() 926 * @return 927 * <jk>true</jk> if namespace usage is detected before serialization. 928 */ 929 protected final boolean isAutoDetectNamespaces() { 930 return ctx.isAutoDetectNamespaces(); 931 } 932 933 /** 934 * Default namespace. 935 * 936 * @see XmlSerializer.Builder#defaultNamespace(Namespace) 937 * @return 938 * The default namespace URI for this document. 939 */ 940 protected final Namespace getDefaultNamespace() { 941 return defaultNamespace; 942 } 943 944 /** 945 * Enable support for XML namespaces. 946 * 947 * @see XmlSerializer.Builder#enableNamespaces() 948 * @return 949 * <jk>false</jk> if XML output will not contain any namespaces regardless of any other settings. 950 */ 951 protected final boolean isEnableNamespaces() { 952 return ctx.isEnableNamespaces(); 953 } 954 955 /** 956 * Default namespaces. 957 * 958 * @see XmlSerializer.Builder#namespaces(Namespace...) 959 * @return 960 * The default list of namespaces associated with this serializer. 961 */ 962 protected final Namespace[] getNamespaces() { 963 return namespaces; 964 } 965 966 //----------------------------------------------------------------------------------------------------------------- 967 // Extended metadata 968 //----------------------------------------------------------------------------------------------------------------- 969 970 /** 971 * Returns the language-specific metadata on the specified class. 972 * 973 * @param cm The class to return the metadata on. 974 * @return The metadata. 975 */ 976 public XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) { 977 return ctx.getXmlClassMeta(cm); 978 } 979 980 /** 981 * Returns the language-specific metadata on the specified bean. 982 * 983 * @param bm The bean to return the metadata on. 984 * @return The metadata. 985 */ 986 public XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) { 987 return ctx.getXmlBeanMeta(bm); 988 } 989 990 /** 991 * Returns the language-specific metadata on the specified bean property. 992 * 993 * @param bpm The bean property to return the metadata on. 994 * @return The metadata. 995 */ 996 public XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) { 997 return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm); 998 } 999}