View Javadoc
1   /*
2    * Copyright 2007 Werner Guttmann
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  package org.exolab.castor.builder;
15  
16  import java.util.ArrayList;
17  import java.util.Enumeration;
18  import java.util.HashMap;
19  import java.util.HashSet;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Set;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.castor.xml.JavaNaming;
28  import org.exolab.castor.builder.binding.ExtendedBinding;
29  import org.exolab.castor.builder.binding.XMLBindingComponent;
30  import org.exolab.castor.builder.binding.XPathHelper;
31  import org.exolab.castor.builder.binding.xml.Exclude;
32  import org.exolab.castor.builder.conflict.strategy.ClassNameConflictResolver;
33  import org.exolab.castor.builder.conflict.strategy.XPATHClassNameConflictResolver;
34  import org.exolab.castor.xml.schema.Annotated;
35  import org.exolab.castor.xml.schema.ElementDecl;
36  import org.exolab.castor.xml.schema.Group;
37  import org.exolab.castor.xml.schema.ModelGroup;
38  import org.exolab.castor.xml.schema.Order;
39  import org.exolab.castor.xml.schema.XMLType;
40  import org.exolab.javasource.JClass;
41  
42  /**
43   * A registry for maintaing information about {@link JClass} instances already processed.
44   * 
45   * @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a>
46   * @since 1.1
47   */
48  public class JClassRegistry {
49  
50    /**
51     * Logger instance used for all logging functionality.
52     */
53    private static final Log LOG = LogFactory.getLog(JClassRegistry.class);
54  
55    /**
56     * Registry for holding a set of global element definitions.
57     */
58    private Set<String> _globalElements = new HashSet<String>();
59  
60    /**
61     * Registry for mapping an XPATH location to a {@link JClass} instance generated for the XML
62     * artefact uniquely identified by the XPATH.
63     */
64    private Map<String, JClass> _xpathToJClass = new HashMap<String, JClass>();
65  
66    /**
67     * Registry for recording naming collisions, keyed by the local part of an otherwise rooted XPATH.
68     */
69    private Map<String, List<String>> _localNames = new HashMap<String, List<String>>();
70  
71    /**
72     * Registry for recording naming collisions, keyed by the typed local part of an otherwise rooted
73     * XPATH.
74     */
75    private Map<String, String> _typedLocalNames = new HashMap<String, String>();
76  
77    /**
78     * JavaNaming to be used.
79     * 
80     * @since 1.1.3
81     */
82    private JavaNaming _javaNaming;
83  
84  
85    /**
86     * Class name conflict resolver.
87     */
88    private ClassNameConflictResolver _classNameConflictResolver =
89        new XPATHClassNameConflictResolver();
90  
91    /**
92     * Registers the XPATH identifier for a global element definition for further use.
93     * 
94     * @param xpath The XPATH identifier of a global element.
95     */
96    public void prebindGlobalElement(final String xpath) {
97      _globalElements.add(xpath);
98    }
99  
100   /**
101    * Creates an instance of this class, providing the class anme conflict resolver to be used during
102    * automatic class name conflict resolution (for local element conflicts).
103    * 
104    * @param resolver {@link ClassNameConflictResolver} instance to be used
105    * @param javaNaming the {@link JavaNaming} to use (must not be null).
106    */
107   public JClassRegistry(final ClassNameConflictResolver resolver, final JavaNaming javaNaming) {
108     _classNameConflictResolver = resolver;
109     _javaNaming = javaNaming;
110   }
111 
112   /**
113    * Registers a {@link JClass} instance for a given XPATH.
114    * 
115    * @param jClass The {@link JClass} instance to register.
116    * @param component Container for the {@link Annotated} instance referred to by the XPATH.
117    * @param mode Whether we register JClass instances in 'field' or 'class'mode.
118    */
119   public void bind(final JClass jClass, final XMLBindingComponent component, final String mode) {
120 
121     Annotated annotated = component.getAnnotated();
122 
123     // String xPath = XPathHelper.getSchemaLocation(annotated);
124     String xPath = XPathHelper.getSchemaLocation(annotated, true);
125     String localXPath = getLocalXPath(xPath);
126     String untypedXPath = xPath;
127 
128     // TODO: it could happen that we silently ignore a custom package as defined in a binding - FIX
129     // !
130 
131     // get local name
132     String localName = getLocalName(xPath);
133     String typedLocalName = localName;
134 
135     if (annotated instanceof ElementDecl) {
136       ElementDecl element = (ElementDecl) annotated;
137       String typexPath = XPathHelper.getSchemaLocation(element.getType());
138       xPath += "[" + typexPath + "]";
139       typedLocalName += "[" + typexPath + "]";
140     } else if (annotated instanceof Group) {
141       Group group = (Group) annotated;
142       if (group.getOrder() == Order.choice && !_globalElements.contains("/" + localXPath)) {
143         xPath += "/#choice";
144       }
145     }
146 
147     ExtendedBinding binding = component.getBinding();
148     if (binding != null) {
149       // deal with explicit exclusions
150       if (binding.existsExclusion(typedLocalName)) {
151         Exclude exclusion = binding.getExclusion(typedLocalName);
152         if (exclusion.getClassName() != null) {
153           LOG.info("Dealing with exclusion for local element " + xPath + " as per binding file.");
154           jClass.changeLocalName(exclusion.getClassName());
155         }
156         return;
157       }
158 
159       // deal with explicit forces
160       if (binding.existsForce(localName)) {
161 
162         List<String> localNamesList = _localNames.get(localName);
163         memorizeCollision(xPath, localName, localNamesList);
164 
165         LOG.info(
166             "Changing class name for local element " + xPath + " as per binding file (force).");
167         checkAndChange(jClass, annotated, untypedXPath, typedLocalName);
168         return;
169       }
170     }
171 
172 
173     String jClassLocalName = jClass.getLocalName();
174     String expectedClassNameDerivedFromXPath = _javaNaming.toJavaClassName(localName);
175     if (!jClassLocalName.equals(expectedClassNameDerivedFromXPath)) {
176       if (component.createGroupItem()) {
177         xPath += "/#item";
178       }
179       _xpathToJClass.put(xPath, jClass);
180       return;
181     }
182 
183     if (mode.equals("field")) {
184       if (annotated instanceof ModelGroup) {
185         ModelGroup group = (ModelGroup) annotated;
186         final boolean isReference = group.isReference();
187         if (isReference) {
188           return;
189         }
190       }
191 
192       if (annotated instanceof ElementDecl) {
193         ElementDecl element = (ElementDecl) annotated;
194 
195         final boolean isReference = element.isReference();
196         if (isReference) {
197           ElementDecl referredElement = element.getReference();
198           // if that global element definition is a substitution head,
199           // we now
200           // need to do work out the global element's type, and use
201           // its
202           // JClass instance to defer the type of the member currently
203           // processed
204           Enumeration<?> possibleSubstitutes = referredElement.getSubstitutionGroupMembers();
205           if (possibleSubstitutes.hasMoreElements()) {
206             XMLType referredType = referredElement.getType();
207             String xPathType = XPathHelper.getSchemaLocation(referredType);
208             JClass typeJClass = _xpathToJClass.get(xPathType);
209             if (typeJClass != null) {
210               jClass.changeLocalName(typeJClass.getLocalName());
211             } else {
212               // manually deriving class name for referenced type
213               XMLBindingComponent temp = component;
214               temp.setView(referredType);
215               jClass.changeLocalName(temp.getJavaClassName());
216               component.setView(annotated);
217             }
218             // String typeXPath = XPathHelper
219             // .getSchemaLocation(referredElement);
220             // JClass referredJClass = (JClass) _xpathToJClass
221             // .get(typeXPath + "_class");
222             // jClass.changeLocalName(referredJClass.getSuperClass()
223             // .getLocalName());
224           }
225           return;
226         }
227       }
228     }
229 
230     final boolean alreadyProcessed = _xpathToJClass.containsKey(xPath);
231 
232     // if already processed, change the JClass instance accordingly
233     if (alreadyProcessed) {
234       JClass jClassAlreadyProcessed = _xpathToJClass.get(xPath);
235       jClass.changeLocalName(jClassAlreadyProcessed.getLocalName());
236       return;
237     }
238 
239     // register JClass instance for XPATH
240     _xpathToJClass.put(xPath, jClass);
241 
242     if (LOG.isDebugEnabled()) {
243       LOG.debug("Binding JClass[" + jClass.getName() + "] for XML schema structure " + xPath);
244     }
245 
246     // global elements don't need to change
247     final boolean isGlobalElement = _globalElements.contains(untypedXPath);
248     if (isGlobalElement) {
249       return;
250     }
251 
252     // resolve references to global elements
253     if (mode.equals("field") && annotated instanceof ElementDecl) {
254       ElementDecl element = (ElementDecl) annotated;
255       final boolean isReference = element.isReference();
256       if (isReference) {
257         ElementDecl referredElement = element.getReference();
258         // if that global element definition is a substitution head, we
259         // now
260         // need to do work out the global element's type, and use its
261         // JClass instance to defer the type of the member currently
262         // processed
263         Enumeration<?> possibleSubstitutes = referredElement.getSubstitutionGroupMembers();
264         if (possibleSubstitutes.hasMoreElements()) {
265           String typeXPath = XPathHelper.getSchemaLocation(referredElement);
266           JClass referredJClass = _xpathToJClass.get(typeXPath + "_class");
267           jClass.changeLocalName(referredJClass.getSuperClass().getLocalName());
268         }
269         return;
270       }
271     }
272 
273     // resolve conflict with a global element
274     final boolean conflictExistsWithGlobalElement = _globalElements.contains("/" + localXPath);
275     if (conflictExistsWithGlobalElement) {
276       LOG.info("Resolving conflict for local element " + xPath + " against global element.");
277 
278       checkAndChange(jClass, annotated, untypedXPath, typedLocalName);
279 
280       // remember that we had a collision for this local element
281       List<String> localNamesList = _localNames.get(localName);
282       memorizeCollision(xPath, localName, localNamesList);
283       return;
284     }
285 
286     List<String> localNamesList = _localNames.get(localName);
287     memorizeCollision(xPath, localName, localNamesList);
288     if (localNamesList == null) {
289       String typedJClassName = _typedLocalNames.get(typedLocalName);
290       if (typedJClassName == null) {
291         _typedLocalNames.put(typedLocalName, jClass.getName());
292       }
293     } else {
294       LOG.info("Resolving conflict for local element " + xPath
295           + " against another local element of the same name.");
296       checkAndChange(jClass, annotated, untypedXPath, typedLocalName);
297     }
298   }
299 
300   /**
301    * Memorize that we have a collision for the 'local name' given.
302    * 
303    * @param xPath Full (typed) XPATH identifier for the local element definition.
304    * @param localName Local element name
305    * @param localNamesList Collection store for collisions for that 'local name'.
306    */
307   private void memorizeCollision(final String xPath, final String localName,
308       final List<String> localNamesList) {
309     // resolve conflict with another element
310     if (localNamesList == null) {
311       // this name never occured before
312       List<String> arrayList = new ArrayList<String>();
313       arrayList.add(xPath);
314       _localNames.put(localName, arrayList);
315 
316     } else {
317       // this entry should be renamed
318       if (!localNamesList.contains(xPath)) {
319         localNamesList.add(xPath);
320       }
321     }
322   }
323 
324   /**
325    * Check and change (suggested) class name.
326    * 
327    * @param jClass {@link JClass} instance
328    * @param annotated {@link Annotated} instance
329    * @param untypedXPath blah
330    * @param typedLocalName blah
331    */
332   private void checkAndChange(final JClass jClass, final Annotated annotated,
333       final String untypedXPath, final String typedLocalName) {
334 
335     // check whether we have seen that typed local name already
336     String typedJClassName = _typedLocalNames.get(typedLocalName);
337     if (typedJClassName != null) {
338       // if so, simple re-use it by changing the local class name
339       String localClassName = typedJClassName.substring(typedJClassName.lastIndexOf(".") + 1);
340       jClass.changeLocalName(localClassName);
341     } else {
342       // 'calculate' a new class name
343       changeClassInfoAsResultOfConflict(jClass, untypedXPath, typedLocalName, annotated);
344 
345       // store it for further use
346       _typedLocalNames.put(typedLocalName, jClass.getName());
347     }
348   }
349 
350   /**
351    * Returns the local name of rooted XPATH expression.
352    * 
353    * @param xPath An (rooted) XPATH expression
354    * @return the local name
355    */
356   private String getLocalName(final String xPath) {
357     String localName = xPath.substring(xPath.lastIndexOf("/") + 1);
358     if (localName.startsWith(ExtendedBinding.COMPLEXTYPE_ID)
359         || localName.startsWith(ExtendedBinding.SIMPLETYPE_ID)
360         || localName.startsWith(ExtendedBinding.ENUMTYPE_ID)
361         || localName.startsWith(ExtendedBinding.GROUP_ID)) {
362       localName = localName.substring(localName.indexOf(":") + 1);
363     }
364     return localName;
365   }
366 
367   /**
368    * Returns the local part of rooted XPATH expression.
369    * 
370    * @param xPath An (rooted) XPATH expression
371    * @return the local part
372    */
373   private String getLocalXPath(final String xPath) {
374     return xPath.substring(xPath.lastIndexOf("/") + 1);
375   }
376 
377   /**
378    * Changes the JClass' internal class name, as a result of an XPATH expression uniquely
379    * identifying an XML artefact within an XML schema.
380    * 
381    * @param jClass The {@link JClass} instance whose local name should be changed.
382    * @param xpath XPATH expression used to defer the new local class name
383    * @param typedXPath Typed XPATH expression used to defer the new local class name
384    * @param annotated {@link Annotated} instance
385    */
386   private void changeClassInfoAsResultOfConflict(final JClass jClass, final String xpath,
387       final String typedXPath, final Annotated annotated) {
388     _classNameConflictResolver.changeClassInfoAsResultOfConflict(jClass, xpath, typedXPath,
389         annotated);
390   }
391 
392   /**
393    * Sets the {@link ClassNameConflictResolver} insatnce to be used.
394    * 
395    * @param conflictResolver {@link ClassNameConflictResolver} insatnce to be used.
396    */
397   public void setClassNameConflictResolver(final ClassNameConflictResolver conflictResolver) {
398     _classNameConflictResolver = conflictResolver;
399   }
400 
401   /**
402    * Utility method to gather and output statistical information about naming collisions occurred
403    * during source code generation.
404    * 
405    * @param binding {@link XMLBindingComponent} instance
406    */
407   public void printStatistics(final XMLBindingComponent binding) {
408     Iterator<String> keyIterator = _localNames.keySet().iterator();
409     LOG.info("*** Summary ***");
410     if (binding.getBinding() != null && binding.getBinding().getForces() != null
411         && !binding.getBinding().getForces().isEmpty()) {
412       Iterator<String> forceIterator = binding.getBinding().getForces().iterator();
413       LOG.info("The following 'forces' have been enabled:");
414       while (forceIterator.hasNext()) {
415         String forceValue = forceIterator.next();
416         LOG.info(forceValue);
417       }
418     }
419     if (keyIterator.hasNext()) {
420       LOG.info("Local name conflicts encountered for the following element definitions");
421       while (keyIterator.hasNext()) {
422         String localName = keyIterator.next();
423         List<String> collisions = _localNames.get(localName);
424         if (collisions.size() > 1 && !ofTheSameType(collisions)) {
425           LOG.info(localName + ", with the following (element) definitions being involved:");
426           for (Iterator<String> iter = collisions.iterator(); iter.hasNext();) {
427             String xPath = iter.next();
428             LOG.info(xPath);
429           }
430         }
431       }
432     }
433 
434     keyIterator = _localNames.keySet().iterator();
435     if (keyIterator.hasNext()) {
436       StringBuilder xmlFragment = new StringBuilder(32);
437       xmlFragment.append("<forces>\n");
438       while (keyIterator.hasNext()) {
439         String localName = keyIterator.next();
440         List<String> collisions = _localNames.get(localName);
441         if (collisions.size() > 1 && !ofTheSameType(collisions)) {
442           xmlFragment.append("   <force>");
443           xmlFragment.append(localName);
444           xmlFragment.append("</force>\n");
445         }
446       }
447       xmlFragment.append("</forces>");
448 
449       LOG.info(xmlFragment.toString());
450     }
451 
452   }
453 
454   /**
455    * Indicates whether all XPATH entries within the list of collisions are of the same type.
456    * 
457    * @param collisions The list of XPATH (collidings) for a local element name
458    * @return True if all are of the same type.
459    */
460   private boolean ofTheSameType(final List<String> collisions) {
461     boolean allSame = true;
462     Iterator<String> iterator = collisions.iterator();
463     String typeString = null;
464     while (iterator.hasNext()) {
465       String xPath = iterator.next();
466       String newTypeString = xPath.substring(xPath.indexOf("[") + 1, xPath.indexOf("]"));
467       if (typeString != null) {
468         if (!typeString.equals(newTypeString)) {
469           allSame = false;
470           break;
471         }
472       } else {
473         typeString = newTypeString;
474       }
475     }
476     return allSame;
477   }
478 
479 }