/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.core.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import io.permazen.core.CollectionField;
import io.permazen.core.CounterField;
import io.permazen.core.DeletedObjectException;
import io.permazen.core.Field;
import io.permazen.core.FieldType;
import io.permazen.core.ListField;
import io.permazen.core.MapField;
import io.permazen.core.ObjId;
import io.permazen.core.ObjType;
import io.permazen.core.ReferenceField;
import io.permazen.core.Schema;
import io.permazen.core.SetField;
import io.permazen.core.SimpleField;
import io.permazen.core.SnapshotTransaction;
import io.permazen.core.Transaction;
import io.permazen.core.UnknownTypeException;
import io.permazen.schema.NameIndex;
import io.permazen.schema.SchemaField;
import io.permazen.schema.SchemaModel;
import io.permazen.schema.SchemaObjectType;
import io.permazen.util.AbstractXMLStreaming;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import org.dellroad.stuff.xml.IndentXMLStreamWriter;
/**
* Utility methods for serializing and deserializing objects in a {@link Transaction} to/from XML.
*
*
* There are two supported formats. The "Storage ID Format" specifies objects and fields using their storage IDs
* and supports all possible database contents.
*
* <objects>
* <object storageId="100" id="64a971e1aef01cc8" version="1">
* <field storageId="101">George Washington</field>
* <field storageId="102">true</field>
* <field storageId="103">
* <entry>
* <key>teeth</key>
* <value>wooden</value>
* </entry>
* </field>
* <field storageId="104">c8b84a08e5c2b1a2</field>
* </object>
* ...
* </objects>
*
*
*
* "Name Format" differs from "Storage ID Format" in that object types and fields are specified by name instead of storage ID.
* However, it does not support schemas that use names that are not valid XML tags.
*
* <objects>
* <Person id="64a971e1aef01cc8" version="1">
* <name>George Washington</name>
* <wasPresident>true</wasPresident>
* <attributes>
* <entry>
* <key>teeth</key>
* <value>wooden</value>
* </entry>
* </attributes>
* <spouse>c8b84a08e5c2b1a2</spouse>
* </Person>
* ...
* </objects>
*
*
*
* Some details on the input logic:
*
* Simple fields that are equal to their default values, and complex fields that are empty, may be omitted
* The {@code "version"} attribute may be omitted, in which case the default schema version associated with
* the {@link Transaction} being written to is assumed.
* The {@code "id"} attribute may be omitted, in which case a random unassigned ID is generated
* Any object ID (including the {@code "id"} attribute) may have the special form
* generated:TYPE :SUFFIX
, where TYPE
is the object type
* name or storage ID and SUFFIX
is an arbitrary string.
* In this case, a random, unassigned object ID for type TYPE
is generated
* on first occurrence, and on subsequent occurences the previously generated ID is recalled. This facilitates
* input generated via XSL and the {@code generate-id()} function.
* The {@linkplain #getGeneratedIdCache configured} {@link GeneratedIdCache} keeps track of generated IDs.
* XML elements and annotations are expected to be in the null namespace; elements and annotations in other
* namespaces are ignored
*
*/
public class XMLObjectSerializer extends AbstractXMLStreaming {
/**
* The supported XML namespace URI.
*
*
* Currently this is {@link XMLConstants#NULL_NS_URI}, i.e., the null/default namespace.
*
*
* XML tags and attributes whose names are in other namespaces are ignored.
*/
public static final String NS_URI = XMLConstants.NULL_NS_URI;
public static final QName ELEMENT_TAG = new QName("element");
public static final QName ENTRY_TAG = new QName("entry");
public static final QName FIELD_TAG = new QName("field");
public static final QName KEY_TAG = new QName("key");
public static final QName OBJECTS_TAG = new QName("objects");
public static final QName OBJECT_TAG = new QName("object");
public static final QName VALUE_TAG = new QName("value");
public static final QName ID_ATTR = new QName("id");
public static final QName STORAGE_ID_ATTR = new QName("storageId");
public static final QName VERSION_ATTR = new QName("version");
public static final QName NULL_ATTR = new QName("null");
private static final Pattern GENERATED_ID_PATTERN = Pattern.compile("generated:([^:]+):(.*)");
private final Transaction tx;
private final HashMap nameIndexMap = new HashMap<>();
private GeneratedIdCache generatedIdCache = new GeneratedIdCache();
private final ObjIdMap unresolvedReferences = new ObjIdMap<>();
private boolean omitDefaultValueFields = true;
private int fieldTruncationLength = -1;
/**
* Constructor.
*
* @param tx {@link Transaction} on which to operate
* @throws IllegalArgumentException if {@code tx} is null
*/
public XMLObjectSerializer(Transaction tx) {
Preconditions.checkArgument(tx != null, "null tx");
this.tx = tx;
// Build name index for each schema version
for (Schema schema : this.tx.getSchemas().getVersions().values())
nameIndexMap.put(schema.getVersionNumber(), new NameIndex(schema.getSchemaModel()));
}
/**
* Get the maximum length (number of characters) of any written simple field.
*
*
* By default, this value is set to {@code -1}, i.e., truncation is disabled.
*
* @return maximum simple field length, or zero for empty simple fields, or {@code -1} if truncation is disabled
* @see #setFieldTruncationLength setFieldTruncationLength()
*/
public int getFieldTruncationLength() {
return this.fieldTruncationLength;
}
/**
* Set the maximum length (number of characters) of any written simple field.
*
*
* Simple field values longer than this will be truncated. If set to zero, all simple field values are written as empty tags.
* If set to {@code -1}, truncation is disabled.
*
*
* Truncation is mainly useful for generating human-readable output without very long lines.
* Obviously, when truncation is enabled, the resulting output, although still valid XML, will
* be missing some information and therefore cannot successfully be read back in by this class.
*
* @param length maximum simple field length, or zero for empty simple fields, or {@code -1} to disable truncation
* @throws IllegalArgumentException if {@code length < -1}
*/
public void setFieldTruncationLength(int length) {
Preconditions.checkArgument(length >= -1, "length < -1");
this.fieldTruncationLength = length;
}
/**
* Get whether to omit fields whose value equals the default value for the field's type.
*
*
* Default true.
*
* @return whether to omit fields with default values
*/
public boolean isOmitDefaultValueFields() {
return this.omitDefaultValueFields;
}
/**
* Set whether to omit fields whose value equals the default value for the field's type.
*
*
* Default true.
*
* @param omitDefaultValueFields true to omit fields with default values
*/
public void setOmitDefaultValueFields(final boolean omitDefaultValueFields) {
this.omitDefaultValueFields = omitDefaultValueFields;
}
/**
* Get all unresolved forward object references.
*
*
* When {@link #read(InputStream, boolean) read()} is invoked with {@code allowUnresolvedReferences = true},
* unresolved forward object references do not trigger an exception; this allows forward references to span
* multiple invocations. Instead, these references are collected and made available to the caller in the returned map.
* Callers may also modify the returned map as desired between invocations.
*
* @return mapping from unresolved forward object reference to some referring field
*/
public ObjIdMap getUnresolvedReferences() {
return this.unresolvedReferences;
}
/**
* Get the {@link GeneratedIdCache} associated with this instance.
*
* @return the associated {@link GeneratedIdCache}
*/
public GeneratedIdCache getGeneratedIdCache() {
return this.generatedIdCache;
}
/**
* Set the {@link GeneratedIdCache} associated with this instance.
*
* @param generatedIdCache the {@link GeneratedIdCache} for this instance to use
* @throws IllegalArgumentException if {@code generatedIdCache} is null
*/
public void setGeneratedIdCache(GeneratedIdCache generatedIdCache) {
Preconditions.checkArgument(generatedIdCache != null, "null generatedIdCache");
this.generatedIdCache = generatedIdCache;
}
/**
* Import objects pairs into the {@link Transaction} associated with this instance from the given XML input.
*
*
* This is a convenience method, equivalent to:
*
* read(input, false)
*
*
* @param input XML input
* @return the number of objects read
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code input} is null
*/
public int read(InputStream input) throws XMLStreamException {
return this.read(input, false);
}
/**
* Import objects pairs into the {@link Transaction} associated with this instance from the given XML input.
*
*
* The input format is auto-detected for each {@code } based on the presense of the {@code "type"} attribute.
*
*
* Can optionally check for unresolved object references after reading is complete. If this checking is enabled,
* an exception is thrown if any unresolved references remain. In any case, the unresolved references are available
* via {@link #getUnresolvedReferences}.
*
* @param input XML input
* @param allowUnresolvedReferences true to allow unresolved references, false to throw an exception
* @return the number of objects read
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code input} is null
* @throws DeletedObjectException if {@code allowUnresolvedReferences} is true and any unresolved references
* remain when loading is complete
*/
public int read(InputStream input, boolean allowUnresolvedReferences) throws XMLStreamException {
Preconditions.checkArgument(input != null, "null input");
final int count = this.read(XMLInputFactory.newFactory().createXMLStreamReader(input));
if (!allowUnresolvedReferences && !this.unresolvedReferences.isEmpty()) {
throw new DeletedObjectException(this.unresolvedReferences.keySet().iterator().next(),
this.unresolvedReferences.size() + " unresolved reference(s) remain");
}
return count;
}
/**
* Import objects into the {@link Transaction} associated with this instance from the given XML input.
* This method expects to see an opening {@code } as the next event (not counting whitespace, comments, etc.),
* which is then consumed up through the closing {@code } event. Therefore this tag could be part of a
* larger XML document.
*
*
* The input format is auto-detected for each {@code } based on the presense of the {@code "type"} attribute.
*
* @param reader XML reader
* @return the number of objects read
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code reader} is null
*/
@SuppressWarnings("unchecked")
public int read(XMLStreamReader reader) throws XMLStreamException {
Preconditions.checkArgument(reader != null, "null reader");
this.expect(reader, false, OBJECTS_TAG);
// Create a snapshot transaction so we can replace objects without triggering DeleteAction's
final SnapshotTransaction snapshot = this.tx.createSnapshotTransaction();
// Iterate over objects
QName name;
int count;
for (count = 0; (name = this.next(reader)) != null; count++) {
// Determine schema version
final String versionAttr = this.getAttr(reader, VERSION_ATTR, false);
final Schema schema;
if (versionAttr != null) {
try {
schema = this.tx.getSchemas().getVersion(Integer.parseInt(versionAttr));
} catch (IllegalArgumentException e) {
throw new XMLStreamException("invalid object schema version `" + versionAttr + "': " + e,
reader.getLocation(), e);
}
} else
schema = this.tx.getSchema();
final NameIndex nameIndex = this.nameIndexMap.get(schema.getVersionNumber());
final SchemaModel schemaModel = schema.getSchemaModel();
// Determine object type
String storageIdAttr = this.getAttr(reader, STORAGE_ID_ATTR, false);
ObjType objType = null;
if (storageIdAttr != null) {
try {
objType = schema.getObjType(Integer.parseInt(storageIdAttr));
} catch (UnknownTypeException e) {
throw new XMLStreamException("invalid object type storage ID `" + storageIdAttr + "': " + e,
reader.getLocation(), e);
}
if (!name.equals(OBJECT_TAG)) {
if (!XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) {
throw new XMLStreamException("unexpected element <" + name.getPrefix() + ":"
+ name.getLocalPart() + ">; expected <" + OBJECT_TAG.getLocalPart() + ">", reader.getLocation());
}
if (!objType.getName().equals(name.getLocalPart())) {
throw new XMLStreamException("element <" + name.getLocalPart() + "> does not match storage ID "
+ objType.getStorageId() + "; should be <" + objType.getName() + ">", reader.getLocation());
}
}
} else {
if (!XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) {
throw new XMLStreamException("unexpected element <" + name.getPrefix() + ":"
+ name.getLocalPart() + ">; expected or object type name", reader.getLocation());
}
if (!name.equals(OBJECT_TAG)) {
final SchemaObjectType schemaObjectType = nameIndex.getSchemaObjectType(name.getLocalPart());
if (schemaObjectType == null) {
throw new XMLStreamException("unexpected element <" + name.getLocalPart()
+ ">; no object type named `" + name.getLocalPart() + "' found in schema version "
+ schema.getVersionNumber(), reader.getLocation());
}
objType = schema.getObjType(schemaObjectType.getStorageId());
}
}
// Reset snapshot
snapshot.reset();
// Determine object ID and create object in snapshot
final String idAttr = this.getAttr(reader, ID_ATTR, false);
ObjId id;
if (idAttr == null) {
// Verify we know object type
if (objType == null) {
throw new XMLStreamException("invalid <" + OBJECT_TAG.getLocalPart() + "> element: either \""
+ STORAGE_ID_ATTR.getLocalPart() + "\" or \"" + ID_ATTR.getLocalPart() + "\" attribute is required",
reader.getLocation());
}
// Create object
id = snapshot.create(objType.getStorageId(), schema.getVersionNumber());
} else {
// Parse id
try {
id = new ObjId(idAttr);
} catch (IllegalArgumentException e) {
final Matcher matcher = GENERATED_ID_PATTERN.matcher(idAttr);
if (!matcher.matches())
throw new XMLStreamException("invalid object ID `" + idAttr + "'", reader.getLocation());
final String typeAttr = matcher.group(1);
final ObjType genType = this.parseGeneratedType(reader, idAttr, typeAttr, schema);
if (objType == null)
objType = genType;
else if (!genType.equals(objType)) {
throw new XMLStreamException("type `" + typeAttr + "' in generated object ID `" + idAttr
+ "' does not match type `" + objType.getName() + "' in schema version "
+ schema.getVersionNumber(), reader.getLocation());
}
id = this.generatedIdCache.getGeneratedId(this.tx, objType.getStorageId(), matcher.group(2));
}
// Create object
snapshot.create(id, schema.getVersionNumber());
}
// Iterate over fields
final SchemaObjectType schemaObjectType = schemaModel.getSchemaObjectTypes().get(objType.getStorageId());
while ((name = this.next(reader)) != null) {
final QName fieldTagName = name;
// Determine the field
storageIdAttr = this.getAttr(reader, STORAGE_ID_ATTR, false);
final Field> field;
if (storageIdAttr != null) {
try {
field = objType.getFields().get(Integer.parseInt(storageIdAttr));
} catch (IllegalArgumentException e) {
throw new XMLStreamException("invalid field storage ID `" + storageIdAttr
+ "': " + e, reader.getLocation(), e);
}
if (field == null) {
throw new XMLStreamException("unknown field storage ID `" + storageIdAttr + "' for object type `"
+ objType.getName() + "' #" + objType.getStorageId() + " in schema version "
+ schema.getVersionNumber(), reader.getLocation());
}
} else {
if (!XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) {
throw new XMLStreamException("unexpected element <" + name.getPrefix() + ":"
+ name.getLocalPart() + ">; expected field name", reader.getLocation());
}
final SchemaField schemaField = nameIndex.getSchemaField(schemaObjectType, name.getLocalPart());
if (schemaField == null) {
throw new XMLStreamException("unexpected element <" + name.getLocalPart() + ">; unknown field `"
+ name.getLocalPart() + "' in object type `" + objType.getName() + "' #"
+ objType.getStorageId() + " in schema version " + schema.getVersionNumber(),
reader.getLocation());
}
field = objType.getFields().get(schemaField.getStorageId());
assert field != null : "field=" + schemaField + " fields=" + objType.getFields();
}
// Parse the field
if (field instanceof SimpleField)
snapshot.writeSimpleField(id, field.getStorageId(), this.readSimpleField(reader, (SimpleField>)field), false);
else if (field instanceof CounterField) {
final long value;
try {
value = Long.parseLong(reader.getElementText());
} catch (Exception e) {
throw new XMLStreamException("invalid counter value for field `" + field.getName() + "': " + e,
reader.getLocation(), e);
}
snapshot.writeCounterField(id, field.getStorageId(), value, false);
} else if (field instanceof CollectionField) {
final SimpleField> elementField = ((CollectionField, ?>)field).getElementField();
final Collection> collection;
if (field instanceof SetField)
collection = snapshot.readSetField(id, field.getStorageId(), false);
else if (field instanceof ListField)
collection = snapshot.readListField(id, field.getStorageId(), false);
else
throw new RuntimeException("internal error: " + field);
while ((name = this.next(reader)) != null) {
if (!ELEMENT_TAG.equals(name)) {
throw new XMLStreamException("invalid field element; expected <" + ELEMENT_TAG.getLocalPart()
+ "> but found opening <" + name.getLocalPart() + ">", reader.getLocation());
}
((Collection)collection).add(this.readSimpleField(reader, elementField));
}
} else if (field instanceof MapField) {
final SimpleField> keyField = ((MapField, ?>)field).getKeyField();
final SimpleField> valueField = ((MapField, ?>)field).getValueField();
final Map, ?> map = snapshot.readMapField(id, field.getStorageId(), false);
while ((name = this.next(reader)) != null) {
if (!ENTRY_TAG.equals(name)) {
throw new XMLStreamException("invalid map field entry; expected <" + ENTRY_TAG.getLocalPart()
+ "> but found opening <" + name.getLocalPart() + ">", reader.getLocation());
}
if (!KEY_TAG.equals(this.next(reader))) {
throw new XMLStreamException("invalid map entry key; expected <" + KEY_TAG.getLocalPart()
+ ">", reader.getLocation());
}
final Object key = this.readSimpleField(reader, keyField);
if (!VALUE_TAG.equals(this.next(reader))) {
throw new XMLStreamException("invalid map entry value; expected <" + VALUE_TAG.getLocalPart()
+ ">", reader.getLocation());
}
final Object value = this.readSimpleField(reader, valueField);
((Map)map).put(key, value);
if ((name = this.next(reader)) != null) {
throw new XMLStreamException("invalid map field entry; expected closing <" + ENTRY_TAG.getLocalPart()
+ "> but found opening <" + name.getLocalPart() + ">", reader.getLocation());
}
}
} else
throw new RuntimeException("internal error: " + field);
}
// Copy over object, replacing any previous
snapshot.copy(id, this.tx, false, false, this.unresolvedReferences, null);
// Removed the copied object from deleted assignments, as any forward reference to it is now resolved
this.unresolvedReferences.remove(id);
}
// Done
return count;
}
/**
* Export all objects from the {@link Transaction} associated with this instance to the given output.
*
* @param output XML output; will not be closed by this method
* @param nameFormat true for Name Format, false for Storage ID Format
* @param indent true to indent output, false for all on one line
* @return the number of objects written
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code output} is null
*/
public int write(OutputStream output, boolean nameFormat, boolean indent) throws XMLStreamException {
Preconditions.checkArgument(output != null, "null output");
XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(output, "UTF-8");
if (indent)
xmlWriter = new IndentXMLStreamWriter(xmlWriter);
xmlWriter.writeStartDocument("UTF-8", "1.0");
return this.write(xmlWriter, nameFormat);
}
/**
* Export all objects from the {@link Transaction} associated with this instance to the given writer.
*
* @param writer XML output; will not be closed by this method
* @param nameFormat true for Name Format, false for Storage ID Format
* @param indent true to indent output, false for all on one line
* @return the number of objects written
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code writer} is null
*/
public int write(Writer writer, boolean nameFormat, boolean indent) throws XMLStreamException {
Preconditions.checkArgument(writer != null, "null writer");
XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer);
if (indent)
xmlWriter = new IndentXMLStreamWriter(xmlWriter);
xmlWriter.writeStartDocument("1.0");
return this.write(xmlWriter, nameFormat);
}
private int write(XMLStreamWriter writer, boolean nameFormat) throws XMLStreamException {
// Gather all known object storage IDs
final TreeSet storageIds = new TreeSet<>();
for (Schema schema : this.tx.getSchemas().getVersions().values())
storageIds.addAll(schema.getSchemaModel().getSchemaObjectTypes().keySet());
// Get corresponding object sets
final ArrayList> sets = storageIds.stream()
.map(this.tx::getAll)
.collect(Collectors.toCollection(() -> new ArrayList<>(storageIds.size())));
// Output all objects
return this.write(writer, nameFormat, Iterables.concat(sets));
}
/**
* Export the specified objects from the {@link Transaction} associated with this instance to the given XML output.
*
*
* This method writes a start element as its first action, allowing the output to be embedded into a larger XML document.
* Callers not embedding the output may with to precede invocation of this method with a call to
* {@link XMLStreamWriter#writeStartDocument writer.writeStartDocument()}.
*
* @param writer XML writer; will not be closed by this method
* @param nameFormat true for Name Format, false for Storage ID Format
* @param objIds object IDs
* @return the number of objects written
* @throws XMLStreamException if an error occurs
* @throws IllegalArgumentException if {@code writer} or {@code objIds} is null
*/
public int write(XMLStreamWriter writer, boolean nameFormat, Iterable extends ObjId> objIds) throws XMLStreamException {
Preconditions.checkArgument(writer != null, "null writer");
Preconditions.checkArgument(objIds != null, "null objIds");
writer.setDefaultNamespace(OBJECTS_TAG.getNamespaceURI());
writer.writeStartElement(OBJECTS_TAG.getNamespaceURI(), OBJECTS_TAG.getLocalPart());
int count = 0;
for (ObjId id : objIds) {
// Get object info
final int typeStorageId = id.getStorageId();
final int version = this.tx.getSchemaVersion(id);
final Schema schema = this.tx.getSchemas().getVersion(version);
final ObjType objType = schema.getObjType(typeStorageId);
// Get format info
final QName objectElement = nameFormat ? new QName(objType.getName()) : OBJECT_TAG;
final int storageIdAttr = nameFormat ? -1 : typeStorageId;
// Output fields; if all are default, output empty tag
boolean tagOutput = false;
ArrayList> fieldList = new ArrayList<>(objType.getFields().values());
if (nameFormat)
Collections.sort(fieldList, Comparator.comparing(Field::getName));
for (Field> field : fieldList) {
// Determine if field equals its default value; if so, skip it
if (this.omitDefaultValueFields && field.hasDefaultValue(this.tx, id))
continue;
// Output opening tag if not output yet
if (!tagOutput) {
this.writeOpenTag(writer, false, objectElement, storageIdAttr, id, version);
tagOutput = true;
}
// Get tag name
final QName fieldTag = nameFormat ? new QName(field.getName()) : FIELD_TAG;
// Special case for simple fields, which use empty tags when null
if (field instanceof SimpleField) {
final Object value = this.tx.readSimpleField(id, field.getStorageId(), false);
if (value == null || this.fieldTruncationLength == 0)
writer.writeEmptyElement(fieldTag.getNamespaceURI(), fieldTag.getLocalPart());
else
writer.writeStartElement(fieldTag.getNamespaceURI(), fieldTag.getLocalPart());
if (!nameFormat)
this.writeAttribute(writer, STORAGE_ID_ATTR, field.getStorageId());
if (value != null && this.fieldTruncationLength != 0) {
this.writeSimpleFieldText(writer, (SimpleField>)field, value);
writer.writeEndElement();
} else if (value == null)
this.writeAttribute(writer, NULL_ATTR, "true");
continue;
}
// Output field opening tag
writer.writeStartElement(fieldTag.getNamespaceURI(), fieldTag.getLocalPart());
if (!nameFormat)
this.writeAttribute(writer, STORAGE_ID_ATTR, field.getStorageId());
// Output field value
if (field instanceof CounterField)
writer.writeCharacters("" + this.tx.readCounterField(id, field.getStorageId(), false));
else if (field instanceof CollectionField) {
final SimpleField> elementField = ((CollectionField, ?>)field).getElementField();
final Iterable> collection = field instanceof SetField ?
this.tx.readSetField(id, field.getStorageId(), false) :
this.tx.readListField(id, field.getStorageId(), false);
for (Object element : collection)
this.writeSimpleTag(writer, ELEMENT_TAG, elementField, element);
} else if (field instanceof MapField) {
final SimpleField> keyField = ((MapField, ?>)field).getKeyField();
final SimpleField> valueField = ((MapField, ?>)field).getValueField();
for (Map.Entry, ?> entry : this.tx.readMapField(id, field.getStorageId(), false).entrySet()) {
writer.writeStartElement(ENTRY_TAG.getNamespaceURI(), ENTRY_TAG.getLocalPart());
this.writeSimpleTag(writer, KEY_TAG, keyField, entry.getKey());
this.writeSimpleTag(writer, VALUE_TAG, valueField, entry.getValue());
writer.writeEndElement();
}
} else
throw new RuntimeException("internal error: " + field);
// Output field closing tag
writer.writeEndElement();
}
// Output empty opening tag if not output yet, otherwise closing tag
if (!tagOutput)
this.writeOpenTag(writer, true, objectElement, storageIdAttr, id, version);
else
writer.writeEndElement();
count++;
}
// Done
writer.writeEndElement();
writer.flush();
return count;
}
// Internal methods
private void writeSimpleTag(XMLStreamWriter writer, QName tag, SimpleField> field, Object value)
throws XMLStreamException {
if (value != null && this.fieldTruncationLength != 0) {
writer.writeStartElement(tag.getNamespaceURI(), tag.getLocalPart());
this.writeSimpleFieldText(writer, field, value);
writer.writeEndElement();
} else {
writer.writeEmptyElement(tag.getNamespaceURI(), tag.getLocalPart());
if (value == null)
this.writeAttribute(writer, NULL_ATTR, "true");
}
}
private void writeSimpleFieldText(XMLStreamWriter writer, SimpleField field, Object value) throws XMLStreamException {
final FieldType fieldType = field.getFieldType();
String text = fieldType.toString(fieldType.validate(value));
final int length = text.length();
if (this.fieldTruncationLength == -1 || length <= this.fieldTruncationLength) {
writer.writeCharacters(text);
return;
}
writer.writeCharacters(text.substring(0, this.fieldTruncationLength));
writer.writeCharacters("...[truncated]");
}
private T readSimpleField(XMLStreamReader reader, SimpleField field) throws XMLStreamException {
// Get field type
final FieldType fieldType = field.getFieldType();
// Check for null
final String nullAttr = this.getAttr(reader, NULL_ATTR, false);
boolean isNull = false;
if (nullAttr != null) {
switch (nullAttr) {
case "true":
case "false":
isNull = Boolean.valueOf(nullAttr);
break;
default:
throw new XMLStreamException("invalid value `" + nullAttr + "' for `" + NULL_ATTR.getLocalPart()
+ "' attribute: value must be \"true\" or \"false\"", reader.getLocation());
}
}
// Get text content
final String text;
try {
text = reader.getElementText();
} catch (Exception e) {
throw new XMLStreamException("invalid value for field `" + field.getName() + "': " + e, reader.getLocation(), e);
}
// If null, verify there is no text content
if (isNull) {
if (text.length() != 0)
throw new XMLStreamException("text content not allowed for values with null=\"true\"", reader.getLocation());
return null;
}
// Handle generated ID's for reference fields
if (field instanceof ReferenceField) {
final Matcher matcher = GENERATED_ID_PATTERN.matcher(text);
if (matcher.matches()) {
final int storageId = this.parseGeneratedType(reader, text, matcher.group(1));
return fieldType.validate(this.generatedIdCache.getGeneratedId(this.tx, storageId, matcher.group(2)));
}
}
// Parse field value
try {
return fieldType.fromString(text);
} catch (Exception e) {
throw new XMLStreamException("invalid value `" + text + "' for field `" + field.getName() + "': " + e,
reader.getLocation(), e);
}
}
// Parse a generated ID type found in a reference
private int parseGeneratedType(XMLStreamReader reader, String text, String attr) throws XMLStreamException {
int storageId = -1;
for (Schema schema : this.tx.getSchemas().getVersions().values()) {
final ObjType objType;
try {
objType = this.parseGeneratedType(reader, text, attr, schema);
} catch (XMLStreamException e) {
continue;
}
if (storageId != -1 && storageId != objType.getStorageId()) {
throw new XMLStreamException("invalid object type `" + attr + "' in generated object ID `"
+ text + "': two or more incompatible object types named `" + attr
+ "' exist (in different schema versions)", reader.getLocation());
}
storageId = objType.getStorageId();
}
if (storageId == -1) {
throw new XMLStreamException("invalid object type `" + attr + "' in generated object ID `"
+ text + "': no such object type found in any schema version", reader.getLocation());
}
return storageId;
}
// Parse a generated ID type found in an object definition
private ObjType parseGeneratedType(XMLStreamReader reader, String text, String attr, Schema schema)
throws XMLStreamException {
// Try object type name
final NameIndex nameIndex = this.nameIndexMap.get(schema.getVersionNumber());
final SchemaObjectType schemaObjectType = nameIndex.getSchemaObjectType(attr);
if (schemaObjectType != null)
return schema.getObjType(schemaObjectType.getStorageId());
// Try storage ID
final int storageId;
try {
storageId = Integer.parseInt(attr);
} catch (IllegalArgumentException e) {
throw new XMLStreamException("invalid object type `" + attr + "' in generated object ID `"
+ text + "': no such object type found in schema version " + schema.getVersionNumber(), reader.getLocation());
}
try {
return schema.getObjType(storageId);
} catch (UnknownTypeException e) {
throw new XMLStreamException("invalid storage ID " + storageId + " in generated object ID `"
+ text + "': no such object type found in schema version " + schema.getVersionNumber(), reader.getLocation());
}
}
private void writeOpenTag(XMLStreamWriter writer, boolean empty, QName element, int storageId, ObjId id, int version)
throws XMLStreamException {
if (empty)
writer.writeEmptyElement(element.getNamespaceURI(), element.getLocalPart());
else
writer.writeStartElement(element.getNamespaceURI(), element.getLocalPart());
if (storageId != -1)
this.writeAttribute(writer, STORAGE_ID_ATTR, storageId);
this.writeAttribute(writer, ID_ATTR, id);
this.writeAttribute(writer, VERSION_ATTR, version);
}
private void writeAttribute(XMLStreamWriter writer, QName attr, Object value) throws XMLStreamException {
final String ns = attr.getNamespaceURI();
if (ns == null || ns.length() == 0)
writer.writeAttribute(attr.getLocalPart(), "" + value);
else
writer.writeAttribute(attr.getNamespaceURI(), attr.getLocalPart(), "" + value);
}
/**
* Skip forward until either the next opening tag is reached, or the currently open tag is closed.
* This override ignores XML tags that are not in our namespace.
*/
@Override
protected QName next(XMLStreamReader reader) throws XMLStreamException {
while (true) {
final QName name = super.next(reader);
if (name == null || NS_URI.equals(name.getNamespaceURI()))
return name;
this.skip(reader);
}
}
}