/* * Copyright 2007 Werner Guttmann * * 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.exolab.castor.builder; import java.util.ArrayList; import java.util.Enumeration; 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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.castor.xml.JavaNaming; import org.exolab.castor.builder.binding.ExtendedBinding; import org.exolab.castor.builder.binding.XMLBindingComponent; import org.exolab.castor.builder.binding.XPathHelper; import org.exolab.castor.builder.binding.xml.Exclude; import org.exolab.castor.builder.conflict.strategy.ClassNameConflictResolver; import org.exolab.castor.builder.conflict.strategy.XPATHClassNameConflictResolver; import org.exolab.castor.xml.schema.Annotated; import org.exolab.castor.xml.schema.ElementDecl; import org.exolab.castor.xml.schema.Group; import org.exolab.castor.xml.schema.ModelGroup; import org.exolab.castor.xml.schema.Order; import org.exolab.castor.xml.schema.XMLType; import org.exolab.javasource.JClass; /** * A registry for maintaing information about {@link JClass} instances already * processed. * * @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a> * @since 1.1 */ public class JClassRegistry { /** * Logger instance used for all logging functionality. */ private static final Log LOG = LogFactory.getLog(JClassRegistry.class); /** * Registry for holding a set of global element definitions. */ private Set<String> _globalElements = new HashSet<String>(); /** * Registry for mapping an XPATH location to a {@link JClass} instance * generated for the XML artefact uniquely identified by the XPATH. */ private Map<String, JClass> _xpathToJClass = new HashMap<String, JClass>(); /** * Registry for recording naming collisions, keyed by the local part of an * otherwise rooted XPATH. */ private Map<String, List<String>> _localNames = new HashMap<String, List<String>>(); /** * Registry for recording naming collisions, keyed by the typed local part of an * otherwise rooted XPATH. */ private Map<String, String> _typedLocalNames = new HashMap<String, String>(); /** * JavaNaming to be used. * @since 1.1.3 */ private JavaNaming _javaNaming; /** * Class name conflict resolver. */ private ClassNameConflictResolver _classNameConflictResolver = new XPATHClassNameConflictResolver(); /** * Registers the XPATH identifier for a global element definition for * further use. * * @param xpath * The XPATH identifier of a global element. */ public void prebindGlobalElement(final String xpath) { _globalElements.add(xpath); } /** * Creates an instance of this class, providing the class anme conflict * resolver to be used during automatic class name conflict resolution * (for local element conflicts). * @param resolver {@link ClassNameConflictResolver} instance to be used * @param javaNaming the {@link JavaNaming} to use (must not be null). */ public JClassRegistry(final ClassNameConflictResolver resolver, final JavaNaming javaNaming) { _classNameConflictResolver = resolver; _javaNaming = javaNaming; } /** * Registers a {@link JClass} instance for a given XPATH. * * @param jClass * The {@link JClass} instance to register. * @param component * Container for the {@link Annotated} instance referred to by the XPATH. * @param mode Whether we register JClass instances in 'field' or 'class'mode. */ public void bind(final JClass jClass, final XMLBindingComponent component, final String mode) { Annotated annotated = component.getAnnotated(); // String xPath = XPathHelper.getSchemaLocation(annotated); String xPath = XPathHelper.getSchemaLocation(annotated, true); String localXPath = getLocalXPath(xPath); String untypedXPath = xPath; //TODO: it could happen that we silently ignore a custom package as defined in a binding - FIX ! // get local name String localName = getLocalName(xPath); String typedLocalName = localName; if (annotated instanceof ElementDecl) { ElementDecl element = (ElementDecl) annotated; String typexPath = XPathHelper.getSchemaLocation(element.getType()); xPath += "[" + typexPath + "]"; typedLocalName += "[" + typexPath + "]"; } else if (annotated instanceof Group) { Group group = (Group) annotated; if (group.getOrder() == Order.choice && !_globalElements.contains("/" + localXPath)) { xPath += "/#choice"; } } ExtendedBinding binding = component.getBinding(); if (binding != null) { // deal with explicit exclusions if (binding.existsExclusion(typedLocalName)) { Exclude exclusion = binding.getExclusion(typedLocalName); if (exclusion.getClassName() != null) { LOG.info("Dealing with exclusion for local element " + xPath + " as per binding file."); jClass.changeLocalName(exclusion.getClassName()); } return; } // deal with explicit forces if (binding.existsForce(localName)) { List<String> localNamesList = _localNames.get(localName); memorizeCollision(xPath, localName, localNamesList); LOG.info("Changing class name for local element " + xPath + " as per binding file (force)."); checkAndChange(jClass, annotated, untypedXPath, typedLocalName); return; } } String jClassLocalName = jClass.getLocalName(); String expectedClassNameDerivedFromXPath = _javaNaming.toJavaClassName(localName); if (!jClassLocalName.equals(expectedClassNameDerivedFromXPath)) { if (component.createGroupItem()) { xPath += "/#item"; } _xpathToJClass.put(xPath, jClass); return; } if (mode.equals("field")) { if (annotated instanceof ModelGroup) { ModelGroup group = (ModelGroup) annotated; final boolean isReference = group.isReference(); if (isReference) { return; } } if (annotated instanceof ElementDecl) { ElementDecl element = (ElementDecl) annotated; final boolean isReference = element.isReference(); if (isReference) { ElementDecl referredElement = element.getReference(); // if that global element definition is a substitution head, // we now // need to do work out the global element's type, and use // its // JClass instance to defer the type of the member currently // processed Enumeration<?> possibleSubstitutes = referredElement .getSubstitutionGroupMembers(); if (possibleSubstitutes.hasMoreElements()) { XMLType referredType = referredElement.getType(); String xPathType = XPathHelper.getSchemaLocation(referredType); JClass typeJClass = _xpathToJClass.get(xPathType); if (typeJClass != null) { jClass.changeLocalName(typeJClass.getLocalName()); } else { // manually deriving class name for referenced type XMLBindingComponent temp = component; temp.setView(referredType); jClass.changeLocalName(temp.getJavaClassName()); component.setView(annotated); } // String typeXPath = XPathHelper // .getSchemaLocation(referredElement); // JClass referredJClass = (JClass) _xpathToJClass // .get(typeXPath + "_class"); // jClass.changeLocalName(referredJClass.getSuperClass() // .getLocalName()); } return; } } } final boolean alreadyProcessed = _xpathToJClass.containsKey(xPath); // if already processed, change the JClass instance accordingly if (alreadyProcessed) { JClass jClassAlreadyProcessed = _xpathToJClass.get(xPath); jClass.changeLocalName(jClassAlreadyProcessed.getLocalName()); return; } // register JClass instance for XPATH _xpathToJClass.put(xPath, jClass); if (LOG.isDebugEnabled()) { LOG.debug("Binding JClass[" + jClass.getName() + "] for XML schema structure " + xPath); } // global elements don't need to change final boolean isGlobalElement = _globalElements.contains(untypedXPath); if (isGlobalElement) { return; } // resolve references to global elements if (mode.equals("field") && annotated instanceof ElementDecl) { ElementDecl element = (ElementDecl) annotated; final boolean isReference = element.isReference(); if (isReference) { ElementDecl referredElement = element.getReference(); // if that global element definition is a substitution head, we // now // need to do work out the global element's type, and use its // JClass instance to defer the type of the member currently // processed Enumeration<?> possibleSubstitutes = referredElement .getSubstitutionGroupMembers(); if (possibleSubstitutes.hasMoreElements()) { String typeXPath = XPathHelper .getSchemaLocation(referredElement); JClass referredJClass = _xpathToJClass .get(typeXPath + "_class"); jClass.changeLocalName(referredJClass.getSuperClass() .getLocalName()); } return; } } // resolve conflict with a global element final boolean conflictExistsWithGlobalElement = _globalElements .contains("/" + localXPath); if (conflictExistsWithGlobalElement) { LOG.info("Resolving conflict for local element " + xPath + " against global element."); checkAndChange(jClass, annotated, untypedXPath, typedLocalName); // remember that we had a collision for this local element List<String> localNamesList = _localNames.get(localName); memorizeCollision(xPath, localName, localNamesList); return; } List<String> localNamesList = _localNames.get(localName); memorizeCollision(xPath, localName, localNamesList); if (localNamesList == null) { String typedJClassName = _typedLocalNames.get(typedLocalName); if (typedJClassName == null) { _typedLocalNames.put(typedLocalName, jClass.getName()); } } else { LOG.info("Resolving conflict for local element " + xPath + " against another local element of the same name."); checkAndChange(jClass, annotated, untypedXPath, typedLocalName); } } /** * Memorize that we have a collision for the 'local name' given. * @param xPath Full (typed) XPATH identifier for the local element definition. * @param localName Local element name * @param localNamesList Collection store for collisions for that 'local name'. */ private void memorizeCollision(final String xPath, final String localName, final List<String> localNamesList) { // resolve conflict with another element if (localNamesList == null) { // this name never occured before List<String> arrayList = new ArrayList<String>(); arrayList.add(xPath); _localNames.put(localName, arrayList); } else { // this entry should be renamed if (!localNamesList.contains(xPath)) { localNamesList.add(xPath); } } } /** * Check and change (suggested) class name. * @param jClass {@link JClass} instance * @param annotated {@link Annotated} instance * @param untypedXPath blah * @param typedLocalName blah */ private void checkAndChange(final JClass jClass, final Annotated annotated, final String untypedXPath, final String typedLocalName) { // check whether we have seen that typed local name already String typedJClassName = _typedLocalNames.get(typedLocalName); if (typedJClassName != null) { // if so, simple re-use it by changing the local class name String localClassName = typedJClassName.substring(typedJClassName.lastIndexOf(".") + 1); jClass.changeLocalName(localClassName); } else { // 'calculate' a new class name changeClassInfoAsResultOfConflict(jClass, untypedXPath, typedLocalName, annotated); // store it for further use _typedLocalNames.put(typedLocalName, jClass.getName()); } } /** * Returns the local name of rooted XPATH expression. * * @param xPath * An (rooted) XPATH expression * @return the local name */ private String getLocalName(final String xPath) { String localName = xPath.substring(xPath.lastIndexOf("/") + 1); if (localName.startsWith(ExtendedBinding.COMPLEXTYPE_ID) || localName.startsWith(ExtendedBinding.SIMPLETYPE_ID) || localName.startsWith(ExtendedBinding.ENUMTYPE_ID) || localName.startsWith(ExtendedBinding.GROUP_ID)) { localName = localName.substring(localName.indexOf(":") + 1); } return localName; } /** * Returns the local part of rooted XPATH expression. * * @param xPath * An (rooted) XPATH expression * @return the local part */ private String getLocalXPath(final String xPath) { return xPath.substring(xPath.lastIndexOf("/") + 1); } /** * Changes the JClass' internal class name, as a result of an XPATH * expression uniquely identifying an XML artefact within an XML schema. * * @param jClass * The {@link JClass} instance whose local name should be * changed. * @param xpath * XPATH expression used to defer the new local class name * @param typedXPath * Typed XPATH expression used to defer the new local class name * @param annotated {@link Annotated} instance */ private void changeClassInfoAsResultOfConflict(final JClass jClass, final String xpath, final String typedXPath, final Annotated annotated) { _classNameConflictResolver.changeClassInfoAsResultOfConflict(jClass, xpath, typedXPath, annotated); } /** * Sets the {@link ClassNameConflictResolver} insatnce to be used. * @param conflictResolver {@link ClassNameConflictResolver} insatnce to be used. */ public void setClassNameConflictResolver(final ClassNameConflictResolver conflictResolver) { _classNameConflictResolver = conflictResolver; } /** * Utility method to gather and output statistical information about naming * collisions occurred during source code generation. * @param binding {@link XMLBindingComponent} instance */ public void printStatistics(final XMLBindingComponent binding) { Iterator<String> keyIterator = _localNames.keySet().iterator(); LOG.info("*** Summary ***"); if (binding.getBinding() != null && binding.getBinding().getForces() != null && binding.getBinding().getForces().size() > 0) { Iterator<String> forceIterator = binding.getBinding().getForces().iterator(); LOG.info("The following 'forces' have been enabled:"); while (forceIterator.hasNext()) { String forceValue = forceIterator.next(); LOG.info(forceValue); } } if (keyIterator.hasNext()) { LOG.info("Local name conflicts encountered for the following element definitions"); while (keyIterator.hasNext()) { String localName = keyIterator.next(); List<String> collisions = _localNames.get(localName); if (collisions.size() > 1 && !ofTheSameType(collisions)) { LOG.info(localName + ", with the following (element) definitions being involved:"); for (Iterator<String> iter = collisions.iterator(); iter.hasNext(); ) { String xPath = iter.next(); LOG.info(xPath); } } } } keyIterator = _localNames.keySet().iterator(); if (keyIterator.hasNext()) { StringBuilder xmlFragment = new StringBuilder(32); xmlFragment.append("<forces>\n"); while (keyIterator.hasNext()) { String localName = keyIterator.next(); List<String> collisions = _localNames.get(localName); if (collisions.size() > 1 && !ofTheSameType(collisions)) { xmlFragment.append(" <force>"); xmlFragment.append(localName); xmlFragment.append("</force>\n"); } } xmlFragment.append("</forces>"); LOG.info(xmlFragment.toString()); } } /** * Indicates whether all XPATH entries within the list of collisions are of the same type. * @param collisions The list of XPATH (collidings) for a local element name * @return True if all are of the same type. */ private boolean ofTheSameType(final List<String> collisions) { boolean allSame = true; Iterator<String> iterator = collisions.iterator(); String typeString = null; while (iterator.hasNext()) { String xPath = iterator.next(); String newTypeString = xPath.substring(xPath.indexOf("[") + 1, xPath.indexOf("]")); if (typeString != null) { if (!typeString.equals(newTypeString)) { allSame = false; break; } } else { typeString = newTypeString; } } return allSame; } }