EN · DE · RU · FR · ES

#758: XmlObjectWriter.java

projectforge-business/src/main/java/org/projectforge/framework/xmlstream/XmlObjectWriter.java Java serialization engine — converts Java objects to XML. 387 lines. Uses dom4j. Source: projectforge-business/src/main/java/org/projectforge/framework/xmlstream/XmlObjectWriter.java 387 lines · 271 code · 80 comments · 36 blank
Purpose: The serialization engine of the XmlStream framework — converts Java objects to XML (dom4j Document). 387 lines. Javadoc explains the motivation: "A simple solution for streaming xml objects and to prevent default values from the xml output (because this feature isn't yet available in XStream)." — at the time of writing, XStream couldn't skip fields with default values, and for data export this is critical (to prevent XML bloat from thousands of <deleted>false</deleted> tags). Pairs with XmlObjectReader: Writer serializes Java → XML, Reader deserializes XML → Java. Both use dom4j as the XML library and @XmlObject/@XmlField annotations for format control.

Deduplication via XML Attributes — ATTR_ID and ATTR_REF_ID

This mechanism solves circular references and deduplication. Example: object A contains a reference to object B, and B contains a back-reference to A. Without deduplication, Writer would enter infinite recursion. With deduplication: A is written with o-id=0, B with o-id=1, back-reference to A is written as ref-id=0.

Written Objects Cache — writtenObjects

Map<String, Element> where key is className:hashCode (e.g., "org.projectforge.business.user.PFUserDO:987345678854"). This is an identity-based cache (by hashCode), not equals-based — two different objects with identical fields will both be written, but one object encountered twice will be written only once. Using hashCode() for identity is theoretically unreliable (collisions possible), but in practice for typical XML sizes (hundreds to thousands of objects), collision probability is vanishingly small.

Core Method — write(Branch parent, Object obj, String name, boolean asAttribute, boolean asCDATA)

Recursive serialization method. Logic mirrors the Reader's type-checking chain but in reverse:

  1. Determine tag name: first AliasMap, then @XmlObject.alias, then XmlRegistry, then fallback to FQCN
  2. Deduplication check: if object is already in writtenObjects — writes ref-id instead of full contents
  3. Converter: if type has a registered IConverter — calls converter.toString() and writes result as element text or attribute
  4. Enum: writes enum.name() (e.g., "ACTIVE", "PAID")
  5. Collection: creates a wrapper element and recursively writes each collection item
  6. Complex object: creates element, registers object in writtenObjects, iterates all fields via reflection, calls writeField() for each non-default field

Default-Value Skipping — The Killer Feature

Method isDefaultType(ann, value) checks whether the field value equals the default from @XmlField.default*. If so, the field is NOT written. This is why XmlStream was created:

hasDefaultType() checks whether a default was declared at all (distinguishes "default not set" from "default == false/0/null"). For Boolean — always true (false is a valid default). For other types — compares against MAGIC sentinels.

Attribute vs. Element Default — asAttributeAsDefault()

Some types are serialized as attributes by default (compact):

Field Ignoring — ignoreField(Field)

Static method used by both Writer and Reader. Skips fields with modifiers: static (class constants), final (immutable after creation), transient (explicitly non-serializable). Also, the protected method ignoreField(Object, Field) can be overridden by subclasses for additional filtering. Plus the @XmlOmitField annotation — an explicit skip marker regardless of modifiers.

Git History

868d6abb7 2025 → 2026 (copyright year update)
63081666f Source file headers: 2024→2025
9f874e26c MAJOR-CHANGE! Migration of integer id's to Long id's
b6092df09 Copyright 2023 → 2024
ab45d51fa Copyright 2001-2022 → 2001-2023
5f7ef41b8 Copyright 2021 → 2022
cd27dd997 package xstream → xmlstream
ceb63e8a1 Source code header: (C) 2001-2021
7c79f1922 Copyright of source header → 2020
24019a0f5 Eliminate DateHolder occurences
af35917ac More code cleanup
73a9755df Code cleanup (StringBuffer→StringBuilder, diamond, deprecated classes)
32f634b88 Optimize imports
000ca723d Remove pointless boolean expressions
dd5ca38ac CopyRight of all java file-header updated or created
a5bbdca6a Change logger to slf4j
f979e8a42 MGC-UPDATE: Update auf Version 3.0.0-SNAPSHOT
9ebb88522 Initial commit
Key changes: 9f874e26c — mass migration of all identifiers from Integer to Long (including refIdCounter). Previously object IDs in XML were Integer (max ~2 billion), after — Long (max ~9 quintillion). Critical for large installations where the counter could overflow. 24019a0f5 — eliminated DateHolder (pre-Java-8 class) from Writer, replaced with java.util.Date and java.time types via converters. cd27dd997 — package renamed from xstream to xmlstream; the commit message "(should be replaced by xstream later)" reveals the author planned to eventually replace the custom framework with XStream — a plan that was never executed.