XStreamTest.java| Commit | Date | Message |
|---|---|---|
f46333761 | 2019-04-12 | TestNG and JUnit4 replaced by JUnit5. Some tests (with powermock) are still JUnit4. No more TestNG tests exist. |
9ebb88522 | 2016-07-18 | Initial commit |
Originally created in 2016 as part of the SchemaExp tool. Migrated from JUnit4 to JUnit5 in April 2019. The core test logic has remained unchanged for 10 years.
package org.projectforge.tools.schemaexp;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.ReferenceByIdMarshallingStrategy;
public class XStreamTest {Lines 1–5: Standard imports. Serializable is required because some XStream serialization modes demand it. HashMap stores the parent→child mapping.
Line 7: org.junit.jupiter.api.Test — confirms this is JUnit 5 (Jupiter), not JUnit 4. The migration commit (f46333761) explicitly mentions this change.
Lines 9–10: The two XStream imports:
XStream — ThoughtWorks' XML serialization library, used by ProjectForge for database XML dumpsReferenceByIdMarshallingStrategy — the strategy that enables cyclic reference handling by assigning internal IDs to objects and referencing them instead of nesting inline. This is the single most important line in the test. public static class Parent implements Serializable {
Map<String, Child> childres = new HashMap<>();
}
public static class Child implements Serializable {
Parent parentRef;
}Parent (line 14): Holds a Map<String, Child> — one parent can reference multiple children. The field name childres has a typo (missing 'n' from "children") — a known minor code issue that doesn't affect functionality.
Child (line 19): Holds a single Parent parentRef — the back-reference that creates the cycle. Without this field, there would be no cycle to test.
static modifier: Both classes are static inner classes — they don't capture a reference to the outer XStreamTest instance. This prevents the test class itself from being serialized.
@Test
public void testXStream() {
Parent parent = new Parent();
Child child = new Child();
child.parentRef = parent; // child → parent (back-reference)
parent.childres.put("asdf", child); // parent → child (forward reference)
// Now both directions exist — a true cycle
XStream xstream = new XStream();
xstream.setMarshallingStrategy(new ReferenceByIdMarshallingStrategy());
String res = xstream.toXML(parent); // Marshal to XML
System.out.println("marshaled: " + res);
xstream = new XStream(); // Fresh instance (no stale state)
xstream.setMarshallingStrategy(new ReferenceByIdMarshallingStrategy());
Object object = xstream.fromXML(res); // Unmarshal from XML
Parent rparent = (Parent) object; // Cast succeeds = test passes
}
}Lines 27–30: Building the cycle. A Parent and Child are created. Line 29 sets child.parentRef = parent (child → parent). Line 30 puts the child into the parent's map with key "asdf" (parent → child). Both directions now exist.
Lines 32–34: Marshalling. A fresh XStream instance is configured with ReferenceByIdMarshallingStrategy. Without line 32, XStream would use TreeMarshallingStrategy which recursively serializes the entire graph — hitting the cycle and throwing StackOverflowError. The XML output will contain <parentRef reference="1"/> instead of inline-nesting the Parent object again.
Lines 36–38: Unmarshalling. A fresh XStream instance is created (line 36) to ensure no state leaks from the marshalling step. The same strategy is applied, then the XML string is deserialized. Line 38's (Parent) object cast is the implicit assertion — if the deserialization failed or the types don't match, a ClassCastException is thrown and the test fails.
No explicit assertions: The test has no assert* calls. It relies on exception-based failure: any StackOverflowError, ClassCastException, or XStream parsing error will cause the test to fail. This is a "smoke test" pattern — verify that the operation doesn't crash.
This test is the foundational validation for the entire SchemaExp tool chain. It proves that XStream can safely handle ProjectForge's entity graph before any production data is touched. The test suite flow:
| File | Role |
|---|---|
| #4108 XStreamTest | Validates cycle handling at the serialization library level |
| #4107 ToXmlTest | End-to-end export of real database to XML |
| #4106 FromXmlTest | End-to-end import of XML into clean database |
| #4105 ClearDbTest | Validates database clearing in isolation |
| #4104 SchemaExpService | Production service using the validated XStream configuration |
In production: The admin UI's "XML Backup/Restore" feature and the setup wizard's "Test system" initialization both depend on this serialization working correctly. A failure here means no XML backups, no test data import, and no database migration between vendors (HSQLDB → PostgreSQL).
@Test from org.junit.jupiter.api confirms JUnit 5. All new tests should follow this pattern.rparent.childres.get("asdf").parentRef == rparent).childres on line 16 should be children. Harmless but worth fixing for readability.
This test validates a critical serialization concern for the SchemaExp tool: can XStream correctly serialize and deserialize Java objects that contain circular references? The ProjectForge domain model is full of bidirectional relationships:
UserDO ↔ GroupDO,TaskDO ↔ TaskDO(parent/child),RechnungDO ↔ RechnungsPositionDO(invoice ↔ line items). Without proper cycle handling, XStream's defaultTreeMarshallingStrategywould enter infinite recursion on these objects and throw aStackOverflowError.The test constructs a minimal parent-child cycle (Parent has Map<String, Child>, each Child has a back-reference to Parent), configures XStream with
ReferenceByIdMarshallingStrategy, and verifies that marshalling → unmarshalling completes without errors. The strategy replaces repeated object references with internal IDs in the XML output and reconstructs the full object graph during deserialization.