/*
* 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;
}
}