/*
* Rapid Beans Framework: TypePropertyCollection.java
*
* Copyright (C) 2009 Martin Bluemel
*
* Creation Date: 11/27/2005
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
* You should have received a copies of the GNU Lesser General Public License and the
* GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.rapidbeans.core.type;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.TreeSet;
import org.rapidbeans.core.basic.AssociationendFlavor;
import org.rapidbeans.core.basic.SortingType;
import org.rapidbeans.core.common.ReadonlyListCollection;
import org.rapidbeans.core.exception.RapidBeansRuntimeException;
import org.rapidbeans.core.util.ClassHelper;
import org.rapidbeans.core.util.StringHelper;
import org.rapidbeans.core.util.XmlNode;
/**
* The type class for association end properties.
*
* @see org.rapidbeans.core.basic.PropertyAssociationEnd
*
* @author Martin Bluemel
*/
public class TypePropertyCollection extends TypeProperty {
@Override
public Class<?> getValuetype() {
return ReadonlyListCollection.class;
}
/**
* the bean target type for the collection property.
*/
private TypeRapidBean targetType = null;
/**
* name of the property of the target type that defines the inverse link for
* this association. If you define that adding a link to an instance of
* targetType will also add an inverse link from the target instance to the
* proerties parent bean. Removing the link will also remove the inverse
* link.
*/
private String inverse = null;
/**
* the composition flag.
*/
private boolean composition;
/**
* the minimal multiplicity.
*/
private int minmult = 0;
public static final int INFINITE = -1;
/**
* the maximal multiplicity. Integer.MAX_VALUE means unlimited.
*/
private int maxmult = INFINITE;
/**
* the flavor (set or bag).
*/
private AssociationendFlavor flavor = null;
/**
* the sorting type.
*/
private SortingType sorting = null;
/**
* defines the property types for sorting in ascending order.
*/
private TypeProperty[] sortingProptypes = null;
/**
* the singular name for collection properties with maxmult > 1.
*/
private String singular = null;
private static char defaultCharSeparator = ' ';
public static char getDefaultCharSeparator() {
return defaultCharSeparator;
}
/**
* @param defaultCharSeparator
* the defaultCharSeparator to set
*/
public static void setDefaultCharSeparator(char defaultCharSeparator) {
TypePropertyCollection.defaultCharSeparator = defaultCharSeparator;
}
private char charSeparator = defaultCharSeparator;
/**
* @return the charSeparator
*/
public char getCharSeparator() {
return charSeparator;
}
/**
* @param charSeparator
* the charSeparator to set
*/
protected void setCharSeparator(char charSeparator) {
this.charSeparator = charSeparator;
}
private static char defaultCharEscape = '\\';
/**
* @param defaultCharEscape
* the defaultCharEscape to set
*/
public static void setDefaultCharEscape(char defaultCharEscape) {
TypePropertyCollection.defaultCharEscape = defaultCharEscape;
}
private char charEscape = defaultCharEscape;
/**
* @return the defaultCharEscape
*/
public static char getDefaultCharEscape() {
return defaultCharEscape;
}
/**
* @return the charEscape
*/
public char getCharEscape() {
return charEscape;
}
/**
* @param charEscape
* the charEscape to set
*/
protected void setCharEscape(char charEscape) {
this.charEscape = charEscape;
}
/**
* RapidBean default Java collection implementation. It has a good
* performance also for big collections (LinkeHashSet doesn't). Elements are
* sorted as created and as serialized / persisted (XML not SQL). Please
* note that ArrayList does not prevent linking the same instance twice
* which complies to a sequence (ordered bag).
*/
public static final Class<?> DEFAULT_COLLECTION_CLASS_DEFAULT = ArrayList.class;
/**
* defines the default collection class for all collections (= association
* roles or ends).
*/
private static Class<?> defaultCollectionClass = DEFAULT_COLLECTION_CLASS_DEFAULT;
/**
* The collection class used for implementing this association role or end.
* The default is a LinkedHashSet which - prevents having the same object
* associated twice - preserves the order
*/
private Class<?> collectionClass = null;
/**
* the default class to take for collection properties.
*/
private Constructor<?> collectionClassConstructor = null;
/**
* just an empty class array.
*/
private static final Class<?>[] COLLECTION_CONSTR_PARAMTYPES = new Class[0];
/**
* Constructor for TypePropertyCollection.
*
* @param xmlNode
* the XML DOM node with the property type description.
* @param parentBeanType
* the parent bean type
*/
public TypePropertyCollection(final XmlNode[] xmlNodes, final TypeRapidBean parentBeanType) {
this(xmlNodes, parentBeanType, "Collection", null, null);
}
/**
* Constructor for TypePropertyCollection.
*
* @param xmlNode
* the XML DOM node with the property type description.
* @param parentBeanType
* the parent bean type
*/
public TypePropertyCollection(final XmlNode[] xmlNodes, final TypeRapidBean parentBeanType,
final String specificTypeNamePart) {
this(xmlNodes, parentBeanType, specificTypeNamePart, null, null);
}
/**
* Constructor for TypePropertyCollection.
*
* @param xmlNode
* the XML DOM node with the property type description.
* @param parentBeanType
* the parent bean type
* @param specificTypeNamePart
* "Collection"
* @param separator
* sep
* @param escape
*/
public TypePropertyCollection(final XmlNode[] xmlNodes, final TypeRapidBean parentBeanType,
final String specificTypeNamePart, final String separator, final String escape) {
super(specificTypeNamePart, xmlNodes, parentBeanType);
super.parseDefaultValue(xmlNodes[0]);
String targetTypeName = xmlNodes[0].getAttributeValue("@targettype");
if (targetTypeName == null || targetTypeName.equals("")) {
throwModelValidationException("no targettype specified for Collection Property " + this.getPropName());
}
if (!targetTypeName.contains(".") && this.getParentBeanType() != null) {
final String packageName = this.getParentBeanType().getPackageName();
if (packageName != null) {
targetTypeName = packageName + "." + targetTypeName;
}
}
this.targetType = (TypeRapidBean) RapidBeansTypeLoader.getInstance().loadType(TypeRapidBean.class,
targetTypeName);
this.inverse = xmlNodes[0].getAttributeValue("@inverse");
this.composition = Boolean.parseBoolean(xmlNodes[0].getAttributeValue("@composition", "false"));
String sMult = xmlNodes[0].getAttributeValue("@minmult");
if (sMult != null) {
this.minmult = Integer.parseInt(sMult);
}
if (this.minmult < 0) {
throwModelValidationException("Invalid minimal multiplicity < 0 specified");
}
sMult = xmlNodes[0].getAttributeValue("@maxmult");
if (sMult != null) {
if (sMult.equals("*")) {
this.maxmult = INFINITE;
} else {
this.maxmult = Integer.parseInt(sMult);
}
if (this.maxmult < 1 && this.maxmult != INFINITE) {
throwModelValidationException("Invalid maximal multiplicity < 1 specified");
}
}
final String sFlavor = xmlNodes[0].getAttributeValue("@flavor", "set");
if (sFlavor != null) {
this.flavor = AssociationendFlavor.valueOf(sFlavor);
}
final String sSorting = xmlNodes[0].getAttributeValue("@sorting");
if (sSorting != null) {
final int iColon = sSorting.indexOf(':');
if (iColon != -1) {
this.sorting = SortingType.valueOf(sSorting.substring(0, iColon));
final ArrayList<TypeProperty> sortingPtypes = new ArrayList<TypeProperty>();
for (String propname : StringHelper.split(sSorting.substring(iColon + 1), ",")) {
propname = propname.trim();
final TypeProperty proptype = this.targetType.getPropertyType(propname);
if (proptype == null) {
throwModelValidationException("Invalid ordering property \"" + propname
+ "\" specified for target type " + "\"" + this.targetType.getName() + "\"\n");
}
sortingPtypes.add(proptype);
}
if (sortingPtypes.size() == 0) {
throwModelValidationException("Specify at least one ordering property.");
}
this.sortingProptypes = new TypeProperty[sortingPtypes.size()];
int i = 0;
for (TypeProperty proptype : sortingPtypes) {
this.sortingProptypes[i++] = proptype;
}
} else {
this.sorting = SortingType.valueOf(sSorting);
}
}
if (this.maxmult == 1 && this.sorting != null) {
throwModelValidationException("Defining sorting for maximal" + " multiplicity 1 is useless");
}
// the default collection class if
// - flavor is undefined and
// - ordering is undefined
this.collectionClass = defaultCollectionClass;
if (this.flavor == null) {
this.throwModelValidationException("unexpected null flavor");
}
switch (this.flavor) {
case set:
if (this.sorting == null) {
// default for an unsorted set
this.collectionClass = LinkedHashSet.class;
} else {
// default for a sorted set
this.collectionClass = TreeSet.class;
}
break;
case bag:
this.flavor = AssociationendFlavor.bag;
if (this.sorting == null) {
// default for an unsorted bag
this.collectionClass = ArrayList.class;
} else {
// default for an sorted bag
throwModelValidationException("no idea how to implement a sorted bag");
}
}
final String collectionClassname = xmlNodes[0].getAttributeValue("@collectionclass");
if (collectionClassname != null && !collectionClassname.equals("")) {
try {
this.collectionClass = Class.forName(collectionClassname);
if (!ClassHelper.classOf(Collection.class, this.collectionClass)) {
throwModelValidationException("Invalid collectionclass: Class \"" + collectionClassname
+ " is not a Collection.");
}
this.collectionClassConstructor = this.collectionClass.getConstructor(COLLECTION_CONSTR_PARAMTYPES);
} catch (ClassNotFoundException e) {
throwModelValidationException("Collection class \"" + collectionClassname + " not found.");
} catch (NoSuchMethodException e) {
throwModelValidationException("invalid collection class \"" + collectionClassname
+ "\" configured for collection" + " properties.\n" + "No empty default constructor found");
}
}
final String sing = xmlNodes[0].getAttributeValue("@singular");
if (sing != null && !sing.equals("")) {
this.singular = sing;
}
if (separator != null) {
if (separator.length() != 1) {
throw new IllegalArgumentException("Illegal separator XML binding option \"" + separator + "\"");
}
this.charSeparator = separator.charAt(0);
}
if (escape != null) {
if (escape.length() != 1) {
throw new IllegalArgumentException("Illegal escape XML binding option \"" + escape + "\"");
}
this.charEscape = escape.charAt(0);
}
}
/**
* @return the property type collection
*/
public PropertyType getProptype() {
return PropertyType.collection;
}
/**
* @return the singular
*/
public String getSingular() {
return singular;
}
/**
* @return the bean target type for the collection property
*/
public TypeRapidBean getTargetType() {
return this.targetType;
}
/**
* @return the name of the property of the target type that defines the
* inverse link for this association.
*/
public String getInverse() {
return this.inverse;
}
/**
* @return if this is a composition
*/
public boolean isComposition() {
return this.composition;
}
/**
* @return how this collection is sorted
*/
public SortingType getSorting() {
return this.sorting;
}
/**
* @return Returns the minmult.
*/
public int getMinmult() {
return this.minmult;
}
/**
* @return Returns the maxmult.
*/
public int getMaxmult() {
return this.maxmult;
}
/**
* !!! For test purposes only. Not for production use. Set the default
* collection class for all for all collections (= association roles or
* ends).
*
* @param defColClass
* the default collection class
*/
public static void setDefaultCollectionClass(final Class<?> defColClass) {
defaultCollectionClass = defColClass;
}
/**
* @return the default collection class.
*/
public static Class<?> getDefaultCollectionClass() {
return defaultCollectionClass;
}
/**
* @return the collection class used for implementing this association role
*/
public Class<?> getCollectionClass() {
return this.collectionClass;
}
/**
* !!! For test purposes only. Not for production use.
*
* @param clazz
* the new default collection class
*/
public void setCollectionClass(final Class<?> clazz) {
this.collectionClass = clazz;
}
/**
* @return the collectionClassConstructor
*/
public Constructor<?> getCollectionClassConstructor() {
return collectionClassConstructor;
}
/**
* @return the sortingProptypes
*/
public TypeProperty[] getSortingProptypes() {
return sortingProptypes;
}
/**
* Evaluate XML binding description (XML)
*
* @param beantype
* the parent bean type
* @param propNode
* the XML property description node
*/
protected void evalXmlBinding(final TypeRapidBean beantype, final XmlNode propNode) {
final String separator = propNode.getAttributeValue("@separator");
if (separator != null) {
if (separator.length() != 1) {
throw new RapidBeansRuntimeException("Invalid XML Binding for type \"" + beantype.getName() + "\":\n"
+ "Property \"" + getPropName() + ", illegal separator character \"" + separator + "\"");
}
setCharSeparator(separator.charAt(0));
}
final String escape = propNode.getAttributeValue("@escape");
if (escape != null) {
if (escape.length() != 1) {
throw new RapidBeansRuntimeException("Invalid XML Binding for type \"" + beantype.getName() + "\":\n"
+ "Property \"" + getPropName() + ", illegal escape character \"" + escape + "\"");
}
setCharEscape(escape.charAt(0));
}
}
}