org.simpleframework.xml.core.Composite Maven / Gradle / Ivy
Show all versions of simple-xml Show documentation
/*
* Composite.java July 2006
*
* Copyright (C) 2006, Niall Gallagher
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.simpleframework.xml.core;
import org.simpleframework.xml.Version;
import org.simpleframework.xml.strategy.Type;
import org.simpleframework.xml.stream.InputNode;
import org.simpleframework.xml.stream.NamespaceMap;
import org.simpleframework.xml.stream.NodeMap;
import org.simpleframework.xml.stream.OutputNode;
import org.simpleframework.xml.stream.Position;
/**
* The Composite
object is used to perform serialization
* of objects that contain XML annotations. Composite objects are objects
* that are not primitive and contain references to serializable fields.
* This Converter
will visit each field within the object
* and deserialize or serialize that field depending on the requested
* action. If a required field is not present when deserializing from
* an XML element this terminates and deserialization reports the error.
*
*
* <element name="test" class="some.package.Type">
* <text>string value</text>
* <integer>1234</integer>
* </element>
*
*
* To deserialize the above XML source this will attempt to match the
* attribute name with an Attribute
annotation from the
* XML schema class, which is specified as "some.package.Type". This
* type must also contain Element
annotations for the
* "text" and "integer" elements.
*
* Serialization requires that contacts marked as required must have
* values that are not null. This ensures that the serialized object
* can be deserialized at a later stage using the same class schema.
* If a required value is null the serialization terminates and an
* exception is thrown.
*
* @author Niall Gallagher
*/
class Composite implements Converter {
/**
* This factory creates instances of the deserialized object.
*/
private final ObjectFactory factory;
/**
* This is used to convert any primitive values that are needed.
*/
private final Primitive primitive;
/**
* This is used to store objects so that they can be read again.
*/
private final Criteria criteria;
/**
* This is the current revision of this composite converter.
*/
private final Revision revision;
/**
* This is the source object for the instance of serialization.
*/
private final Context context;
/**
* This is the type that this composite produces instances of.
*/
private final Type type;
/**
* Constructor for the Composite
object. This creates
* a converter object capable of serializing and deserializing root
* objects labeled with XML annotations. The XML schema class must
* be given to the instance in order to perform deserialization.
*
* @param context the source object used to perform serialization
* @param type this is the XML schema type to use for this
*/
public Composite(Context context, Type type) {
this(context, type, null);
}
/**
* Constructor for the Composite
object. This creates
* a converter object capable of serializing and deserializing root
* objects labeled with XML annotations. The XML schema class must
* be given to the instance in order to perform deserialization.
*
* @param context the source object used to perform serialization
* @param type this is the XML schema type to use for this
* @param override this is the override type declared for this
*/
public Composite(Context context, Type type, Class override) {
this.factory = new ObjectFactory(context, type, override);
this.primitive = new Primitive(context, type);
this.criteria = new Collector();
this.revision = new Revision();
this.context = context;
this.type = type;
}
/**
* This read
method performs deserialization of the XML
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
*
* @return this returns the fully deserialized object graph
*/
public Object read(InputNode node) throws Exception {
Instance value = factory.getInstance(node);
Class type = value.getType();
if(value.isReference()) {
return value.getInstance();
}
if(context.isPrimitive(type)) {
return readPrimitive(node, value);
}
return read(node, value, type);
}
/**
* This read
method performs deserialization of the XML
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
* @param source the object whose contacts are to be deserialized
*
* @return this returns the fully deserialized object graph
*/
public Object read(InputNode node, Object source) throws Exception {
Class type = source.getClass();
Schema schema = context.getSchema(type);
Caller caller = schema.getCaller();
read(node, source, schema);
criteria.commit(source);
caller.validate(source);
caller.commit(source);
return readResolve(node, source, caller);
}
/**
* This read
method performs deserialization of the XML
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
* @param value this is the instance for the object within the graph
* @param real this is the real type that is to be evaluated
*
* @return this returns the fully deserialized object graph
*/
private Object read(InputNode node, Instance value, Class real) throws Exception {
Schema schema = context.getSchema(real);
Caller caller = schema.getCaller();
Builder builder = read(schema, value);
Object source = builder.read(node);
caller.validate(source);
caller.commit(source);
value.setInstance(source);
return readResolve(node, source, caller);
}
/**
* This read
method performs deserialization of the XML
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
* @param schema this is the schema for the class to be deserialized
* @param value this is the value used for the deserialization
*
* @return this returns the fully deserialized object graph
*/
private Builder read(Schema schema, Instance value) throws Exception {
Instantiator creator = schema.getInstantiator();
if(creator.isDefault()) {
return new Builder(this, criteria, schema, value);
}
return new Injector(this, criteria, schema, value);
}
/**
* This readPrimitive
method will extract the text value
* from the node and replace any template variables before converting
* it to a primitive value. This uses a Primitive
object
* to convert the node text to the resulting string. This will also
* respect all references on the node so cycle can be followed.
*
* @param node this is the node to be converted to a primitive
* @param value this is the type for the object within the graph
*
* @return this returns the primitive that has been deserialized
*/
private Object readPrimitive(InputNode node, Instance value) throws Exception {
Class type = value.getType();
Object result = primitive.read(node, type);
if(type != null) {
value.setInstance(result);
}
return result;
}
/**
* The readResolve
method is used to determine if there
* is a resolution method which can be used to substitute the object
* deserialized. The resolve method is used when an object wishes
* to provide a substitute within the deserialized object graph.
* This acts as an equivalent to the Java Object Serialization
* readResolve
method for the object deserialization.
*
* @param node the XML element object provided as a replacement
* @param source the type of the object that is being deserialized
* @param caller this is used to invoke the callback methods
*
* @return this returns a replacement for the deserialized object
*/
private Object readResolve(InputNode node, Object source, Caller caller) throws Exception {
if(source != null) {
Position line = node.getPosition();
Object value = caller.resolve(source);
Class expect = type.getType();
Class real = value.getClass();
if(!expect.isAssignableFrom(real)) {
throw new ElementException("Type %s does not match %s at %s", real, expect, line);
}
return value;
}
return source;
}
/**
* This read
method performs deserialization of the XML
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
* @param source this type of the object that is to be deserialized
* @param schema this object visits the objects contacts
*/
private void read(InputNode node, Object source, Schema schema) throws Exception {
Section section = schema.getSection();
readVersion(node, source, schema);
readSection(node, source, section);
}
/**
* This readSection
method performs deserialization of a
* schema class type by traversing the contacts and instantiating them
* using details from the provided XML element. Because this will
* convert a non-primitive value it delegates to other converters to
* perform deserialization of lists and primitives.
*
* If any of the required contacts are not present within the provided
* XML element this will terminate deserialization and throw an
* exception. The annotation missing is reported in the exception.
*
* @param node the XML element contact values are deserialized from
* @param source this type of the object that is to be deserialized
* @param section this is the XML section that contains the structure
*/
private void readSection(InputNode node, Object source, Section section) throws Exception {
readText(node, source, section);
readAttributes(node, source, section);
readElements(node, source, section);
}
/**
* This method is used to read the version from the provided input
* node. Once the version has been read it is used to determine how
* to deserialize the object. If the version is not the initial
* version then it is read in a manner that ignores excessive XML
* elements and attributes. Also none of the annotated fields or
* methods are required if the version is not the initial version.
*
* @param node the XML element contact values are deserialized from
* @param source this object whose contacts are to be deserialized
* @param schema this object visits the objects contacts
*/
private void readVersion(InputNode node, Object source, Schema schema) throws Exception {
Label label = schema.getVersion();
Class expect = type.getType();
if(label != null) {
String name = label.getName();
NodeMap map = node.getAttributes();
InputNode value = map.remove(name);
if(value != null) {
readVersion(value, source, label);
} else {
Version version = context.getVersion(expect);
Double start = revision.getDefault();
Double expected = version.revision();
criteria.set(label, start);
revision.compare(expected, start);
}
}
}
/**
* This method is used to read the version from the provided input
* node. Once the version has been read it is used to determine how
* to deserialize the object. If the version is not the initial
* version then it is read in a manner that ignores excessive XML
* elements and attributes. Also none of the annotated fields or
* methods are required if the version is not the initial version.
*
* @param node the XML element contact values are deserialized from
* @param source the type of the object that is being deserialized
* @param label this is the label used to read the version attribute
*/
private void readVersion(InputNode node, Object source, Label label) throws Exception {
Object value = readInstance(node, source, label);
Class expect = type.getType();
if(value != null) {
Version version = context.getVersion(expect);
Double actual = version.revision();
if(!value.equals(revision)) {
revision.compare(actual, value);
}
}
}
/**
* This readAttributes
method reads the attributes from
* the provided XML element. This will iterate over all attributes
* within the element and convert those attributes as primitives to
* contact values within the source object.
*
* Once all attributes within the XML element have been evaluated
* the Schema
is checked to ensure that there are no
* required contacts annotated with the Attribute
that
* remain. If any required attribute remains an exception is thrown.
*
* @param node this is the XML element to be evaluated
* @param source the type of the object that is being deserialized
* @param section this is the XML section that contains the structure
*/
private void readAttributes(InputNode node, Object source, Section section) throws Exception {
NodeMap list = node.getAttributes();
LabelMap map = section.getAttributes();
for(String name : list) {
InputNode value = node.getAttribute(name);
if(value != null) {
readAttribute(value, source, section, map);
}
}
validate(node, map, source);
}
/**
* This readElements
method reads the elements from
* the provided XML element. This will iterate over all elements
* within the element and convert those elements to primitives or
* composite objects depending on the contact annotation.
*
* Once all elements within the XML element have been evaluated
* the Schema
is checked to ensure that there are no
* required contacts annotated with the Element
that
* remain. If any required element remains an exception is thrown.
*
* @param node this is the XML element to be evaluated
* @param source the type of the object that is being deserialized
* @param section the XML section that contains the structure
*/
private void readElements(InputNode node, Object source, Section section) throws Exception {
LabelMap map = section.getElements();
InputNode child = node.getNext();
while(child != null) {
String name = child.getName();
Section block = section.getSection(name);
if(block != null) {
readSection(child, source, block);
} else {
readElement(child, source, section, map);
}
child = node.getNext();
}
validate(node, map, source);
}
/**
* This readText
method is used to read the text value
* from the XML element node specified. This will check the class
* schema to determine if a Text
annotation was
* specified. If one was specified then the text within the XML
* element input node is used to populate the contact value.
*
* @param node this is the XML element to acquire the text from
* @param source the type of the object that is being deserialized
* @param section this is used to visit the element contacts
*/
private void readText(InputNode node, Object source, Section section) throws Exception {
Label label = section.getText();
if(label != null) {
readInstance(node, source, label);
}
}
/**
* This readAttribute
method is used for deserialization
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. When
* the delegate converter has completed the deserialized value is
* assigned to the contact.
*
* @param node this is the node that contains the contact value
* @param source the type of the object that is being deserialized
* @param section this is the section to read the attribute from
* @param map this is the map that contains the label objects
*/
private void readAttribute(InputNode node, Object source, Section section, LabelMap map) throws Exception {
String name = node.getName();
String path = section.getAttribute(name);
Label label = map.getLabel(path);
if(label == null) {
Position line = node.getPosition();
Class expect = context.getType(type, source);
if(map.isStrict(context) && revision.isEqual()) {
throw new AttributeException("Attribute '%s' does not have a match in %s at %s", path, expect, line);
}
} else {
readInstance(node, source, label);
}
}
/**
* This readElement
method is used for deserialization
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. When
* the delegate converter has completed the deserialized value is
* assigned to the contact.
*
* @param node this is the node that contains the contact value
* @param source the type of the object that is being deserialized
* @param section this is the section to read the element from
* @param map this is the map that contains the label objects
*/
private void readElement(InputNode node, Object source, Section section, LabelMap map) throws Exception {
String name = node.getName();
String path = section.getPath(name);
Label label = map.getLabel(path);
if(label == null) {
label = criteria.resolve(path);
}
if(label == null) {
Position line = node.getPosition();
Class expect = context.getType(type, source);
if(map.isStrict(context) && revision.isEqual()) {
throw new ElementException("Element '%s' does not have a match in %s at %s", path, expect, line);
} else {
node.skip();
}
} else {
readUnion(node, source, map, label);
}
}
/**
* The readUnion
method is determine the unions
* for a particular label and set the value of that union to
* the same value as the label. This helps the deserialization
* process by ensuring once a union is read it is not
* replaced. This is also required when reading inline lists.
*
* @param node this is the XML element to read the elements from
* @param source this is the instance to read the unions from
* @param map this is the label map associated with the label
* @param label this is the label used to define the XML element
*/
private void readUnion(InputNode node, Object source, LabelMap map, Label label) throws Exception {
Object value = readInstance(node, source, label);
String[] list = label.getPaths();
for(String key : list) {
map.getLabel(key);
}
if(label.isInline()) {
criteria.set(label, value);
}
}
/**
* This read
method is used to perform deserialization
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. When
* the delegate converter has completed the deserialized value is
* assigned to the contact.
*
* @param node this is the node that contains the contact value
* @param source the type of the object that is being deserialized
* @param label this is the label used to create the converter
*/
private Object readInstance(InputNode node, Object source, Label label) throws Exception {
Object object = readVariable(node, source, label);
if(object == null) {
Position line = node.getPosition();
Class expect = context.getType(type, source);
if(label.isRequired() && revision.isEqual()) {
throw new ValueRequiredException("Empty value for %s in %s at %s", label, expect, line);
}
} else {
if(object != label.getEmpty(context)) {
criteria.set(label, object);
}
}
return object;
}
/**
* This readObject
method is used to perform the
* deserialization of the XML in to any original value. If there
* is no original value then this will do a read and instantiate
* a new value to deserialize in to. Reading in to the original
* ensures that existing lists or maps can be read in to.
*
* @param node this is the node that contains the contact value
* @param source the source object to assign the contact value to
* @param label this is the label used to create the converter
*
* @return this returns the original value deserialized in to
*/
private Object readVariable(InputNode node, Object source, Label label) throws Exception {
Converter reader = label.getConverter(context);
if(label.isCollection()) {
Variable variable = criteria.get(label);
Contact contact = label.getContact();
if(variable != null) {
Object value = variable.getValue();
return reader.read(node, value);
}
if(source != null) {
Object value = contact.get(source);
if(value != null) {
return reader.read(node, value);
}
}
}
return reader.read(node);
}
/**
* This method checks to see if there are any Label
* objects remaining in the provided map that are required. This is
* used when deserialization is performed to ensure the the XML
* element deserialized contains sufficient details to satisfy the
* XML schema class annotations. If there is a required label that
* remains it is reported within the exception thrown.
*
* @param node this is the node that contains the contact value
* @param map this is the map to check for remaining labels
* @param source this is the object that has been deserialized
*/
private void validate(InputNode node, LabelMap map, Object source) throws Exception {
Class expect = context.getType(type, source);
Position line = node.getPosition();
for(Label label : map) {
if(label.isRequired() && revision.isEqual()) {
throw new ValueRequiredException("Unable to satisfy %s for %s at %s", label, expect, line);
}
Object value = label.getEmpty(context);
if(value != null) {
criteria.set(label, value);
}
}
}
/**
* This validate
method performs validation of the XML
* schema class type by traversing the contacts and validating them
* using details from the provided XML element. Because this will
* validate a non-primitive value it delegates to other converters
* to perform validation of lists, maps, and primitives.
*
* If any of the required contacts are not present within the given
* XML element this will terminate validation and throw an exception
* The annotation missing is reported in the exception.
*
* @param node the XML element contact values are validated from
*
* @return true if the XML element matches the XML schema class given
*/
public boolean validate(InputNode node) throws Exception {
Instance value = factory.getInstance(node);
if(!value.isReference()) {
Object result = value.setInstance(null);
Class type = value.getType();
return validate(node, type);
}
return true;
}
/**
* This validate
method performs validation of the XML
* schema class type by traversing the contacts and validating them
* using details from the provided XML element. Because this will
* validate a non-primitive value it delegates to other converters
* to perform validation of lists, maps, and primitives.
*
* If any of the required contacts are not present within the given
* XML element this will terminate validation and throw an exception
* The annotation missing is reported in the exception.
*
* @param node the XML element contact values are validated from
* @param type this is the type to validate against the input node
*/
private boolean validate(InputNode node, Class type) throws Exception {
Schema schema = context.getSchema(type);
Section section = schema.getSection();
validateText(node, schema);
validateSection(node, section);
return node.isElement();
}
/**
* This validateSection
method performs validation of a
* schema class type by traversing the contacts and validating them
* using details from the provided XML element. Because this will
* validate a non-primitive value it delegates to other converters
* to perform validation of lists, maps, and primitives.
*
* If any of the required contacts are not present within the given
* XML element this will terminate validation and throw an exception
* The annotation missing is reported in the exception.
*
* @param node the XML element contact values are validated from
* @param section this is the section that defines the XML structure
*/
private void validateSection(InputNode node, Section section) throws Exception {
validateAttributes(node, section);
validateElements(node, section);
}
/**
* This validateAttributes
method validates the attributes
* from the provided XML element. This will iterate over all attributes
* within the element and validate those attributes as primitives to
* contact values within the source object.
*
* Once all attributes within the XML element have been evaluated the
* Schema
is checked to ensure that there are no required
* contacts annotated with the Attribute
that remain. If
* any required attribute remains an exception is thrown.
*
* @param node this is the XML element to be validated
* @param section this is the section that defines the XML structure
*/
private void validateAttributes(InputNode node, Section section) throws Exception {
NodeMap list = node.getAttributes();
LabelMap map = section.getAttributes();
for(String name : list) {
InputNode value = node.getAttribute(name);
if(value != null) {
validateAttribute(value, section, map);
}
}
validate(node, map);
}
/**
* This validateElements
method validates the elements
* from the provided XML element. This will iterate over all elements
* within the element and validate those elements as primitives or
* composite objects depending on the contact annotation.
*
* Once all elements within the XML element have been evaluated
* the Schema
is checked to ensure that there are no
* required contacts annotated with the Element
that
* remain. If any required element remains an exception is thrown.
*
* @param node this is the XML element to be evaluated
* @param section this is the section that defines the XML structure
*/
private void validateElements(InputNode node, Section section) throws Exception {
LabelMap map = section.getElements();
InputNode next = node.getNext();
while(next != null) {
String name = next.getName();
Section child = section.getSection(name);
if(child != null) {
validateSection(next, child);
} else {
validateElement(next, section, map);
}
next = node.getNext();
}
validate(node, map);
}
/**
* This validateText
method validates the text value
* from the XML element node specified. This will check the class
* schema to determine if a Text
annotation was used.
* If one was specified then the text within the XML element input
* node is checked to determine if it is a valid entry.
*
* @param node this is the XML element to acquire the text from
* @param schema this is used to visit the element contacts
*/
private void validateText(InputNode node, Schema schema) throws Exception {
Label label = schema.getText();
if(label != null) {
validate(node, label);
}
}
/**
* This validateAttribute
method performs a validation
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. If this
* fails validation then an exception is thrown to report the issue.
*
* @param node this is the node that contains the contact value
* @param section this is the section to validate this attribute in
* @param map this is the map that contains the label objects
*/
private void validateAttribute(InputNode node, Section section, LabelMap map) throws Exception {
Position line = node.getPosition();
String name = node.getName();
String path = section.getAttribute(name);
Label label = map.getLabel(path);
if(label == null) {
Class expect = type.getType();
if(map.isStrict(context) && revision.isEqual()) {
throw new AttributeException("Attribute '%s' does not exist for %s at %s", path, expect, line);
}
} else {
validate(node, label);
}
}
/**
* This validateElement
method performs a validation
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. If this
* fails validation then an exception is thrown to report the issue.
*
* @param node this is the node that contains the contact value
* @param section this is the section to validate this element in
* @param map this is the map that contains the label objects
*/
private void validateElement(InputNode node, Section section, LabelMap map) throws Exception {
String name = node.getName();
String path = section.getPath(name);
Label label = map.getLabel(path);
if(label == null) {
label = criteria.resolve(path);
}
if(label == null) {
Position line = node.getPosition();
Class expect = type.getType();
if(map.isStrict(context) && revision.isEqual()) {
throw new ElementException("Element '%s' does not exist for %s at %s", path, expect, line);
} else {
node.skip();
}
} else {
validateUnion(node, map, label);
}
}
/**
* The validateUnion
method is determine the unions
* for a particular label and set the value of that union to
* the same value as the label. This helps the deserialization
* process by ensuring once a union is validated it is not
* replaced. This is also required when validating inline lists.
*
* @param node this is the XML element to read the elements from
* @param map this is the label map associated with the label
* @param label this is the label used to define the XML element
*/
private void validateUnion(InputNode node, LabelMap map, Label label) throws Exception {
String[] list = label.getPaths();
for(String key : list) {
map.getLabel(key);
}
if(label.isInline()) {
criteria.set(label, null);
}
validate(node, label);
}
/**
* This validate
method is used to perform validation
* of the provided node object using a delegate converter. This is
* typically another Composite
converter, or if the
* node is an attribute a Primitive
converter. If this
* fails validation then an exception is thrown to report the issue.
*
* @param node this is the node that contains the contact value
* @param label this is the label used to create the converter
*/
private void validate(InputNode node, Label label) throws Exception {
Converter reader = label.getConverter(context);
Position line = node.getPosition();
Class expect = type.getType();
boolean valid = reader.validate(node);
if(valid == false) {
throw new PersistenceException("Invalid value for %s in %s at %s", label, expect, line);
}
criteria.set(label, null);
}
/**
* This method checks to see if there are any Label
* objects remaining in the provided map that are required. This is
* used when validation is performed to ensure the the XML element
* validated contains sufficient details to satisfy the XML schema
* class annotations. If there is a required label that remains it
* is reported within the exception thrown.
*
* @param node this is the node that contains the composite data
* @param map this contains the converters to perform validation
*/
private void validate(InputNode node, LabelMap map) throws Exception {
Position line = node.getPosition();
for(Label label : map) {
Class expect = type.getType();
if(label.isRequired() && revision.isEqual()) {
throw new ValueRequiredException("Unable to satisfy %s for %s at %s", label, expect, line);
}
}
}
/**
* This write
method is used to perform serialization of
* the given source object. Serialization is performed by appending
* elements and attributes from the source object to the provided XML
* element object. How the objects contacts are serialized is
* determined by the XML schema class that the source object is an
* instance of. If a required contact is null an exception is thrown.
*
* @param source this is the source object to be serialized
* @param node the XML element the object is to be serialized to
*/
public void write(OutputNode node, Object source) throws Exception {
Class type = source.getClass();
Schema schema = context.getSchema(type);
Caller caller = schema.getCaller();
try {
if(schema.isPrimitive()) {
primitive.write(node, source);
} else {
caller.persist(source);
write(node, source, schema);
}
} finally {
caller.complete(source);
}
}
/**
* This write
method is used to perform serialization of
* the given source object. Serialization is performed by appending
* elements and attributes from the source object to the provided XML
* element object. How the objects contacts are serialized is
* determined by the XML schema class that the source object is an
* instance of. If a required contact is null an exception is thrown.
*
* @param source this is the source object to be serialized
* @param node the XML element the object is to be serialized to
* @param schema this is used to track the referenced contacts
*/
private void write(OutputNode node, Object source, Schema schema) throws Exception {
Section section = schema.getSection();
writeVersion(node, source, schema);
writeSection(node, source, section);
}
/**
* This writeSection
method is used to perform serialization
* of the given source object. Serialization is performed by appending
* elements and attributes from the source object to the provided XML
* element object. How the objects contacts are serialized is
* determined by the XML schema class that the source object is an
* instance of. If a required contact is null an exception is thrown.
*
* @param source this is the source object to be serialized
* @param node the XML element the object is to be serialized to
* @param section this is the section that defines the XML structure
*/
private void writeSection(OutputNode node, Object source, Section section) throws Exception {
NamespaceMap scope = node.getNamespaces();
String prefix = section.getPrefix();
if(prefix != null) {
String reference = scope.getReference(prefix);
if(reference == null) {
throw new ElementException("Namespace prefix '%s' in %s is not in scope", prefix, type);
} else {
node.setReference(reference);
}
}
writeAttributes(node, source, section);
writeElements(node, source, section);
writeText(node, source, section);
}
/**
* This method is used to write the version attribute. A version is
* written only if it is not the initial version or if it required.
* The version is used to determine how to deserialize the XML. If
* the version is different from the expected version then it allows
* the object to be deserialized in a manner that does not require
* any attributes or elements, and unmatched nodes are ignored.
*
* @param node this is the node to read the version attribute from
* @param source this is the source object that is to be written
* @param schema this is the schema that contains the version
*/
private void writeVersion(OutputNode node, Object source, Schema schema) throws Exception {
Version version = schema.getRevision();
Label label = schema.getVersion();
if(version != null) {
Double start = revision.getDefault();
Double value = version.revision();
if(revision.compare(value, start)) {
if(label.isRequired()) {
writeAttribute(node, value, label);
}
} else {
writeAttribute(node, value, label);
}
}
}
/**
* This write method is used to write all the attribute contacts from
* the provided source object to the XML element. This visits all
* the contacts marked with the Attribute
annotation in
* the source object. All annotated contacts are written as attributes
* to the XML element. This will throw an exception if a required
* contact within the source object is null.
*
* @param source this is the source object to be serialized
* @param node this is the XML element to write attributes to
* @param section this is the section that defines the XML structure
*/
private void writeAttributes(OutputNode node, Object source, Section section) throws Exception {
LabelMap attributes = section.getAttributes();
for(Label label : attributes) {
Contact contact = label.getContact();
Object value = contact.get(source);
Class expect = context.getType(type, source);
if(value == null) {
value = label.getEmpty(context);
}
if(value == null && label.isRequired()) {
throw new AttributeException("Value for %s is null in %s", label, expect);
}
writeAttribute(node, value, label);
}
}
/**
* This write method is used to write all the element contacts from
* the provided source object to the XML element. This visits all
* the contacts marked with the Element
annotation in
* the source object. All annotated contacts are written as children
* to the XML element. This will throw an exception if a required
* contact within the source object is null.
*
* @param source this is the source object to be serialized
* @param node this is the XML element to write elements to
* @param section this is the section that defines the XML structure
*/
private void writeElements(OutputNode node, Object source, Section section) throws Exception {
for(String name : section) {
Section child = section.getSection(name);
if(child != null) {
OutputNode next = node.getChild(name);
writeSection(next, source, child);
} else {
String path = section.getPath(name);
Label label = section.getElement(path);
Class expect = context.getType(type, source);
Object value = criteria.get(label);
if(value == null) {
if(label == null) {
throw new ElementException("Element '%s' not defined in %s", name, expect);
}
writeUnion(node, source, section, label);
}
}
}
}
/**
* The writeUnion
method is determine the unions
* for a particular label and set the value of that union to
* the same value as the label. This helps the serialization
* process by ensuring once a union is written it is not
* replaced. This is also required when writing inline lists.
*
* @param node this is the XML element to write elements to
* @param source this is the source object to be serialized
* @param section this is the section associated with the label
* @param label this is the label used to define the XML element
*/
private void writeUnion(OutputNode node, Object source, Section section, Label label) throws Exception {
Contact contact = label.getContact();
Object value = contact.get(source);
Class expect = context.getType(type, source);
if(value == null && label.isRequired()) {
throw new ElementException("Value for %s is null in %s", label, expect);
}
Object replace = writeReplace(value);
if(replace != null) {
writeElement(node, replace, label);
}
criteria.set(label, replace);
}
/**
* The writeReplace
method is used to replace an object
* before it is serialized. This is used so that an object can give
* a substitute to be written to the XML document in the event that
* the actual object is not suitable or desired for serialization.
* This acts as an equivalent to the Java Object Serialization
* writeReplace
method for the object serialization.
*
* @param source this is the source object that is to be replaced
*
* @return this returns the object to use as a replacement value
*/
private Object writeReplace(Object source) throws Exception {
if(source != null) {
Class type = source.getClass();
Caller caller = context.getCaller(type);
return caller.replace(source);
}
return source;
}
/**
* This write method is used to write the text contact from the
* provided source object to the XML element. This takes the text
* value from the source object and writes it to the single contact
* marked with the Text
annotation. If the value is
* null and the contact value is required an exception is thrown.
*
* @param source this is the source object to be serialized
* @param node this is the XML element to write text value to
* @param section this is used to track the referenced elements
*/
private void writeText(OutputNode node, Object source, Section section) throws Exception {
Label label = section.getText();
if(label != null) {
Contact contact = label.getContact();
Object value = contact.get(source);
Class expect = context.getType(type, source);
if(value == null) {
value = label.getEmpty(context);
}
if(value == null && label.isRequired()) {
throw new TextException("Value for %s is null in %s", label, expect);
}
writeText(node, value, label);
}
}
/**
* This write method is used to set the value of the provided object
* as an attribute to the XML element. This will acquire the string
* value of the object using toString
only if the
* object provided is not an enumerated type. If the object is an
* enumerated type then the Enum.name
method is used.
*
* @param value this is the value to be set as an attribute
* @param node this is the XML element to write the attribute to
* @param label the label that contains the contact details
*/
private void writeAttribute(OutputNode node, Object value, Label label) throws Exception {
if(value != null) {
Decorator decorator = label.getDecorator();
String name = label.getName();
String text = factory.getText(value);
OutputNode done = node.setAttribute(name, text);
decorator.decorate(done);
}
}
/**
* This write method is used to append the provided object as an
* element to the given XML element object. This will recursively
* write the contacts from the provided object as elements. This is
* done using the Converter
acquired from the contact
* label. If the type of the contact value is not of the same
* type as the XML schema class a "class" attribute is appended.
*
* If the element being written is inline, then this will not
* check to see if there is a "class" attribute specifying the
* name of the class. This is because inline elements do not have
* an outer class and thus could never have an override.
*
* @param value this is the value to be set as an element
* @param node this is the XML element to write the element to
* @param label the label that contains the contact details
*/
private void writeElement(OutputNode node, Object value, Label label) throws Exception {
if(value != null) {
Class real = value.getClass();
Label match = label.getLabel(real);
String name = match.getName();
Type type = label.getType(real);
OutputNode next = node.getChild(name);
if(!match.isInline()) {
writeNamespaces(next, type, match);
}
if(match.isInline() || !isOverridden(next, value, type)) {
Converter convert = match.getConverter(context);
boolean data = match.isData();
next.setData(data);
writeElement(next, value, convert);
}
}
}
/**
* This is used write the element specified using the specified
* converter. Writing the value using the specified converter will
* result in the node being populated with the elements, attributes,
* and text values to the provided node. If there is a problem
* writing the value using the converter an exception is thrown.
*
* @param node this is the node that the value is to be written to
* @param value this is the value that is to be written
* @param convert this is the converter used to perform writing
*/
private void writeElement(OutputNode node, Object value, Converter convert) throws Exception {
convert.write(node, value);
}
/**
* This is used to apply Decorator
objects to the
* provided node before it is written. Application of decorations
* before the node is written allows namespaces and comments to be
* applied to the node. Decorations such as this do not affect the
* overall structure of the XML that is written.
*
* @param node this is the node that decorations are applied to
* @param type this is the type to acquire the decoration for
* @param label this contains the primary decorator to be used
*/
private void writeNamespaces(OutputNode node, Type type, Label label) throws Exception {
Class expect = type.getType();
Decorator primary = context.getDecorator(expect);
Decorator decorator = label.getDecorator();
decorator.decorate(node, primary);
}
/**
* This write method is used to set the value of the provided object
* as the text for the XML element. This will acquire the string
* value of the object using toString
only if the
* object provided is not an enumerated type. If the object is an
* enumerated type then the Enum.name
method is used.
*
* @param value this is the value to set as the XML element text
* @param node this is the XML element to write the text value to
* @param label the label that contains the contact details
*/
private void writeText(OutputNode node, Object value, Label label) throws Exception {
if(value != null && !label.isTextList()) {
String text = factory.getText(value);
boolean data = label.isData();
node.setData(data);
node.setValue(text);
}
}
/**
* This is used to determine whether the specified value has been
* overridden by the strategy. If the item has been overridden
* then no more serialization is require for that value, this is
* effectively telling the serialization process to stop writing.
*
* @param node the node that a potential override is written to
* @param value this is the object instance to be serialized
* @param type this is the type of the object to be serialized
*
* @return returns true if the strategy overrides the object
*/
private boolean isOverridden(OutputNode node, Object value, Type type) throws Exception{
return factory.setOverride(type, value, node);
}
/**
* This takes the approach that the object is instantiated first and
* then the annotated fields and methods are deserialized from the
* XML elements and attributes. When all the details have be read
* they are set on the internal contacts of the object. This is used
* for places where we have a default no argument constructor.
*
* @author Niall Gallagher
*/
private static class Builder {
/**
* This is the composite converter that the builder will use.
*/
protected final Composite composite;
/**
* This is the criteria object used to collect the values.
*/
protected final Criteria criteria;
/**
* This is the schema object that contains the XML definition.
*/
protected final Schema schema;
/**
* This is the object instance created by the strategy object.
*/
protected final Instance value;
/**
* Constructor for the Builder
object. This creates
* a builder object capable of instantiating a object using a
* default no argument constructor. All fields are deserialized
* after the object has been instantiated.
*
* @param composite this is the composite object used by this
* @param criteria this collects the objects being deserialized
* @param schema this is the class schema used by this
* @param value this is the instance created by the strategy
*/
public Builder(Composite composite, Criteria criteria, Schema schema, Instance value) {
this.composite = composite;
this.criteria = criteria;
this.schema = schema;
this.value = value;
}
/**
* This read
method performs deserialization of the
* XML schema class type by traversing the contacts and using
* details from the provided XML element. Here an instance is
* created first then the contacts are traversed, this is done
* when a default constructor is the only option.
*
* @param node the XML element that will be deserialized by this
*
* @return this returns the fully deserialized object graph
*/
public Object read(InputNode node) throws Exception {
Object source = value.getInstance();
Section section = schema.getSection();
value.setInstance(source);
composite.readVersion(node, source, schema);
composite.readText(node, source, section);
composite.readAttributes(node, source, section);
composite.readElements(node, source, section);
criteria.commit(source);
return source;
}
}
/**
* This takes the approach that the objects are deserialized first
* then the instance is created using a constructor. In order for
* the best constructor to be found all the potential objects need
* to be deserialized first, then based on what has been deserialized
* a constructor is chosen and the parameters are injected in to it.
*
* @author Niall Gallagher
*/
private class Injector extends Builder {
/**
* Constructor for the Injector
object. This creates
* a builder object capable of instantiating a object using a
* constructor. It injects the constructor parameters in to the
* constructor by using the deserialized objects.
*
* @param composite this is the composite object used by this
* @param criteria this collects the objects being deserialized
* @param schema this is the class schema used by this
* @param value this is the instance created by the strategy
*/
private Injector(Composite composite, Criteria criteria, Schema schema, Instance value) {
super(composite, criteria, schema, value);
}
/**
* This read
method performs deserialization of the
* XML schema class type by traversing the contacts and using
* details from the provided XML element. Here an instance is
* instantiated only after everything has been deserialized so
* that the instances can be injected in to a constructor.
*
* @param node the XML element that will be deserialized by this
*
* @return this returns the fully deserialized object graph
*/
public Object read(InputNode node) throws Exception {
Section section = schema.getSection();
composite.readVersion(node, null, schema);
composite.readText(node, null, section);
composite.readAttributes(node, null, section);
composite.readElements(node, null, section);
return readInject(node);
}
/**
* This readInject
method performs deserialization
* of the XML schema class type by traversing the contacts and
* creating them using details from the provided XML element.
* Because this will convert a non-primitive value it delegates
* to other converters to perform deserialization of lists and
* primitives.
*
* This takes the approach of reading the elements and attributes
* before instantiating the object. Instantiation is performed
* using a declared constructor. The parameters are taken from
* the deserialized objects.
*
* @param node the XML element that will be deserialized by this
*
* @return this returns the fully deserialized object graph
*/
private Object readInject(InputNode node) throws Exception {
Instantiator creator = schema.getInstantiator();
Object source = creator.getInstance(criteria);
value.setInstance(source);
criteria.commit(source);
return source;
}
}
}