/* * Copyright (c) 2007-2008, Dennis M. Sosnoski All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following * disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of * JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jibx.binding.generator; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.jibx.binding.model.IClass; import org.jibx.binding.model.IClassItem; import org.jibx.binding.model.IClassLocator; import org.jibx.binding.util.StringArray; import org.jibx.custom.CustomUtils; import org.jibx.runtime.EnumSet; import org.jibx.runtime.IUnmarshallingContext; import org.jibx.runtime.QName; import org.jibx.util.InsertionOrderedMap; /** * Class customization information. This supports direct class customizations. (such as the corresponding element name, * when building a concrete mapping) and also acts as a container for individual fields and/or properties. * * @author Dennis M. Sosnoski */ public class ClassCustom extends NestingBase implements IApply { /** Enumeration of allowed attribute names */ public static final StringArray s_allowedAttributes = new StringArray(new String[] { "create-type", "element-name", "enum-value-method", "excludes", "factory", "form", "includes", "name", "optionals", "requireds", "type-name", "use-super" }, NestingBase.s_allowedAttributes); /** Element name in XML customization file. */ public static final String ELEMENT_NAME = "class"; // value set information public static final int DEFAULT_REPRESENTATION = 0; public static final int CONCRETE_MAPPING_REPRESENTATION = 1; public static final int ABSTRACT_MAPPING_REPRESENTATION = 2; public static final EnumSet s_representationEnum = new EnumSet(DEFAULT_REPRESENTATION, new String[] { "default", "concrete-mapping", "abstract-mapping" }); // values specific to class level private String m_name; private String m_elementName; private String m_typeName; private String m_createType; private String m_factoryMethod; private String m_enumValueMethod; private int m_form; private String[] m_includes; private String[] m_excludes; private boolean m_useSuper; private String[] m_requireds; private String[] m_optionals; // list of contained items private final ArrayList m_children; // values filled in by apply() method private boolean m_isApplied; private QName m_typeQName; private QName m_elementQName; private IClass m_classInformation; private InsertionOrderedMap m_memberMap; /** * Constructor. * * @param parent * @param name class simple name (without package) */ /* package */ClassCustom(NestingBase parent, String name) { super(parent); m_name = name; m_children = new ArrayList(); m_useSuper = true; } /** * Make sure all attributes are defined. * * @param uctx unmarshalling context */ private void preSet(IUnmarshallingContext uctx) { validateAttributes(uctx, s_allowedAttributes); } /** * Get fully-qualified class name. * * @return class name */ public String getName() { PackageCustom parent = (PackageCustom)getParent(); String pack = parent.getName(); if (pack.length() > 0) { return pack + '.' + m_name; } else { return m_name; } } /** * Get simple class name. * * @return class name */ public String getSimpleName() { return m_name; } /** * Get the element name to be used for this class in a concrete mapping. * * @return element name */ public String getElementName() { return m_elementName; } /** * Get the qualified element name to be used for this class in a concrete mapping. * * @return element name */ public QName getElementQName() { return m_elementQName; } /** * Get the type name to be used for this class in an abstract mapping. * * @return type name */ public String getTypeName() { return m_typeName; } /** * Get the type name to be used when creating an instance of this class. * * @return type name */ public String getCreateType() { return m_createType; } /** * Set the type name to be used when creating an instance of this class. * * @param type */ public void setCreateType(String type) { m_createType = type; } /** * Get the method used to retrieve the text value for an enum class. * * @return method name */ public String getEnumValueMethod() { return m_enumValueMethod; } /** * Get the factory method to be used when creating an instance of this class. * * @return method name */ public String getFactoryMethod() { return m_factoryMethod; } /** * Get the qualified type name to be used for this class in an abstract mapping. * * @return type qname */ public QName getTypeQName() { return m_typeQName; } /** * Get the representation code. * * @return value from {@link #s_representationEnum} enumeration */ public int getForm() { return m_form; } /** * Get list of names to be excluded from class representation. * * @return excludes (null if none) */ public String[] getExcludes() { return m_excludes; } /** * Get list of names to be included in class representation. * * @return includes (null if none) */ public String[] getIncludes() { return m_includes; } /** * Check for superclass to be included in binding. * * @return true if superclass included, false if not */ public boolean isUseSuper() { return m_useSuper; } /** * Check if this is a directly instantiable class (not an interface, and not abstract) * * @return true if instantiable, false if not */ public boolean isConcrete() { return !(m_classInformation.isAbstract() || m_classInformation.isInterface()); } /** * Get list of children. * * @return list */ public List getChildren() { return m_children; } /** * Add child. * * @param child */ protected void addChild(CustomBase child) { if (child.getParent() == this) { m_children.add(child); } else { throw new IllegalStateException("Internal error: child not linked"); } } /** * Form set text method. This is intended for use during unmarshalling. TODO: add validation * * @param text * @param ictx */ private void setFormText(String text, IUnmarshallingContext ictx) { m_form = s_representationEnum.getValue(text); } /** * Form get text method. This is intended for use during marshalling. * * @return text */ private String getFormText() { return s_representationEnum.getName(m_form); } /** * Build map from member names to read access methods. This assumes that each no-argument method which returns a * value and has a name beginning with "get" or "is" is a property read access method. It maps the corresponding * property name to the method, and returns the map. * * @param methods * @param inclset set of member names to be included (null if not specified) * @param exclset set of member names to be excluded (null if not specified, ignored if inclset is * non-null) * @return map */ private Map mapPropertyReadMethods(IClassItem[] methods, Set inclset, Set exclset) { // check all methods for property read access matches InsertionOrderedMap getmap = new InsertionOrderedMap(); for (int i = 0; i < methods.length; i++) { IClassItem item = methods[i]; String name = item.getName(); if (item.getArgumentCount() == 0 && ((name.startsWith("get") && !item.getTypeName().equals("void")) || (name.startsWith("is") && item .getTypeName().equals("boolean")))) { // have what appears to be a getter, check if it should be used String memb = MemberCustom.memberNameFromGetMethod(name); boolean use = true; if (inclset != null) { use = inclset.contains(memb.toLowerCase()); } else if (exclset != null) { use = !exclset.contains(memb.toLowerCase()); } if (use) { getmap.put(memb, item); } } } return getmap; } /** * Build map from member names to write access methods. This assumes that each single-argument method which returns * void and has a name beginning with "set" is a property write access method. It maps the corresponding property * name to the method, and returns the map. * * @param methods * @param inclset set of member names to be included (null if not specified) * @param exclset set of member names to be excluded (null if not specified, ignored if inclset is * non-null) * @return map */ private Map mapPropertyWriteMethods(IClassItem[] methods, Set inclset, Set exclset) { // check all methods for property write access matches InsertionOrderedMap setmap = new InsertionOrderedMap(); for (int i = 0; i < methods.length; i++) { IClassItem item = methods[i]; String name = item.getName(); if (item.getArgumentCount() == 1 && name.startsWith("set") && item.getTypeName().equals("void")) { // have what appears to be a setter, check if it should be used String memb = MemberCustom.memberNameFromSetMethod(name); boolean use = true; if (inclset != null) { use = inclset.contains(memb.toLowerCase()); } else if (exclset != null) { use = !exclset.contains(memb.toLowerCase()); } if (use) { setmap.put(memb, item); } } } return setmap; } /** * Build map from member names to fields. This includes all non-static and non-transient fields of the class. * * @param fields * @param prefs prefixes to be stripped in deriving names * @param suffs suffixes to be stripped in deriving names * @param inclset set of member names to be included (null if not specified) * @param exclset set of member names to be excluded (null if not specified, ignored if inclset is * non-null) * @return map */ private Map mapFields(IClassItem[] fields, String[] prefs, String[] suffs, Set inclset, Set exclset) { // check all fields for use as members InsertionOrderedMap fieldmap = new InsertionOrderedMap(); for (int i = 0; i < fields.length; i++) { IClassItem item = fields[i]; String name = item.getName(); String memb = MemberCustom.memberNameFromField(name, prefs, suffs); boolean use = true; if (inclset != null) { use = inclset.contains(memb.toLowerCase()); } else if (exclset != null) { use = !exclset.contains(memb.toLowerCase()); } if (use) { fieldmap.put(memb, item); } } return fieldmap; } /** * Find the most specific type for a property based on the access methods. * * @param gmeth read access method (null if not defined) * @param smeth write access method (null if not defined) * @param icl * @return most specific type name */ private String findPropertyType(IClassItem gmeth, IClassItem smeth, IClassLocator icl) { String type; if (gmeth == null) { if (smeth == null) { throw new IllegalArgumentException("Internal error: no access methods known"); } else { type = smeth.getArgumentType(0); } } else if (smeth == null) { type = gmeth.getTypeName(); } else { String gtype = gmeth.getTypeName(); String stype = smeth.getArgumentType(0); IClass gclas = icl.getClassInfo(gtype); if (gclas.isSuperclass(stype) || gclas.isImplements(stype)) { type = gtype; } else { type = stype; } } return type; } /** * Utility method to strip any leading non-alphanumeric characters from an array of name strings. * * @param names (null if none) * @return array of stripped names (null if none) */ private static String[] stripNames(String[] names) { if (names == null) { return null; } else { String[] strips = null; for (int i = 0; i < names.length; i++) { String name = names[i]; int index = 0; while (index < name.length()) { char chr = name.charAt(index); if (Character.isLetter(chr) || Character.isDigit(chr)) { break; } else { index++; } } if (index > 0) { if (strips == null) { strips = new String[names.length]; System.arraycopy(names, 0, strips, 0, names.length); } strips[i] = name.substring(index); } } return (strips == null) ? names : strips; } } /** * Classify an array of names as elements or attributes, based on leading flag characters ('@' for an attribute, * '<' for an element). * * @param names (null if none) * @param elems set of element names * @param attrs set of attribute names */ private static void classifyNames(String[] names, Set elems, Set attrs) { if (names != null) { for (int i = 0; i < names.length; i++) { String name = names[i]; if (name.length() > 0) { char chr = name.charAt(0); Set addset = null; Set altset = null; if (chr == '@') { addset = attrs; altset = elems; } else if (chr == '/') { addset = elems; altset = attrs; } if (addset != null) { name = name.substring(1).toLowerCase(); addset.add(name); if (altset.contains(name)) { throw new IllegalArgumentException("Name " + name + " used as both element and attribute"); } } } } } } /** * Apply customizations to class to fill out members. * * @param icl class locator */ public void apply(IClassLocator icl) { // check for repeated call (happens due to package-level apply() if (m_isApplied) { return; } m_isApplied = true; // initialize class information m_classInformation = icl.getClassInfo(getName()); if (m_classInformation == null) { throw new IllegalStateException("Internal error: unable to find class " + m_name); } // inherit namespace directly from package level, if not specified String ns = getSpecifiedNamespace(); if (ns == null) { ns = getParent().getNamespace(); } setNamespace(ns); // set the name(s) to be used if mapped String cname = convertName(getSimpleName()); if (m_elementName == null) { m_elementName = cname; } m_elementQName = new QName(getNamespace(), m_elementName); if (m_typeName == null) { m_typeName = cname; } m_typeQName = new QName(getNamespace(), m_typeName); // initialize maps with existing member customizations HashMap namemap = new HashMap(); for (Iterator iter = getChildren().iterator(); iter.hasNext();) { MemberCustom memb = (MemberCustom)iter.next(); String name = memb.getBaseName(); if (name != null) { namemap.put(name, memb); } } // generate sets of names to be included or ignored, and optional/requires, elements/attributes Set exclset = null; Set inclset = null; Set elemset = Collections.EMPTY_SET; Set attrset = Collections.EMPTY_SET; Set optset = Collections.EMPTY_SET; Set reqset = Collections.EMPTY_SET; if (m_includes != null || m_optionals != null || m_requireds != null) { String[] optionals = stripNames(m_optionals); String[] requireds = stripNames(m_requireds); inclset = CustomUtils.noCaseNameSet(stripNames(m_includes)); if (optionals != null) { optset = CustomUtils.noCaseNameSet(optionals); } if (requireds != null) { reqset = CustomUtils.noCaseNameSet(requireds); } elemset = new HashSet(); attrset = new HashSet(); classifyNames(m_requireds, elemset, attrset); classifyNames(m_optionals, elemset, attrset); classifyNames(m_includes, elemset, attrset); } else if (m_excludes != null) { exclset = CustomUtils.noCaseNameSet(stripNames(m_excludes)); } // first find members from property access methods Map getmap = null; Map setmap = null; IClassItem[] methods = m_classInformation.getMethods(); GlobalCustom global = getGlobal(); if (global.isOutput()) { // find properties using read access methods getmap = mapPropertyReadMethods(methods, inclset, exclset); if (global.isInput()) { // find properties using write access methods setmap = mapPropertyWriteMethods(methods, inclset, exclset); // discard any read-only properties for (Iterator iter = getmap.keySet().iterator(); iter.hasNext();) { if (!setmap.containsKey(iter.next())) { iter.remove(); } } } } if (global.isInput()) { // find properties using write access methods setmap = mapPropertyWriteMethods(methods, inclset, exclset); } // find members from fields Map fieldmap = mapFields(m_classInformation.getFields(), getStripPrefixes(), getStripSuffixes(), inclset, exclset); // get list of names selected for use by options ArrayList names; if (isPropertyAccess()) { if (global.isOutput()) { names = new ArrayList(getmap.keySet()); } else { names = new ArrayList(setmap.keySet()); } } else { names = new ArrayList(); for (Iterator iter = fieldmap.keySet().iterator(); iter.hasNext();) { String name = (String)iter.next(); IClassItem field = (IClassItem)fieldmap.get(name); int access = field.getAccessFlags(); if (!Modifier.isStatic(access) && !Modifier.isTransient(access)) { names.add(name); } } } // process all members found in class m_memberMap = new InsertionOrderedMap(); boolean auto = !getName().startsWith("java.") && !getName().startsWith("javax."); for (int i = 0; i < names.size(); i++) { // get basic member information String name = (String)names.get(i); String lcname = name.toLowerCase(); MemberCustom cust = null; IClassItem gmeth = (IClassItem)(getmap == null ? null : getmap.get(name)); IClassItem smeth = (IClassItem)(setmap == null ? null : setmap.get(name)); IClassItem field = (IClassItem)(fieldmap == null ? null : fieldmap.get(name)); // find the optional/required setting Boolean isreq = null; if (optset.contains(lcname)) { isreq = Boolean.FALSE; } if (reqset.contains(lcname)) { isreq = Boolean.TRUE; } // find the style setting Integer style = null; if (attrset.contains(lcname)) { style = ATTRIBUTE_STYLE_INTEGER; } else if (elemset.contains(lcname)) { style = ELEMENT_STYLE_INTEGER; } // check for existing customization if (namemap.containsKey(name)) { // fill in data missing from existing member customization cust = (MemberCustom)namemap.get(name); if (cust instanceof MemberFieldCustom) { MemberFieldCustom mfcust = (MemberFieldCustom)cust; mfcust.completeField((IClassItem)fieldmap.get(name), isreq, style); } else { String type = findPropertyType(gmeth, smeth, icl); MemberPropertyCustom mpcust = (MemberPropertyCustom)cust; mpcust.completeProperty(gmeth, smeth, type, isreq, style); } } else if (auto) { if (isPropertyAccess()) { if (gmeth != null || smeth != null) { // check for a collection property MemberPropertyCustom pcust; String type = findPropertyType(gmeth, smeth, icl); IClass info = icl.getClassInfo(type); if (type.endsWith("[]") || info.isImplements("Ljava/util/Collection;")) { pcust = new CollectionPropertyCustom(this, name); } else { pcust = new MemberPropertyCustom(this, name); } // fill in the details of the property pcust.completeProperty(gmeth, smeth, type, isreq, style); cust = pcust; } } else if (field != null) { // check for a collection field MemberFieldCustom fcust; String type = field.getTypeName(); IClass info = icl.getClassInfo(type); if (type.endsWith("[]") || info.isImplements("Ljava/util/Collection;")) { fcust = new CollectionFieldCustom(this, name); } else { fcust = new MemberFieldCustom(this, name); } // fill in the details of the property fcust.completeField(field, isreq, style); cust = fcust; } } // add customization to map if (cust != null) { m_memberMap.put(name, cust); } } // check for any supplied customizations that haven't been matched for (Iterator iter = namemap.keySet().iterator(); iter.hasNext();) { String name = (String)iter.next(); String lcname = name.toLowerCase(); if (!m_memberMap.containsKey(name)) { // find the optional/required setting Boolean req = null; if (optset != null && optset.contains(lcname)) { req = Boolean.FALSE; } if (reqset != null && reqset.contains(lcname)) { req = Boolean.TRUE; } // find the style setting Integer style = null; if (attrset.contains(lcname)) { style = ATTRIBUTE_STYLE_INTEGER; } else if (elemset.contains(lcname)) { style = ELEMENT_STYLE_INTEGER; } // complete the customization and add to map MemberCustom cust = (MemberCustom)namemap.get(name); cust.complete(null, req, style); m_memberMap.put(name, cust); } } } /** * Get customization information for a member by name. This method may only be called after * {@link #apply(IClassLocator)}. * * @param name * @return customization, or null if none */ public MemberCustom getMember(String name) { return (MemberCustom)m_memberMap.get(name); } /** * Get actual class information. This method may only be called after {@link #apply(IClassLocator)}. * * @return class information */ public IClass getClassInformation() { return m_classInformation; } /** * Get collection of members in class. * * @return members */ public Collection getMembers() { return m_memberMap.values(); } }