org.simpleframework.xml.core.InstantiatorBuilder Maven / Gradle / Ivy
Show all versions of simple-xml Show documentation
/*
* InstantiatorBuilder.java July 2011
*
* 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 static org.simpleframework.xml.core.Support.isAssignable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* The InstantiatorBuilder
object is used to resolve labels
* based on a provided parameter. Resolution of labels is done using
* a set of mappings based on the names of the labels. Because many
* labels might have the same name, this will only map a label if it
* has unique name. As well as resolving by name, this can resolve
* using the path of the parameter.
*
* @author Niall Gallagher
*
* @see org.simpleframework.xml.core.StructureBuilder
*/
class InstantiatorBuilder {
/**
* This is the list of creators representing an object constructor.
*/
private List options;
/**
* This is the single instance of the instantiator to be built.
*/
private Instantiator factory;
/**
* This is used to maintain the mappings for the attribute labels.
*/
private LabelMap attributes;
/**
* This is used to maintain the mappings for the element labels.
*/
private LabelMap elements;
/**
* This is used to maintain the mappings for the text labels.
*/
private LabelMap texts;
/**
* This is used for validation to compare annotations used.
*/
private Comparer comparer;
/**
* This is used to acquire the signatures created for the type.
*/
private Scanner scanner;
/**
* This is the detail the instantiator uses to create objects.
*/
private Detail detail;
/**
* Constructor for the InstantiatorBuilder
object. This
* is used to create an object that can resolve a label using a
* parameter. Resolution is performed using either the name of
* the parameter or the path of the parameter.
*
* @param scanner this is the scanner to acquire signatures from
* @param detail contains the details instantiators are built with
*/
public InstantiatorBuilder(Scanner scanner, Detail detail) {
this.options = new ArrayList();
this.comparer = new Comparer();
this.attributes = new LabelMap();
this.elements = new LabelMap();
this.texts = new LabelMap();
this.scanner = scanner;
this.detail = detail;
}
/**
* This is used to build the Instantiator
object that
* will be used to instantiate objects. Validation is performed on
* all of the parameters as well as the Creator
objects
* associated with the type. This validation ensures that the labels
* and constructor parameters match based on annotations.
*
* @return this returns the instance that has been built
*/
public Instantiator build() throws Exception {
if(factory == null) {
populate(detail);
build(detail);
validate(detail);
}
return factory;
}
/**
* This is used to build the Instantiator
object that
* will be used to instantiate objects. Validation is performed on
* all of the parameters as well as the Creator
objects
* associated with the type. This validation ensures that the labels
* and constructor parameters match based on annotations.
*
* @param detail contains the details instantiators are built with
*/
private Instantiator build(Detail detail) throws Exception {
if(factory == null) {
factory = create(detail);
}
return factory;
}
/**
* This is used to create the Instantiator
object that
* will be used to instantiate objects. Validation is performed on
* all of the parameters as well as the Creator
objects
* associated with the type. This validation ensures that the labels
* and constructor parameters match based on annotations.
*
* @param detail contains the details instantiators are built with
*
* @return this returns the instance that has been created
*/
private Instantiator create(Detail detail) throws Exception {
Signature primary = scanner.getSignature();
ParameterMap registry = scanner.getParameters();
Creator creator = null;
if(primary != null) {
creator = new SignatureCreator(primary);
}
return new ClassInstantiator(options, creator, registry, detail);
}
/**
* This is used to create a new Creator
object from
* the provided signature. Once created the instance is added to
* the list of available creators which can be used to instantiate
* an object instance.
*
* @param signature this is the signature that is to be used
*
* @return this returns the creator associated with this builder
*/
private Creator create(Signature signature) {
Creator creator = new SignatureCreator(signature);
if(signature != null) {
options.add(creator);
}
return creator;
}
/**
* This is used to create a Parameter
based on the
* currently registered labels. Creating a replacement in this way
* ensures that all parameters are keyed in the exact same way
* that the label is, making it easier and quicker to match them.
*
* @param original this is the original parameter to replace
*
* @return this returns the replacement parameter object
*/
private Parameter create(Parameter original) throws Exception {
Label label = resolve(original);
if(label != null) {
return new CacheParameter(original, label);
}
return null;
}
/**
* This used to populate replace the parameters extracted from the
* scanning process with ones matched with registered labels. By
* replacing parameters in this way the parameters can be better
* matched with the associated labels using the label keys.
*
* @param detail contains the details instantiators are built with
*/
private void populate(Detail detail) throws Exception {
List list = scanner.getSignatures();
for(Signature signature : list) {
populate(signature);
}
}
/**
* This used to populate replace the parameters extracted from the
* scanning process with ones matched with registered labels. By
* replacing parameters in this way the parameters can be better
* matched with the associated labels using the label keys.
*
* @param signature this is the signature used for a creator
*/
private void populate(Signature signature) throws Exception {
Signature substitute = new Signature(signature);
for(Parameter parameter : signature) {
Parameter replace = create(parameter);
if(replace != null) {
substitute.add(replace);
}
}
create(substitute);
}
/**
* This is used to ensure that for each parameter in the builder
* there is a matching method or field. This ensures that the
* class schema is fully readable and writable. If not method or
* field annotation exists for the parameter validation fails.
*
* @param detail contains the details instantiators are built with
*/
private void validate(Detail detail) throws Exception {
ParameterMap registry = scanner.getParameters();
List list = registry.getAll();
for(Parameter parameter : list) {
Label label = resolve(parameter);
String path = parameter.getPath();
if(label == null) {
throw new ConstructorException("Parameter '%s' does not have a match in %s", path, detail);
}
validateParameter(label, parameter);
}
validateConstructors();
}
/**
* This is used to validate the Parameter
object that
* exist in the constructors. Validation is performed against the
* annotated methods and fields to ensure that they match up.
*
* @param label this is the annotated method or field to validate
* @param parameter this is the parameter to validate with
*/
private void validateParameter(Label label, Parameter parameter) throws Exception {
Contact contact = label.getContact();
String name = parameter.getName();
Class expect = parameter.getType();
Class actual = contact.getType();
if(!isAssignable(expect, actual)) {
throw new ConstructorException("Type is not compatible with %s for '%s' in %s", label, name, parameter);
}
validateNames(label, parameter);
validateAnnotations(label, parameter);
}
/**
* This is used to validate the names associated with the parameters.
* Validation is performed by checking that the parameter name is
* present in the list of names the label is known by. This is used
* to ensure that the if the label is a union the parameter is one of
* the names declared within the union.
*
* @param label this is the label to validate the parameter against
* @param parameter this is the parameter that is to be validated
*/
private void validateNames(Label label, Parameter parameter) throws Exception {
String[] options = label.getNames();
String name = parameter.getName();
if(!contains(options, name)) {
String require = label.getName();
if(name != require) {
if(name == null || require == null) {
throw new ConstructorException("Annotation does not match %s for '%s' in %s", label, name, parameter);
}
if(!name.equals(require)) {
throw new ConstructorException("Annotation does not match %s for '%s' in %s", label, name, parameter);
}
}
}
}
/**
* This is used to validate the annotations associated with a field
* and a matching constructor parameter. Each constructor parameter
* paired with an annotated field or method must be the same
* annotation type and must also contain the same name.
*
* @param label this is the label associated with the parameter
* @param parameter this is the constructor parameter to use
*/
private void validateAnnotations(Label label, Parameter parameter) throws Exception {
Annotation field = label.getAnnotation();
Annotation argument = parameter.getAnnotation();
String name = parameter.getName();
if(!comparer.equals(field, argument)) {
Class expect = field.annotationType();
Class actual = argument.annotationType();
if(!expect.equals(actual)) {
throw new ConstructorException("Annotation %s does not match %s for '%s' in %s", actual, expect, name, parameter);
}
}
}
/**
* This is used to ensure that final methods and fields have a
* constructor parameter that allows the value to be injected in
* to. Validating the constructor in this manner ensures that the
* class schema remains fully serializable and deserializable.
*/
private void validateConstructors() throws Exception {
List list = factory.getCreators();
if(factory.isDefault()) {
validateConstructors(elements);
validateConstructors(attributes);
}
if(!list.isEmpty()) {
validateConstructors(elements, list);
validateConstructors(attributes, list);
}
}
/**
* This is used when there are only default constructors. It will
* check to see if any of the annotated fields or methods is read
* only. If a read only method or field is found then this will
* throw an exception to indicate that it is not valid.
*
* @param map this is the map of values that is to be validated
*/
private void validateConstructors(LabelMap map) throws Exception {
for(Label label : map) {
if(label != null) {
Contact contact = label.getContact();
if(contact.isReadOnly()) {
throw new ConstructorException("Default constructor can not accept read only %s in %s", label, detail);
}
}
}
}
/**
* This is used to ensure that final methods and fields have a
* constructor parameter that allows the value to be injected in
* to. Validating the constructor in this manner ensures that the
* class schema remains fully serializable and deserializable.
*
* @param map this is the map that contains the labels to validate
* @param list this is the list of builders to validate
*/
private void validateConstructors(LabelMap map, List list) throws Exception {
for(Label label : map) {
if(label != null) {
validateConstructor(label, list);
}
}
if(list.isEmpty()) {
throw new ConstructorException("No constructor accepts all read only values in %s", detail);
}
}
/**
* This is used to ensure that final methods and fields have a
* constructor parameter that allows the value to be injected in
* to. Validating the constructor in this manner ensures that the
* class schema remains fully serializable and deserializable.
*
* @param label this is the variable to check in constructors
* @param list this is the list of builders to validate
*/
private void validateConstructor(Label label, List list) throws Exception {
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
Creator instantiator = iterator.next();
Signature signature = instantiator.getSignature();
Contact contact = label.getContact();
Object key = label.getKey();
if(contact.isReadOnly()) {
Parameter value = signature.get(key);
if(value == null) {
iterator.remove();
}
}
}
}
/**
* This register
method is used to register a label
* based on its name and path. Registration like this is done
* to ensure that the label can be resolved based on a parameter
* name or path.
*
* @param label this is the label that is to be registered
*/
public void register(Label label) throws Exception {
if(label.isAttribute()) {
register(label, attributes);
} else if(label.isText()){
register(label, texts);
} else {
register(label, elements);
}
}
/**
* This register
method is used to register a label
* based on its name and path. Registration like this is done
* to ensure that the label can be resolved based on a parameter
* name or path.
*
* Registration here ensures a parameter can be resolved on both
* name and path. However, because we want these mappings to be
* unique we do not allow multiple names to be mapped to the same
* label. For example, say we have 'x/@a' and 'y/@a', these both
* have the same name 'a' even though the point/put( to different
* things. Here we would not allow a mapping from 'a' and keep
* only mappings based on the full path. This means that any
* parameters specified must declare the full path also.
*
* @param label this is the label that is to be registered
* @param map this is the map that the label is registered with
*/
private void register(Label label, LabelMap map) throws Exception {
String name = label.getName();
String path = label.getPath();
if(map.containsKey(name)) {
Label current = map.get(name);
String key = current.getPath();
if(!key.equals(name)) {
map.remove(name);
}
} else {
map.put(name, label);
}
map.put(path, label);
}
/**
* This resolve
method is used to find a label based
* on the name and path of the provided parameter. If it can not
* be found then this will return null.
*
* @param parameter this is the parameter used for resolution
*
* @return the label that has been resolved, or null
*/
private Label resolve(Parameter parameter) throws Exception {
if(parameter.isAttribute()) {
return resolve(parameter, attributes);
} else if(parameter.isText()){
return resolve(parameter, texts);
}
return resolve(parameter, elements);
}
/**
* This resolve
method is used to find a label based
* on the name and path of the provided parameter. If it can not
* be found then this will return null.
*
* @param parameter this is the parameter used for resolution
* @param map this is the map that is used for resolution
*
* @return the label that has been resolved, or null
*/
private Label resolve(Parameter parameter, LabelMap map) throws Exception {
String name = parameter.getName();
String path = parameter.getPath();
Label label = map.get(path);
if(label == null) {
return map.get(name);
}
return label;
}
/**
* This is used to determine if a value exists within an array.
* Searching the array like this is required when no collections
* are available to use on the list of attributes.
*
* @param list this is the list to begin searching for a value
* @param value this is the value to be searched for
*
* @return true if the list contains the specified value
*/
private boolean contains(String[] list, String value) throws Exception {
for(String entry : list) {
if(entry == value) {
return true;
}
if(entry.equals(value)) {
return true;
}
}
return false;
}
}