View Javadoc

1   /*******************************************************************************
2    * Copyright (c) 2015 LegSem.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the GNU Lesser Public License v2.1
5    * which accompanies this distribution, and is available at
6    * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
7    * 
8    * Contributors:
9    *     LegSem - initial API and implementation
10   ******************************************************************************/
11  package com.legstar.jaxb.plugin;
12  
13  import java.io.IOException;
14  import java.util.Arrays;
15  import java.util.Collections;
16  import java.util.List;
17  import java.util.Locale;
18  import java.util.Map.Entry;
19  
20  import javax.xml.bind.annotation.XmlSchemaType;
21  
22  import org.apache.commons.lang.ArrayUtils;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.w3c.dom.Element;
26  import org.xml.sax.ErrorHandler;
27  
28  import com.legstar.coxb.CobolComplexType;
29  import com.legstar.coxb.CobolElement;
30  import com.legstar.coxb.CobolMarkup;
31  import com.legstar.coxb.CobolType;
32  import com.sun.codemodel.JAnnotationUse;
33  import com.sun.codemodel.JDefinedClass;
34  import com.sun.codemodel.JExpr;
35  import com.sun.codemodel.JFieldVar;
36  import com.sun.tools.xjc.BadCommandLineException;
37  import com.sun.tools.xjc.Options;
38  import com.sun.tools.xjc.Plugin;
39  import com.sun.tools.xjc.model.CClassInfo;
40  import com.sun.tools.xjc.model.CElement;
41  import com.sun.tools.xjc.model.CElementInfo;
42  import com.sun.tools.xjc.model.CPluginCustomization;
43  import com.sun.tools.xjc.model.CPropertyInfo;
44  import com.sun.tools.xjc.model.CReferencePropertyInfo;
45  import com.sun.tools.xjc.model.Model;
46  import com.sun.tools.xjc.model.nav.NClass;
47  import com.sun.tools.xjc.outline.ClassOutline;
48  import com.sun.tools.xjc.outline.FieldOutline;
49  import com.sun.tools.xjc.outline.Outline;
50  import com.sun.xml.bind.api.impl.NameConverter;
51  
52  /**
53   * This is an extension to the JAXB XJC plugin. It is being invoked by the JAXB
54   * XML to Java compilation and injects supplementary cobol annotations into the
55   * generated Java classes. Add -Dcom.sun.tools.xjc.Options.findServices=true to
56   * VM arguments to help solve classpath issues.
57   * 
58   */
59  public class CobolJAXBAnnotator extends Plugin {
60  
61      /**
62       * Constant values used throughout the annotator.
63       */
64      /** Option passed to XJC to enable this cobol plugin. */
65      public static final String OPTION_NAME = "Xlegstar-code";
66  
67      /** Will be true in ECI compatibility mode. */
68      private boolean isEciCompatible;
69  
70      /** Logger to be used only at development time (messes up ant output). */
71      private final Log _log = LogFactory.getLog(getClass());
72  
73      /** Command line help for cobol plugin XJC option. */
74      public static final String OPTION_USAGE = "  -Xlegstar-code      :  inject cobol binding annotation into the "
75              + "generated code";
76  
77      private static final List < String > WINDOWS_RESERVED_FILE_NAMES = Arrays
78              .asList(new String[] { "CON", "PRN", "AUX", "NUL", "COM1", "COM2",
79                      "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
80                      "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
81                      "LPT8", "LPT9" });
82  
83      private static final String RESERVED_FILE_NAME_SUFFIX = "w";
84  
85      /** {@inheritDoc} */
86      @Override
87      /** This will let XJC know that this plugin supports this argument*/
88      public String getOptionName() {
89          return OPTION_NAME;
90      }
91  
92      /** {@inheritDoc} */
93      @Override
94      /** Just in case XJC requires a friendly comment on this plugin */
95      public String getUsage() {
96          return OPTION_USAGE;
97      }
98  
99      /** {@inheritDoc} */
100     @Override
101     /** This lets XJC know what are the namespaces to watch for in the source
102      *  schema */
103     public List < String > getCustomizationURIs() {
104         return Collections.singletonList(CobolMarkup.NS);
105     }
106 
107     /** {@inheritDoc} */
108     @Override
109     /** Just to be extra sure, XJC will call us on each element from the source
110      * schema that seems to belong to the namespaces to watch for. We need to 
111      * tell XJC whether this is an actual supported customization. */
112     public boolean isCustomizationTagName(final String nsUri,
113             final String localName) {
114 
115         return (nsUri.equals(CobolMarkup.NS) && (localName
116                 .equals(CobolMarkup.ELEMENT)
117                 || localName.equals(CobolMarkup.ELEMENT_VALUE) || localName
118                 .equals(CobolMarkup.COMPLEX_TYPE)));
119     }
120 
121     /**
122      * {@inheritDoc} . Since we have no direct way of communicating with the
123      * annotator, we pass options as extra XJC command line parameters.
124      */
125     @Override
126     public int parseArgument(Options opt, String[] args, int i)
127             throws BadCommandLineException, IOException {
128         String arg = args[i];
129 
130         // NameConverters can be set only once so we do it on the first argument
131         if (arg.equals("-" + OPTION_NAME)) {
132             if (ArrayUtils.contains(args, "-eci")) {
133                 opt.setNameConverter(new EciCompatibleNameConverter(), this);
134             } else {
135                 opt.setNameConverter(new WinCompatibleNameConverter(), this);
136             }
137         }
138 
139         // Tell XJC we consumed this option which is legstar specific
140         if (arg.equals("-eci")) {
141             isEciCompatible = true;
142             return 1;
143         }
144         return 0;
145     }
146 
147     /**
148      * {@inheritDoc}
149      * <p>
150      * XJC has built an abstract in-memory model of the target classes. We are
151      * given a chance to tweak it.
152      * */
153     public void postProcessModel(Model model, ErrorHandler errorHandler) {
154         /*
155          * With ECI we need to change field names so that they match the bean
156          * getter/setter convention.
157          */
158         if (isEciCompatible()) {
159             for (Entry < NClass, CClassInfo > entry : model.beans().entrySet()) {
160                 CClassInfo classInfo = entry.getValue();
161                 List < CPropertyInfo > properties = classInfo.getProperties();
162                 for (CPropertyInfo property : properties) {
163                     String publicName = property.getName(true);
164                     String newPrivateName = Character.toLowerCase(publicName
165                             .charAt(0)) + publicName.substring(1);
166                     property.setName(false, newPrivateName);
167                 }
168             }
169         }
170     }
171 
172     /**
173      * {@inheritDoc}
174      * <p>
175      * This is where the real action takes place. XJC has done its job of
176      * building an in-memory model of the soon-to-be generated java classes. We
177      * are given a chance to change that model so that the generated classes
178      * will include the extra annotations that we need.
179      * */
180     @Override
181     public boolean run(final Outline model, final Options opt,
182             final ErrorHandler errorHandler) {
183 
184         long start = System.currentTimeMillis();
185 
186         /*
187          * Each simpleType at the root level in the source schema will become a
188          * JAXBElement in the ObjectFactory class. .
189          */
190         for (CElementInfo eo : model.getModel().getAllElements()) {
191 
192             if (_log.isDebugEnabled()) {
193                 _log.debug("CobolJAXBAnnotator::run::CElementInfo::"
194                         + eo.fullName());
195             }
196             CPluginCustomization c = eo.getCustomizations().find(
197                     CobolMarkup.NS, CobolMarkup.ELEMENT);
198             if (c == null) {
199                 continue; // no customization --- nothing to inject here
200             }
201 
202             /* Mark the annotation as acknowledged */
203             c.markAsAcknowledged();
204 
205         }
206 
207         /*
208          * Each complexType in the source schema will result in a class outline
209          * and its own implementation class.
210          */
211         for (ClassOutline co : model.getClasses()) {
212 
213             if (_log.isDebugEnabled()) {
214                 _log.debug("CobolJAXBAnnotator::run::ClassOutline::"
215                         + co.implClass);
216             }
217             annotateClass(co);
218 
219             for (FieldOutline fo : co.getDeclaredFields()) {
220 
221                 if (_log.isDebugEnabled()) {
222                     _log.debug("CobolJAXBAnnotator::run::FieldOutline::"
223                             + fo.getPropertyInfo().getName(false));
224                 }
225 
226                 /*
227                  * Get the customization depending on whether this is a direct
228                  * element or a reference to an element.Elements such as arrays
229                  * of hexBinary will result in a CReferencePropertyInfo
230                  */
231                 CPluginCustomization c = null;
232                 if (fo.getPropertyInfo() instanceof CReferencePropertyInfo) {
233                     if (_log.isDebugEnabled()) {
234                         _log.debug("FieldOutline is CReferencePropertyInfo");
235                     }
236 
237                     for (CElement ce : ((CReferencePropertyInfo) fo
238                             .getPropertyInfo()).getElements()) {
239                         c = ce.getCustomizations().find(CobolMarkup.NS,
240                                 CobolMarkup.ELEMENT);
241                     }
242                 } else {
243                     c = fo.getPropertyInfo().getCustomizations()
244                             .find(CobolMarkup.NS, CobolMarkup.ELEMENT);
245                 }
246 
247                 if (c == null) {
248                     continue; // no customization --- nothing to inject here
249                 }
250                 if (_log.isDebugEnabled()) {
251                     String javaType = fo.getRawType().name();
252                     _log.debug("CobolJAXBAnnotator::run::ClassOutline::"
253                             + c.element.getLocalName() + " type=" + javaType);
254                 }
255 
256                 c.markAsAcknowledged();
257 
258                 /* Retrieve the field identified by its private name. */
259                 JDefinedClass coClass = co.implClass;
260                 JFieldVar jf = coClass.fields().get(
261                         fo.getPropertyInfo().getName(false));
262 
263                 /* Inject a cobol annotation on this field. */
264                 JAnnotationUse ce = jf.annotate(CobolElement.class);
265                 mapAnnotations(c, ce);
266 
267                 setDefaultValue(jf, c.element);
268 
269                 /*
270                  * HexBinary items are missing a JAXB annotation that we inject
271                  * here
272                  */
273                 if (fo.getRawType().name().compareTo("byte[]") == 0) {
274                     JAnnotationUse xmlSchemaType = jf
275                             .annotate(XmlSchemaType.class);
276                     xmlSchemaType.param("name", "hexBinary");
277                 }
278 
279             }
280         }
281 
282         long end = System.currentTimeMillis();
283         if (_log.isDebugEnabled()) {
284             _log.debug("Cobol annotation success.");
285             _log.debug("Duration=" + (end - start) + " ms");
286         }
287 
288         return true;
289     }
290 
291     /**
292      * Attempts to set a default value for the java field based on the COBOL
293      * default value.
294      * <p/>
295      * Will not attempt to initialize arrays or complex types.
296      * <p/>
297      * Strings which COBOL peer defaults to low values or high values are
298      * initialized with an empty string.
299      * <p/>
300      * Leading plus signs are removed from numerics, they cause
301      * NumberFormatException.
302      * 
303      * @param jf the java field
304      * @param e the XML node holding COBOL annotations
305      */
306     protected void setDefaultValue(final JFieldVar jf, final Element e) {
307         if (!e.hasAttribute(CobolMarkup.VALUE)) {
308             return;
309         }
310         String value = e.getAttribute(CobolMarkup.VALUE).trim();
311         String type = jf.type().binaryName();
312         if (type.equals("java.lang.String")) {
313             jf.init(JExpr.lit(value));
314         } else {
315             /* Assume a numeric from now on */
316             if (value.length() == 0) {
317                 return;
318             }
319             /* Java does not like leading plus sign */
320             if (value.startsWith("+")) {
321                 value = value.substring(1);
322             }
323             if (type.equals("java.math.BigDecimal")) {
324                 jf.init(JExpr.direct("new BigDecimal(\"" + value + "\")"));
325             } else if (type.equals("java.math.BigInteger")) {
326                 jf.init(JExpr.direct("new BigInteger(\"" + value + "\")"));
327             } else if (type.equals("short")) {
328                 jf.init(JExpr.lit(Short.parseShort(value)));
329             } else if (type.equals("int")) {
330                 jf.init(JExpr.lit(Integer.parseInt(value)));
331             } else if (type.equals("long")) {
332                 jf.init(JExpr.lit(Long.parseLong(value)));
333             } else if (type.equals("float")) {
334                 jf.init(JExpr.lit(Float.parseFloat(value)));
335             } else if (type.equals("double")) {
336                 jf.init(JExpr.lit(Double.parseDouble(value)));
337             }
338         }
339 
340     }
341 
342     /**
343      * Propagate xsd complex type annotations on a class type.
344      * 
345      * @param co the class outline
346      */
347     protected void annotateClass(final ClassOutline co) {
348         CPluginCustomization c = co.target.getCustomizations().find(
349                 CobolMarkup.NS, CobolMarkup.COMPLEX_TYPE);
350         if (c == null) {
351             return; // no customization --- nothing to inject here
352         }
353         c.markAsAcknowledged();
354 
355         JAnnotationUse ce = co.implClass.annotate(CobolComplexType.class);
356         ce.param(CobolMarkup.JAVA_CLASS_NAME,
357                 c.element.getAttribute(CobolMarkup.JAVA_CLASS_NAME));
358     }
359 
360     /**
361      * Each annotation is extracted from the XML schema customization and
362      * injected back into the JAXB class code.
363      * 
364      * @param c the XML Schema annotation element
365      * @param ce the Java code Cobol annotation
366      */
367     protected void mapAnnotations(final CPluginCustomization c,
368             final JAnnotationUse ce) {
369 
370         ce.param("cobolName", c.element.getAttribute(CobolMarkup.COBOL_NAME));
371 
372         String cobolType = c.element.getAttribute(CobolMarkup.TYPE);
373 
374         ce.param("type", CobolType.valueOf(cobolType));
375 
376         setNumericParm(c.element, CobolMarkup.LEVEL_NUMBER, ce);
377         setBooleanParm(c.element, CobolMarkup.IS_JUSTIFIED_RIGHT, ce);
378         setBooleanParm(c.element, CobolMarkup.IS_SIGNED, ce);
379         setBooleanParm(c.element, CobolMarkup.IS_SIGN_LEADING, ce);
380         setBooleanParm(c.element, CobolMarkup.IS_SIGN_SEPARATE, ce);
381         setNumericParm(c.element, CobolMarkup.TOTAL_DIGITS, ce);
382         setNumericParm(c.element, CobolMarkup.FRACTION_DIGITS, ce);
383         setNumericParm(c.element, CobolMarkup.MIN_OCCURS, ce);
384         setNumericParm(c.element, CobolMarkup.MAX_OCCURS, ce);
385         setStringParm(c.element, CobolMarkup.DEPENDING_ON, ce);
386         setBooleanParm(c.element, CobolMarkup.IS_ODO_OBJECT, ce);
387         setStringParm(c.element, CobolMarkup.REDEFINES, ce);
388         setBooleanParm(c.element, CobolMarkup.IS_REDEFINED, ce);
389         setStringParm(c.element, CobolMarkup.PICTURE, ce);
390         setStringParm(c.element, CobolMarkup.USAGE, ce);
391         setStringParm(c.element, CobolMarkup.VALUE, ce);
392         setBooleanParm(c.element, CobolMarkup.IS_CUSTOM_VARIABLE, ce);
393         setStringParm(c.element, CobolMarkup.MARSHAL_CHOICE_STRATEGY, ce);
394         setStringParm(c.element, CobolMarkup.UNMARSHAL_CHOICE_STRATEGY, ce);
395         setNumericParm(c.element, CobolMarkup.SRCE_LINE, ce);
396 
397     }
398 
399     /**
400      * Move an attribute value from the XML markup to the Cobol annotation.
401      * 
402      * @param e the Node holding the XML markup
403      * @param xmlMarkup the name of the XML markup tag
404      * @param ce the target annotation recipient
405      */
406     protected void setBooleanParm(final Element e, final String xmlMarkup,
407             final JAnnotationUse ce) {
408 
409         String cobolProperty = xmlMarkup;
410         /*
411          * TODO There are some differences between the XML markup and the java
412          * annotation that need to go away in some future version.
413          */
414         if (!cobolProperty.startsWith("is")) {
415             cobolProperty = "is"
416                     + xmlMarkup.substring(0, 1)
417                             .toUpperCase(Locale.getDefault())
418                     + xmlMarkup.substring(1, xmlMarkup.length());
419         }
420         String value = e.getAttribute(xmlMarkup);
421         if (value == null || value.length() == 0) {
422             return;
423         }
424         ce.param(cobolProperty, Boolean.valueOf(value));
425     }
426 
427     /**
428      * Move an attribute value from the XML markup to the Cobol annotation.
429      * 
430      * @param e the Node holding the XML markup
431      * @param xmlMarkup the name of the XML markup tag
432      * @param ce the target annotation recipient
433      */
434     protected void setNumericParm(final Element e, final String xmlMarkup,
435             final JAnnotationUse ce) {
436 
437         String cobolProperty = xmlMarkup;
438         String value = e.getAttribute(xmlMarkup);
439         if (value == null || value.length() == 0) {
440             return;
441         }
442         ce.param(cobolProperty, Integer.valueOf(value));
443     }
444 
445     /**
446      * Move an attribute value from the XML markup to the Cobol annotation.
447      * 
448      * @param e the Node holding the XML markup
449      * @param xmlMarkup the name of the XML markup tag
450      * @param ce the target annotation recipient
451      */
452     protected void setStringParm(final Element e, final String xmlMarkup,
453             final JAnnotationUse ce) {
454 
455         String cobolProperty = xmlMarkup;
456         String value = e.getAttribute(xmlMarkup);
457         if (value == null || value.length() == 0) {
458             return;
459         }
460         ce.param(cobolProperty, value);
461     }
462 
463     /**
464      * @return true in ECI compatibility mode
465      */
466     public boolean isEciCompatible() {
467         return isEciCompatible;
468     }
469 
470     /**
471      * This overrides the standard JAXB name converter when in ECI compatible
472      * mode.
473      * <p/>
474      * ECI does not remove underscores from variable names like the standard
475      * JAXB name converter does. The code here borrows from
476      * underscoreBinding=asCharInWord JAXB option.
477      * <p/>
478      * Also ECI does not uppercase tokens following underscores like JAXB does.
479      * 
480      */
481     protected class EciCompatibleNameConverter extends
482             WinCompatibleNameConverter {
483 
484         /** Underscore is not a punctuation. */
485         @Override
486         protected boolean isPunct(char c) {
487             return (c == '.' || c == '-' || c == ';' /* || c == '_' */
488                     || c == '\u00b7' || c == '\u0387' || c == '\u06dd' || c == '\u06de');
489         }
490 
491         /** Underscore is a regular letter. */
492         @Override
493         protected boolean isLetter(char c) {
494             return super.isLetter(c) || c == '_';
495         }
496 
497         /** Underscore is a regular letter. */
498         @Override
499         protected int classify(char c0) {
500             if (c0 == '_')
501                 return OTHER_LETTER;
502             return super.classify(c0);
503         }
504 
505         /** Makes sure only the first character is uppercased when needed. */
506         @Override
507         protected String toMixedCaseName(List < String > ss, boolean startUpper) {
508             StringBuilder sb = new StringBuilder();
509             if (!ss.isEmpty()) {
510                 if (startUpper) {
511                     sb.append(Character.toUpperCase(ss.get(0).charAt(0)));
512                     sb.append(ss.get(0).substring(1));
513                 } else {
514                     sb.append(ss.get(0).toLowerCase());
515                 }
516                 for (int i = 1; i < ss.size(); i++)
517                     sb.append(ss.get(i));
518             }
519             return sb.toString();
520         }
521 
522         /** Don't uppercase systematically like JAXB does. */
523         @Override
524         public String capitalize(String s) {
525             return s;
526         }
527     }
528 
529     /**
530      * Some file names are forbidden on Windows. Since class names end up being
531      * file names, we add a suffix here for these cases.
532      * 
533      */
534     protected class WinCompatibleNameConverter extends NameConverter.Standard {
535         public String toClassName(String s) {
536             String className = super.toClassName(s);
537             return WINDOWS_RESERVED_FILE_NAMES
538                     .contains(className.toUpperCase()) ? className
539                     + RESERVED_FILE_NAME_SUFFIX : className;
540         }
541 
542     }
543 
544 }