All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.persistence.jaxb.plugins.BeanValidationPlugin Maven / Gradle / Ivy

There is a newer version: 5.0.0-B03
Show newest version
/*
 * Copyright (c) 2014, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Marcel Valovy - 2.6 - initial implementation
package org.eclipse.persistence.jaxb.plugins;

import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.annotation.XmlElement;

import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JAnnotationValue;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JExpressionImpl;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JFormatter;
import com.sun.codemodel.JStringLiteral;
import com.sun.codemodel.JType;
import com.sun.tools.xjc.BadCommandLineException;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.model.CAttributePropertyInfo;
import com.sun.tools.xjc.model.CCustomizable;
import com.sun.tools.xjc.model.CElementPropertyInfo;
import com.sun.tools.xjc.model.CPluginCustomization;
import com.sun.tools.xjc.model.CPropertyInfo;
import com.sun.tools.xjc.model.CPropertyVisitor2;
import com.sun.tools.xjc.model.CReferencePropertyInfo;
import com.sun.tools.xjc.model.CValuePropertyInfo;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.Outline;
import com.sun.xml.xsom.XSElementDecl;
import com.sun.xml.xsom.XSFacet;
import com.sun.xml.xsom.XSParticle;
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSTerm;
import com.sun.xml.xsom.XSType;
import com.sun.xml.xsom.impl.AttributeUseImpl;
import com.sun.xml.xsom.impl.ParticleImpl;
import com.sun.xml.xsom.impl.RestrictionSimpleTypeImpl;
import com.sun.xml.xsom.impl.parser.DelayedRef;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;

import static com.sun.xml.xsom.XSFacet.FACET_FRACTIONDIGITS;
import static com.sun.xml.xsom.XSFacet.FACET_LENGTH;
import static com.sun.xml.xsom.XSFacet.FACET_MAXEXCLUSIVE;
import static com.sun.xml.xsom.XSFacet.FACET_MAXINCLUSIVE;
import static com.sun.xml.xsom.XSFacet.FACET_MAXLENGTH;
import static com.sun.xml.xsom.XSFacet.FACET_MINEXCLUSIVE;
import static com.sun.xml.xsom.XSFacet.FACET_MININCLUSIVE;
import static com.sun.xml.xsom.XSFacet.FACET_MINLENGTH;
import static com.sun.xml.xsom.XSFacet.FACET_PATTERN;
import static com.sun.xml.xsom.XSFacet.FACET_TOTALDIGITS;

/**
 * XJC Plugin for generation of JSR349 (Bean Validation) annotations.
 * 

* Has two mods: *

 *  "jsr303" - enables backward compatibility.
 *  "simpleRegex" - disables translation of UNICODE XML regex into UNICODE Java regex.
 *  Java ASCII regex are shorter to read.
 * 
* Is capable of generating the following annotations: *
 *  - @DecimalMax
 *  - @DecimalMin
 *  - @Digits
 *  - @NotNull
 *  - @Pattern
 *  - @Size
 *  - @Valid
 *  - @AssertTrue
 *  - @AssertFalse
 *  - @Future
 *  - @Past
 * 
* Reacts to the following XSD restrictions and facets: *
 *  - maxExclusive
 *  - minExclusive
 *  - maxInclusive
 *  - minInclusive
 *  - nillable
 *  - pattern
 *  - length
 *  - maxLength
 *  - minLength
 *  - minOccurs
 *  - maxOccurs
 * 
* Basic usage: *
 *  {@code xjc file.xsd -XBeanVal}
 * 
* Example usage with mods: *
 *  {@code xjc file.xsd -XBeanVal jsr303 simpleRegex}
 * 
* Programmatic usage, with mods and extensions enabled: *
 *  Driver.run(new String[] { schemaPath, "-extension", "-XBeanVal", "jsr303", "simpleRegex" }, System.out,
 *  System.out);
 * 
* Supports setting Target groups and Error message through binding customizations. Example: *
 * {@code 
 *   
 *    
 *    
 *   
 *  }
 * 
*

* Supports custom-created BV annotations. Example: *

 * {@code 
 *   
 *    
 *   
 *  }
 * 
* * @author Marcel Valovy - [email protected] */ @java.lang.SuppressWarnings("squid:S1191") public class BeanValidationPlugin extends Plugin { /* ######### LAUNCHING ######### */ public static final String PLUGIN_OPTION = "XBeanVal"; public static final String JSR_303_MOD = "jsr303"; public static final String SIMPLE_REGEX_MOD = "simpleRegex"; public static final String NS_URI = "http://jaxb.dev.java.net/plugin/bean-validation"; public static final String FACET = "facet"; private static final String VALUE = "value"; private boolean jsr303 = false; private boolean simpleRegex = false; @Override public String getOptionName() { return PLUGIN_OPTION; } @Override public String getUsage() { return " -" + PLUGIN_OPTION + " : convert xsd restrictions to" + " javax.validation annotations. Usage with mods: -" + PLUGIN_OPTION + " " + JSR_303_MOD + " " + SIMPLE_REGEX_MOD; } @Override public List getCustomizationURIs() { return Collections.singletonList(NS_URI); } @Override public boolean isCustomizationTagName(String nsUri, String localName) { return nsUri.equals(NS_URI) && localName.equals(FACET); } @Override public int parseArgument(Options opt, String[] args, int i) throws BadCommandLineException, IOException { int mods = 0; int argNumber = i; if (("-" + PLUGIN_OPTION).equals(args[i])) { while (++argNumber < args.length) { if (args[argNumber].contains(JSR_303_MOD)) { jsr303 = true; mods++; } else if (args[argNumber].contains(SIMPLE_REGEX_MOD)) { simpleRegex = true; mods++; } } return 1 + mods; } return 0; } /* ######### CORE FUNCTIONALITY ######### */ private static final String PATTERN_ANNOTATION_NOT_APPLICABLE = "Facet \"pattern\" was detected on a DOM node with non-string base type. Annotation was not generated, because it is not supported by the Bean Validation specification."; private static final JClass ANNOTATION_VALID; private static final JClass ANNOTATION_NOTNULL; private static final JClass ANNOTATION_SIZE; private static final JClass ANNOTATION_DECIMALMIN; private static final JClass ANNOTATION_DECIMALMAX; private static final JClass ANNOTATION_DIGITS; private static final JClass ANNOTATION_PATTERN; private static final JClass ANNOTATION_PATTERNLIST; private static final JClass ANNOTATION_ASSERTFALSE; private static final JClass ANNOTATION_ASSERTTRUE; private static final JClass ANNOTATION_FUTURE; private static final JClass ANNOTATION_PAST; private static final JClass ANNOTATION_XMLELEMENT; // We want this plugin to work without requiring the presence of JSR-303/349 jar. private static final JCodeModel CODEMODEL = new JCodeModel(); static { ANNOTATION_VALID = CODEMODEL.ref("javax.validation.Valid"); ANNOTATION_NOTNULL = CODEMODEL.ref("javax.validation.constraints.NotNull"); ANNOTATION_SIZE = CODEMODEL.ref("javax.validation.constraints.Size"); ANNOTATION_DECIMALMIN = CODEMODEL.ref("javax.validation.constraints.DecimalMin"); ANNOTATION_DECIMALMAX = CODEMODEL.ref("javax.validation.constraints.DecimalMax"); ANNOTATION_DIGITS = CODEMODEL.ref("javax.validation.constraints.Digits"); ANNOTATION_PATTERN = CODEMODEL.ref("javax.validation.constraints.Pattern"); ANNOTATION_PATTERNLIST = CODEMODEL.ref("javax.validation.constraints.Pattern.List"); ANNOTATION_ASSERTFALSE = CODEMODEL.ref("javax.validation.constraints.AssertFalse"); ANNOTATION_ASSERTTRUE = CODEMODEL.ref("javax.validation.constraints.AssertTrue"); ANNOTATION_FUTURE = CODEMODEL.ref("javax.validation.constraints.Future"); ANNOTATION_PAST = CODEMODEL.ref("javax.validation.constraints.Past"); ANNOTATION_XMLELEMENT = CODEMODEL.ref("javax.xml.bind.annotation.XmlElement"); } @Override public boolean run(Outline outline, Options opts, ErrorHandler errorHandler) { final Visitor visitor = this.new Visitor(); for (ClassOutline classOutline : outline.getClasses()) { for (CPropertyInfo property : classOutline.target.getProperties()) { property.accept(visitor, classOutline); } } return true; } /** * Processes an xsd value in form of xsd attribute from extended base. *

* Example: * * * <- xsd extension base * * * * *

* * * <- This is a special field that is added to the generated class, called "value" (corresponds to the valuePropertyName), * it gets processed by this method and the "value" field receives @Size(min = 1, max = 5). * * */ private void processValueFromExtendedBase(CValuePropertyInfo valueProperty, ClassOutline classOutline, List customizations) { String valuePropertyName = valueProperty.getName(false); JFieldVar fieldVar = classOutline.implClass.fields().get(valuePropertyName); XSSimpleType type = ((RestrictionSimpleTypeImpl) valueProperty.getSchemaComponent()).asSimpleType(); processSimpleType(null, type, fieldVar, customizations); } /** * Processes an xsd attribute. *

* Example: * * * * << "id" is the attributePropertyName * * * */ private void processAttribute(CAttributePropertyInfo attributeProperty, ClassOutline classOutline, List customizations) { String attributePropertyName = attributeProperty.getName(false); JFieldVar fieldVar = classOutline.implClass.fields().get(attributePropertyName); AttributeUseImpl attribute = (AttributeUseImpl) attributeProperty.getSchemaComponent(); XSSimpleType type = attribute.getDecl().getType(); // Use="required". It makes sense to annotate a required attribute with @NotNull even though it's not 100 % semantically equivalent. if (attribute.isRequired() && !fieldVar.type().isPrimitive()) { notNullAnnotate(fieldVar); } processSimpleType(null, type, fieldVar, customizations); } /** * Processes an xsd element. *

* Example: * */ private void processElement(CElementPropertyInfo propertyInfo, ClassOutline co, List customizations) { XSParticle particle = (XSParticle) propertyInfo.getSchemaComponent(); JFieldVar fieldVar = co.implClass.fields().get(propertyInfo.getName(false)); processMinMaxOccurs(particle, fieldVar); XSTerm term = particle.getTerm(); if (term instanceof XSElementDecl) { processTermElement(particle, fieldVar, (XSElementDecl) term, customizations); // When a complex type resides inside another complex type and thus gets lazily loaded or processed. } else if (term instanceof DelayedRef.Element) { processTermElement(particle, fieldVar, ((DelayedRef.Element) term).get(), customizations); } } private void processTermElement(XSParticle particle, JFieldVar fieldVar, XSElementDecl element, List customizations) { final int minOccurs = getOccursValue("minOccurs", particle); XSType elementType = element.getType(); if (elementType.isComplexType()) { validAnnotate(fieldVar); if (!element.isNillable() && minOccurs > 0) { notNullAnnotate(fieldVar); } if (elementType.getBaseType().isSimpleType()) { processSimpleType(particle, elementType.getBaseType().asSimpleType(), fieldVar, customizations); } } else { processSimpleType(particle, elementType.asSimpleType(), fieldVar, customizations); } } private void processSimpleType(XSParticle particle, XSSimpleType simpleType, JFieldVar fieldVar, List customizations) { Map annotationsAndTheirOrigin = new HashMap(); applyAnnotations(particle, simpleType, fieldVar, annotationsAndTheirOrigin); applyCustomizations(fieldVar, customizations, annotationsAndTheirOrigin); } /* * Reads supported facets from the simpleType. * Converts them to corresponding bean validation annotations. * Annotations are applied on the fieldVar if it isn't yet annotated by them. * Why? If there is something else (e.g. plugin) which generates BV annotations, * we mustn't interfere with it. Two annotations of the same kind on a Java field * do not compile. * Stores the applied annotations and their origin into the map arg. */ private void applyAnnotations(XSParticle particle, XSSimpleType simpleType, JFieldVar fieldVar, Map a) { XSFacet facet = null; // Auxiliary field. JType fieldType = fieldVar.type(); if (notAnnotated(fieldVar, ANNOTATION_SIZE) && isSizeAnnotationApplicable(fieldType)) { try { if ((facet = simpleType.getFacet(FACET_LENGTH)) != null) { int length = Integer.parseInt(facet.getValue().value); a.put(fieldVar.annotate(ANNOTATION_SIZE).param("min", length).param("max", length), FacetType.length); } else { Integer minLength = (facet = simpleType.getFacet(FACET_MINLENGTH)) != null ? Integer.parseInt(facet.getValue().value) : null; Integer maxLength = (facet = simpleType.getFacet(FACET_MAXLENGTH)) != null ? Integer.parseInt(facet.getValue().value) : null; // Note: If using both minLength + maxLength, the minLength's customizations are considered. if (minLength != null && maxLength != null) { a.put(fieldVar.annotate(ANNOTATION_SIZE).param("min", minLength).param("max", maxLength), FacetType.minLength); } else if (minLength != null) { a.put(fieldVar.annotate(ANNOTATION_SIZE).param("min", minLength), FacetType.minLength); } else if (maxLength != null) { a.put(fieldVar.annotate(ANNOTATION_SIZE).param("max", maxLength), FacetType.maxLength); } } } catch (NumberFormatException nfe) { if (facet != null) { String msg = "'" + facet.getName() + "' in '" + simpleType.getName() + "' cannot be parsed."; throw new RuntimeException(new SAXParseException(msg, facet.getLocator(), nfe)); } } } if ((facet = simpleType.getFacet(FACET_MAXINCLUSIVE)) != null && isNumberOrCharSequence(fieldType, false)) { String maxIncValue = facet.getValue().value; if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMAX, maxIncValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMAX).param(VALUE, maxIncValue), FacetType.maxInclusive); convertToElement(particle, fieldVar); } } if ((facet = simpleType.getFacet(FACET_MININCLUSIVE)) != null && isNumberOrCharSequence(fieldType, false)) { String minIncValue = facet.getValue().value; if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMIN, minIncValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMIN).param(VALUE, minIncValue), FacetType.minInclusive); convertToElement(particle, fieldVar); } } if ((facet = simpleType.getFacet(FACET_MAXEXCLUSIVE)) != null && isNumberOrCharSequence(fieldType, false)) { String maxExcValue = facet.getValue().value; if (!jsr303) { // ~ if jsr349 if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMAX, maxExcValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMAX).param(VALUE, maxExcValue).param("inclusive", false), FacetType.maxExclusive); convertToElement(particle, fieldVar); } } else { Integer intMaxExc = Integer.parseInt(maxExcValue) - 1; maxExcValue = intMaxExc.toString(); if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMAX, maxExcValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMAX).param(VALUE, maxExcValue), FacetType.maxExclusive); convertToElement(particle, fieldVar); } } } if ((facet = simpleType.getFacet(FACET_MINEXCLUSIVE)) != null && isNumberOrCharSequence(fieldType, false)) { String minExcValue = facet.getValue().value; if (!jsr303) { // ~ if jsr349 if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMIN, minExcValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMIN).param(VALUE, minExcValue).param("inclusive", false), FacetType.minExclusive); convertToElement(particle, fieldVar); } else { Integer intMinExc = Integer.parseInt(minExcValue) + 1; minExcValue = intMinExc.toString(); if (notAnnotatedAndNotDefaultBoundary(fieldVar, ANNOTATION_DECIMALMIN, minExcValue)) { a.put(fieldVar.annotate(ANNOTATION_DECIMALMIN).param(VALUE, minExcValue), FacetType.minExclusive); convertToElement(particle, fieldVar); } } } } if ((facet = simpleType.getFacet(FACET_TOTALDIGITS)) != null && isNumberOrCharSequence(fieldType, true)) { Integer digits = Integer.valueOf(facet.getValue().value); if (digits != null) { XSFacet fractionDigits = simpleType.getFacet(FACET_FRACTIONDIGITS); int fractionDigs = 0; if (fractionDigits != null) { try { fractionDigs = Integer.parseInt(fractionDigits.getValue().value); } catch (NumberFormatException nfe) { fractionDigs = 0; } } if (notAnnotated(fieldVar, ANNOTATION_DIGITS)) { a.put(fieldVar.annotate(ANNOTATION_DIGITS).param("integer", (digits - fractionDigs)).param("fraction", fractionDigs), FacetType.totalDigits); } } } List patternList = simpleType.getFacets(FACET_PATTERN); if (patternList.size() > 1) { if (notAnnotated(fieldVar, ANNOTATION_PATTERNLIST)) { JAnnotationUse list = fieldVar.annotate(ANNOTATION_PATTERNLIST); JAnnotationArrayMember listValue = list.paramArray(VALUE); for (XSFacet xsFacet : patternList) // If corresponds to . if ("String".equals(fieldType.name())) { a.put(listValue.annotate(ANNOTATION_PATTERN).param("regexp", eliminateShorthands(xsFacet.getValue().value)), FacetType.pattern); } else { Logger.getLogger(this.getClass().getName()).log(Level.WARNING, PATTERN_ANNOTATION_NOT_APPLICABLE); } } } else if ((facet = simpleType.getFacet(FACET_PATTERN)) != null) { if ("String".equals(fieldType.name())) { // if (notAnnotated(fieldVar, ANNOTATION_PATTERN)) { a.put(fieldVar.annotate(ANNOTATION_PATTERN).param("regexp", eliminateShorthands(facet.getValue().value)), FacetType.pattern); } } else { Logger.getLogger(this.getClass().getName()).log(Level.WARNING, PATTERN_ANNOTATION_NOT_APPLICABLE); } } } /** * Implements the GoF Visitor pattern. */ private final class Visitor implements CPropertyVisitor2 { @Override public Void visit(CElementPropertyInfo t, ClassOutline p) { processElement(t, p, detectCustomizations(t)); return null; } @Override public Void visit(CAttributePropertyInfo t, ClassOutline p) { processAttribute(t, p, detectCustomizations(t)); return null; } @Override public Void visit(CValuePropertyInfo t, ClassOutline p) { processValueFromExtendedBase(t, p, detectCustomizations(t)); return null; } @Override public Void visit(CReferencePropertyInfo t, ClassOutline p) { return null; } /* * Scans the input DOM node for custom bindings. * If found, converts them into {@link FacetCustomization} objects * and returns them. */ private List detectCustomizations(CCustomizable ca) { List facetCustomizations = new ArrayList(); List pluginCustomizations = ca.getCustomizations(); if (pluginCustomizations != null) for (CPluginCustomization c : pluginCustomizations) { c.markAsAcknowledged(); String groups = c.element.getAttribute("groups"); String message = c.element.getAttribute("message"); String type = c.element.getAttribute("type"); if ("".equals(type)) { throw new RuntimeException("DOM attribute \"type\" is required in custom facet declarations."); } String value = c.element.getAttribute(VALUE); facetCustomizations.add(new FacetCustomization(groups, message, type, value)); } return facetCustomizations; } } /* ######### CUSTOMIZATIONS FUNCTIONALITY ######### */ /* * Applies the input customizations on the fieldVar: * - Detects custom facets and applies them. * - Matches standard facets with corresponding facet customizations. */ private void applyCustomizations(JFieldVar fieldVar, List customizations, Map a) { for (FacetCustomization c : customizations) { // Programming by exception is a bad programming practice, however // it is the best available solution here. Catching IAE means that // we have encountered a custom facet. try { switch (FacetType.valueOf(c.type)) { case assertFalse: customizeAnnotation(fieldVar.annotate(ANNOTATION_ASSERTFALSE), c); continue; case assertTrue: customizeAnnotation(fieldVar.annotate(ANNOTATION_ASSERTTRUE), c); continue; case future: customizeAnnotation(fieldVar.annotate(ANNOTATION_FUTURE), c); continue; case past: customizeAnnotation(fieldVar.annotate(ANNOTATION_PAST), c); continue; } } catch (IllegalArgumentException programmingByException) { JAnnotationUse annotationUse = fieldVar.annotate(CODEMODEL.ref(c.type)); if (!c.value.equals("")) annotationUse.param(VALUE, c.value); customizeAnnotation(annotationUse, c); continue; } customizeRegularAnnotations(a, c); } } /** * Reads attributes "groups" and "message" from facet customization * and if they don't contain empty values, applies them to annotation use * as a parameter. */ private void customizeAnnotation(JAnnotationUse a, final FacetCustomization c) { /** * Transposes an array of Strings into a single String containing * the Strings, separated by comma + space (i.e. ", ") and with ".class" * extension. */ final class GroupsParser extends JExpressionImpl { @Override public void generate(JFormatter f) { if (c.groups.length == 1) f.p(c.groups[0] + ".class"); else { StringBuilder b = new StringBuilder(c.groups.length * 64); b.append('{'); int i = 0; for (; i < c.groups.length - 1; i++) b.append(c.groups[i]).append(".class, "); b.append(c.groups[i]).append(".class}"); f.p(b.toString()); } } } if (c.groups != null && c.groups.length != 0) a.param("groups", new GroupsParser()); if (!c.message.equals("")) a.param("message", c.message); } /* Note: * Keep in mind that the only facet that has maxOccurs > 1 is xs:pattern. * If multiple patterns are present on an element, they all must share the * same validation group, because they all are included in the XML * Validation process. */ private void customizeRegularAnnotations(Map annotations, FacetCustomization c) { for (Map.Entry e : annotations.entrySet()) if (FacetType.valueOf(c.type) == e.getValue()) customizeAnnotation(e.getKey(), c); } /** * Value Object for Facet Customizations and Custom Facets. */ private static final class FacetCustomization { private final String[]/*@NotEmpty*/groups; private final String message; private final String type; private final String value; private FacetCustomization(String groups, String message, String type, String value) { this.groups = groups.isEmpty() ? null : groups(groups); this.message = message; this.type = type; this.value = value; } private String[] groups(String groups) { return ClassNameTrimmer.trim(groups).split(","); } private static final class ClassNameTrimmer { private static final Pattern ws = Pattern.compile("[\\u0009-\\u000D\\u0020\\u0085\\u00A0\\u1680\\u180E\\" + "u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+"); private static String trim(String s) { return ws.matcher(s).replaceAll("").replace('$', '.'); } } } private static enum FacetType { // XML Facets + Restrictions. maxExclusive, minExclusive, maxInclusive, minInclusive, nillable, pattern, length, maxLength, minLength, minOccurs, maxOccurs, totalDigits, fractionDigits, // Custom facets. assertFalse, assertTrue, future, past } /* ######### AUXILIARY FUNCTIONALITY ######### */ /** * Processes minOccurs and maxOccurs attributes of XS Element. * If the values are not default, the property will be annotated with @Size. */ private void processMinMaxOccurs(XSParticle particle, JFieldVar fieldVar) { final int maxOccurs = getOccursValue("maxOccurs", particle); final int minOccurs = getOccursValue("minOccurs", particle); if (maxOccurs > 1) { if (notAnnotated(fieldVar, ANNOTATION_SIZE)) fieldVar.annotate(ANNOTATION_SIZE).param("min", minOccurs).param("max", maxOccurs); } else if (maxOccurs == -1) // maxOccurs -1 = "unbounded" if (notAnnotated(fieldVar, ANNOTATION_SIZE)) fieldVar.annotate(ANNOTATION_SIZE).param("min", minOccurs); } /** * Annotates the field with @XmlElement. Without this functionality, the * field wouldn't be recognized by Schemagen. @XmlElement annotation may * also trigger change of the field's type from primitive to object. */ private void convertToElement(XSParticle particle, JFieldVar fieldVar) { if (notAnnotated(fieldVar, ANNOTATION_XMLELEMENT)) { fieldVar.annotate(XmlElement.class); if (particle != null && getOccursValue("minOccurs", particle) > 0) { notNullAnnotate(fieldVar); } } } private void validAnnotate(JFieldVar fieldVar) { if (notAnnotated(fieldVar, ANNOTATION_VALID)) fieldVar.annotate(ANNOTATION_VALID); } private void notNullAnnotate(JFieldVar fieldVar) { if (notAnnotated(fieldVar, ANNOTATION_NOTNULL)) fieldVar.annotate(ANNOTATION_NOTNULL); } private boolean notAnnotated(JFieldVar fieldVar, JClass annotationClass) { for (JAnnotationUse annotationUse : fieldVar.annotations()) if ((annotationUse.getAnnotationClass().toString().equals(annotationClass.toString()))) return false; return true; } /** * Checks if the desired annotation is not a boundary of a primitive type and * Checks if the fieldVar is already annotated with the desired annotation. * * @return true if the fieldVar should be annotated, false if the fieldVar is * already annotated or the value is a default boundary. */ private boolean notAnnotatedAndNotDefaultBoundary(JFieldVar fieldVar, JClass annotationClass, String boundaryValue) { if (isDefaultBoundary(fieldVar.type().name(), annotationClass.fullName(), boundaryValue)) return false; for (JAnnotationUse annotationUse : fieldVar.annotations()) { if ((annotationUse.getAnnotationClass().toString().equals(annotationClass.toString()))) { boolean previousAnnotationRemoved = false; String annotationName = annotationUse.getAnnotationClass().fullName(); if (annotationName.equals(ANNOTATION_DECIMALMIN.fullName())) previousAnnotationRemoved = isMoreSpecificBoundary(fieldVar, boundaryValue, annotationUse, false); else if (annotationName.equals(ANNOTATION_DECIMALMAX.fullName())) previousAnnotationRemoved = isMoreSpecificBoundary(fieldVar, boundaryValue, annotationUse, true); // If the previous field's annotation was removed, the fieldVar // now is not annotated and should be given a new annotation, // i.e. return true. return previousAnnotationRemoved; } } return true; } /** * @param xorComplement - flips the result of compareTo (set to true for decimalMaxAnn and false for decimalMinAnn). */ private boolean isMoreSpecificBoundary(JFieldVar fieldVar, String boundaryValue, JAnnotationUse annotationUse, boolean xorComplement) { String existingBoundaryValue = getExistingBoundaryValue(annotationUse); if (existingBoundaryValue == null) return true; else if (Long.valueOf(boundaryValue).compareTo(Long.valueOf(existingBoundaryValue)) > 0 ^ xorComplement) return fieldVar.removeAnnotation(annotationUse); return false; } private boolean isSizeAnnotationApplicable(JType jType) { if (jType.isArray()) return true; Class clazz = loadClass(jType.fullName()); return clazz != null && (CharSequence.class.isAssignableFrom(clazz) || Collection.class.isAssignableFrom(clazz)); } /* ######### GENERAL UTILITIES ######### */ private Class loadClass(String className) { Class clazz = null; try { clazz = PrivilegedAccessHelper.callDoPrivilegedWithException( () -> Class.forName(className) ); } catch (ClassNotFoundException ignored) { /* - ClassNotFoundException for us "means" that the fieldVar is of some unknown class - not an issue to be solved by this plugin. */ } catch (Exception ex) { throw new RuntimeException(String.format("Failed loading of %s class", className), ex); } return clazz; } private int getOccursValue(final String attributeName, final XSParticle xsParticle) { return PrivilegedAccessHelper.callDoPrivileged( () -> loadOccursValue(attributeName, xsParticle).intValue() ); } private String getExistingBoundaryValue(final JAnnotationUse jAnnotationUse) { return PrivilegedAccessHelper.callDoPrivileged( () -> loadExistingBoundaryValue(jAnnotationUse) ); } private String eliminateShorthands(String regex) { return regexMutator.mutate(regex); } private final RegexMutator regexMutator = this.new RegexMutator(); /** * Provides means for maintaining compatibility between XML Schema regex and Java Pattern regex. * Replaces Java regex shorthands, which support only ASCII encoding, with full UNICODE equivalent. *

* Also replaces the two special XML regex character sets which aren't supported in Java, \i and \c. *

* Replaced shorthands and their negations: *

     * \i - Matches any character that may be the first character of an XML name.
     * \c - Matches any character that may occur after the first character in an XML name.
     * \d - All digits.
     * \w - Word character.
     * \s - Whitespace character.
     * \b, \B - Boundary definitions.
     * \h - Horizontal whitespace character - Java does not support, changed in Java 8 though.
     * \v - Vertical whitespace character - Java translates the shorthand to \cK only, meaning changed in Java 8 though.
     * \X - Extended grapheme cluster.
     * \R - Carriage return.
     * 
*

* Changes to this class should also be reflected in the opposite * {@link org.eclipse.persistence.jaxb.compiler.SchemaGenerator.RegexMutator} class within SchemaGen. * * @see tchrist's work * @see Special shorthands in XML Schema. */ private final class RegexMutator { private final Map shorthandReplacements = simpleRegex ? new LinkedHashMap(8) {{ put(Pattern.compile("\\\\i"), "[_:A-Za-z]"); put(Pattern.compile("\\\\I"), "[^:A-Z_a-z]"); put(Pattern.compile("\\\\c"), "[-.0-9:A-Z_a-z]"); put(Pattern.compile("\\\\C"), "[^-.0-9:A-Z_a-z]"); }} : new LinkedHashMap(32) {{ put(Pattern.compile("\\\\i"), "[:A-Z_a-z\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD]"); put(Pattern.compile("\\\\I"), "[^:A-Z_a-z\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD]"); put(Pattern.compile("\\\\c"), "[-.0-9:A-Z_a-z\\\\u00B7\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u203F\\\\u2040\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD]"); put(Pattern.compile("\\\\C"), "[^-.0-9:A-Z_a-z\\\\u00B7\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u203F\\\\u2040\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD]"); put(Pattern.compile("\\\\s"), "[\\\\u0009-\\\\u000D\\\\u0020\\\\u0085\\\\u00A0\\\\u1680\\\\u180E\\\\u2000-\\\\u200A\\\\u2028\\\\u2029\\\\u202F\\\\u205F\\\\u3000]"); put(Pattern.compile("\\\\S"), "[^\\\\u0009-\\\\u000D\\\\u0020\\\\u0085\\\\u00A0\\\\u1680\\\\u180E\\\\u2000-\\\\u200A\\\\u2028\\\\u2029\\\\u202F\\\\u205F\\\\u3000]"); put(Pattern.compile("\\\\v"), "[\\\\u000A-\\\\u000D\\\\u0085\\\\u2028\\\\u2029]"); put(Pattern.compile("\\\\V"), "[^\\\\u000A-\\\\u000D\\\\u0085\\\\u2028\\\\u2029]"); put(Pattern.compile("\\\\h"), "[\\\\u0009\\\\u0020\\\\u00A0\\\\u1680\\\\u180E\\\\u2000-\\\\u200A\\\\u202F\\\\u205F\\\\u3000]"); put(Pattern.compile("\\\\H"), "[^\\\\u0009\\\\u0020\\\\u00A0\\\\u1680\\\\u180E\\\\u2000\\\\u2001-\\\\u200A\\\\u202F\\\\u205F\\\\u3000]"); put(Pattern.compile("\\\\w"), "[\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]]"); put(Pattern.compile("\\\\W"), "[^\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]]"); put(Pattern.compile("\\\\b"), "(?:(?<=[\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]])(?![\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]])|(?\\\\u000D\\\\u000A)|[\\\\u000A\\\\u000B\\\\u000C\\\\u000D\\\\u0085\\\\u2028\\\\u2029])"); put(Pattern.compile("\\\\X"), "(?:(?:\\\\u000D\\\\u000A)|(?:[\\\\u0E40\\\\u0E41\\\\u0E42\\\\u0E43\\\\u0E44\\\\u0EC0\\\\u0EC1\\\\u0EC2\\\\u0EC3\\\\u0EC4\\\\uAAB5\\\\uAAB6\\\\uAAB9\\\\uAABB\\\\uAABC]*(?:[\\\\u1100-\\\\u115F\\\\uA960-\\\\uA97C]+|([\\\\u1100-\\\\u115F\\\\uA960-\\\\uA97C]*((?:[[\\\\u1160-\\\\u11A2\\\\uD7B0-\\\\uD7C6][\\\\uAC00\\\\uAC1C\\\\uAC38]][\\\\u1160-\\\\u11A2\\\\uD7B0-\\\\uD7C6]*|[\\\\uAC01\\\\uAC02\\\\uAC03\\\\uAC04])[\\\\u11A8-\\\\u11F9\\\\uD7CB-\\\\uD7FB]*))|[\\\\u11A8-\\\\u11F9\\\\uD7CB-\\\\uD7FB]+|[^[\\\\p{Zl}\\\\p{Zp}\\\\p{Cc}\\\\p{Cf}&&[^\\\\u000D\\\\u000A\\\\u200C\\\\u200D]]\\\\u000D\\\\u000A])[[\\\\p{Mn}\\\\p{Me}\\\\u200C\\\\u200D\\\\u0488\\\\u0489\\\\u20DD\\\\u20DE\\\\u20DF\\\\u20E0\\\\u20E2\\\\u20E3\\\\u20E4\\\\uA670\\\\uA671\\\\uA672\\\\uFF9E\\\\uFF9F][\\\\p{Mc}\\\\u0E30\\\\u0E32\\\\u0E33\\\\u0E45\\\\u0EB0\\\\u0EB2\\\\u0EB3]]*)|(?s:.))"); }}; /** * @param xmlRegex XML regex * @return Java regex */ private String mutate(String xmlRegex) { for (Map.Entry entry : shorthandReplacements.entrySet()) { Matcher m = entry.getKey().matcher(xmlRegex); xmlRegex = m.replaceAll(entry.getValue()); } return xmlRegex; } } private boolean isDefaultBoundary(String fieldVarType, String annotationClass, String boundaryValue) { return ANNOTATION_DECIMALMIN.fullName().equals(annotationClass) && nonFloatingDigitsClassesBoundaries.get(fieldVarType).min.equals(boundaryValue) || (ANNOTATION_DECIMALMAX.fullName().equals(annotationClass) && nonFloatingDigitsClassesBoundaries.get(fieldVarType).max.equals(boundaryValue)); } private boolean isNumberOrCharSequence(JType jType, boolean supportsFloating) { String shortClazzName = jType.name(); if (nonFloatingDigitsClasses.contains(shortClazzName)) return true; if (supportsFloating && floatingDigitsClasses.contains(shortClazzName)) return true; Class clazz = loadClass(jType.fullName()); return clazz != null && CharSequence.class.isAssignableFrom(clazz); } private static final Set nonFloatingDigitsClasses; static { Set set = new HashSet(); set.add("byte"); set.add("Byte"); set.add("short"); set.add("Short"); set.add("int"); set.add("Integer"); set.add("long"); set.add("Long"); set.add("BigDecimal"); set.add("BigInteger"); nonFloatingDigitsClasses = Collections.unmodifiableSet(set); } private static final Set floatingDigitsClasses; static { Set set = new HashSet(); set.add("float"); set.add("Float"); set.add("double"); set.add("Double"); floatingDigitsClasses = Collections.unmodifiableSet(new HashSet(set)); } private static final Map nonFloatingDigitsClassesBoundaries; static { HashMap map = new HashMap(); map.put("byte", new MinMaxTuple(Byte.MIN_VALUE, Byte.MAX_VALUE)); map.put("Byte", new MinMaxTuple(Byte.MIN_VALUE, Byte.MAX_VALUE)); map.put("short", new MinMaxTuple(Short.MIN_VALUE, Short.MAX_VALUE)); map.put("Short", new MinMaxTuple(Short.MIN_VALUE, Short.MAX_VALUE)); map.put("int", new MinMaxTuple(Integer.MIN_VALUE, Integer.MAX_VALUE)); map.put("Integer", new MinMaxTuple(Integer.MIN_VALUE, Integer.MAX_VALUE)); map.put("long", new MinMaxTuple(Long.MIN_VALUE, Long.MAX_VALUE)); map.put("Long", new MinMaxTuple(Long.MIN_VALUE, Long.MAX_VALUE)); nonFloatingDigitsClassesBoundaries = Collections.unmodifiableMap(map); } private static final class MinMaxTuple { private final String min; private final String max; private MinMaxTuple(T min, T max) { this.min = String.valueOf(min); this.max = String.valueOf(max); } } private static BigInteger loadOccursValue(String fieldName, XSParticle xsParticle) { try { Field field = ParticleImpl.class.getDeclaredField(fieldName); field.setAccessible(true); return ((BigInteger) field.get(xsParticle)); } catch (Exception e) { // Nothing we can do, the user should be notified that his app is unable to // execute this plugin correctly and not should not receive generated default values. throw new RuntimeException(e); } } private static String loadExistingBoundaryValue(JAnnotationUse jAnnotationUse) { JAnnotationValue jAnnotationValue = jAnnotationUse.getAnnotationMembers().get(VALUE); Class clazz = jAnnotationValue.getClass(); try { Field theValueField = clazz.getDeclaredField(VALUE); theValueField.setAccessible(true); return ((JStringLiteral) theValueField.get(jAnnotationValue)).str; } catch (Exception e) { // Nothing we can do, user should be notified that his app is unable to // execute this plugin correctly and not should not receive generated default values. throw new RuntimeException(e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy