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