blob: 505670840f0bc78d30fac1425d8574a2054c008b [file] [log] [blame]
/*
* 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.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 jakarta.xml.bind.annotation.XmlElement;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
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.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.XSAttributeUse;
import com.sun.xml.xsom.XSElementDecl;
import com.sun.xml.xsom.XSFacet;
import com.sun.xml.xsom.XSParticle;
import com.sun.xml.xsom.XSRestrictionSimpleType;
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSTerm;
import com.sun.xml.xsom.XSType;
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.
* <p>
* Has two mods:
* <blockquote><pre>
* "jsr303" - enables backward compatibility.
* "simpleRegex" - disables translation of UNICODE XML regex into UNICODE Java regex.
* Java ASCII regex are shorter to read.
* </pre></blockquote>
* Is capable of generating the following annotations:
* <blockquote><pre>
* - @DecimalMax
* - @DecimalMin
* - @Digits
* - @NotNull
* - @Pattern
* - @Size
* - @Valid
* - @AssertTrue
* - @AssertFalse
* - @Future
* - @Past
* </pre></blockquote>
* Reacts to the following XSD restrictions and facets:
* <blockquote><pre>
* - maxExclusive
* - minExclusive
* - maxInclusive
* - minInclusive
* - nillable
* - pattern
* - length
* - maxLength
* - minLength
* - minOccurs
* - maxOccurs
* </pre></blockquote>
* Basic usage:
* <blockquote><pre>
* {@code xjc file.xsd -XBeanVal}
* </pre></blockquote>
* Example usage with mods:
* <blockquote><pre>
* {@code xjc file.xsd -XBeanVal jsr303 simpleRegex}
* </pre></blockquote>
* Programmatic usage, with mods and extensions enabled:
* <blockquote><pre>
* Driver.run(new String[] { schemaPath, "-extension", "-XBeanVal", "jsr303", "simpleRegex" }, System.out,
* System.out);
* </pre></blockquote>
* Supports setting Target groups and Error message through binding customizations. Example:
* <blockquote><pre>
* {@code <xs:appinfo>
* <jxb:bindings node="/xs:schema/xs:complexType/xs:sequence/xs:element[@name='generic']">
* <bv:facet type="maxLength" message="Hello, world!" groups="Object"/>
* <bv:facet type="future" message="Welcome to the Future!"/>
* </jxb:bindings>
* </xs:appinfo>}
* </pre></blockquote>
* <p>
* Supports custom-created BV annotations. Example:
* <blockquote><pre>
* {@code <xs:appinfo>
* <jxb:bindings node="/xs:schema/xs:complexType/xs:sequence/xs:element[@name='generic']">
* <bv:facet type="org.eclipse.persistence.annotations.AdditionalCriteria" value="This is a real custom annotation."/>
* </jxb:bindings>
* </xs:appinfo>}
* </pre></blockquote>
*
* @author Marcel Valovy - marcel.valovy@oracle.com
*/
@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" +
" jakarta.validation annotations. Usage with mods: -" + PLUGIN_OPTION
+ " " + JSR_303_MOD + " " + SIMPLE_REGEX_MOD;
}
@Override
public List<String> 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("jakarta.validation.Valid");
ANNOTATION_NOTNULL = CODEMODEL.ref("jakarta.validation.constraints.NotNull");
ANNOTATION_SIZE = CODEMODEL.ref("jakarta.validation.constraints.Size");
ANNOTATION_DECIMALMIN = CODEMODEL.ref("jakarta.validation.constraints.DecimalMin");
ANNOTATION_DECIMALMAX = CODEMODEL.ref("jakarta.validation.constraints.DecimalMax");
ANNOTATION_DIGITS = CODEMODEL.ref("jakarta.validation.constraints.Digits");
ANNOTATION_PATTERN = CODEMODEL.ref("jakarta.validation.constraints.Pattern");
ANNOTATION_PATTERNLIST = CODEMODEL.ref("jakarta.validation.constraints.Pattern.List");
ANNOTATION_ASSERTFALSE = CODEMODEL.ref("jakarta.validation.constraints.AssertFalse");
ANNOTATION_ASSERTTRUE = CODEMODEL.ref("jakarta.validation.constraints.AssertTrue");
ANNOTATION_FUTURE = CODEMODEL.ref("jakarta.validation.constraints.Future");
ANNOTATION_PAST = CODEMODEL.ref("jakarta.validation.constraints.Past");
ANNOTATION_XMLELEMENT = CODEMODEL.ref("jakarta.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.
* <p>
* Example:
* <pre>{@code
* <xsd:complexType name="Employee">
* <xsd:simpleContent>
* <xsd:extension base="a:ShortId"> <- xsd extension base
* <xsd:attribute name="id" type="xsd:string" use="optional"/>
* </xsd:extension>
* </xsd:simpleContent>
* </xsd:complexType>
* <p>
* <xsd:simpleType name="ShortId">
* <xsd:restriction base="xsd:string">
* <xsd:minLength value="1"/> <- This is a special field that is added to the generated class, called "value" (corresponds to the valuePropertyName),
* <xsd:maxLength value="5"/> it gets processed by this method and the "value" field receives @Size(min = 1, max = 5).
* </xsd:restriction>
* </xsd:simpleType>}</pre>
*/
private void processValueFromExtendedBase(CValuePropertyInfo valueProperty, ClassOutline classOutline, List<FacetCustomization> customizations) {
String valuePropertyName = valueProperty.getName(false);
JFieldVar fieldVar = classOutline.implClass.fields().get(valuePropertyName);
XSSimpleType type = ((XSRestrictionSimpleType) valueProperty.getSchemaComponent()).asSimpleType();
processSimpleType(null, type, fieldVar, customizations);
}
/**
* Processes an xsd attribute.
* <p>
* Example:
* <pre>{@code
* <xsd:complexType name="Employee">
* <xsd:simpleContent>
* <xsd:extension base="a:Person">
* <xsd:attribute name="id" type="xsd:string" use="optional"/> << "id" is the attributePropertyName
* </xsd:extension>
* </xsd:simpleContent>
* </xsd:complexType>}</pre>
*/
private void processAttribute(CAttributePropertyInfo attributeProperty, ClassOutline classOutline, List<FacetCustomization> customizations) {
String attributePropertyName = attributeProperty.getName(false);
JFieldVar fieldVar = classOutline.implClass.fields().get(attributePropertyName);
XSAttributeUse attribute = (XSAttributeUse) 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.
* <p>
* Example:
* {@code <xsd:element name="someCollection" minOccurs="1" maxOccurs="unbounded"/>}
*/
private void processElement(CElementPropertyInfo propertyInfo, ClassOutline co, List<FacetCustomization> 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<FacetCustomization> customizations) {
final int minOccurs = particle.getMinOccurs().intValue();
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<FacetCustomization> customizations) {
Map<JAnnotationUse, FacetType> 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<JAnnotationUse, FacetType> 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<XSFacet> 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 <xsd:restriction base="xsd:string">.
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())) { // <xsd:restriction base="xsd:string">
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<Void, ClassOutline> {
@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<FacetCustomization> detectCustomizations(CCustomizable ca) {
List<FacetCustomization> facetCustomizations = new ArrayList<>();
List<CPluginCustomization> 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<FacetCustomization> customizations, Map<JAnnotationUse, FacetType> 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<JAnnotationUse, FacetType> annotations, FacetCustomization c) {
for (Map.Entry<JAnnotationUse, FacetType> 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 = particle.getMaxOccurs().intValue();
final int minOccurs = particle.getMinOccurs().intValue();
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 && particle.getMinOccurs().intValue() > 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 = annotationUse.getAnnotationMembers().get(VALUE).toString();
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 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.
* <p>
* Also replaces the two special XML regex character sets which aren't supported in Java, \i and \c.
* <p>
* Replaced shorthands and their negations:
* <blockquote><pre>
* \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.
* </pre></blockquote>
* <p>
* Changes to this class should also be reflected in the opposite
* {@link org.eclipse.persistence.jaxb.compiler.SchemaGenerator.RegexMutator} class within SchemaGen.
*
* @see <a href="http://stackoverflow.com/questions/4304928/unicode-equivalents-for-w-and-b-in-java-regular-expressions">tchrist's work</a>
* @see <a href="http://www.regular-expressions.info/shorthand.html#xml">Special shorthands in XML Schema.</a>
*/
private final class RegexMutator {
private final Map<Pattern, String> shorthandReplacements = simpleRegex
? new LinkedHashMap<Pattern, String>(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<Pattern, String>(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}]])|(?<![\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]])(?=[\\\\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}]])|(?<![\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]])(?![\\\\pL\\\\pM\\\\p{Nd}\\\\p{Nl}\\\\p{Pc}[\\\\p{InEnclosedAlphanumerics}&&\\\\p{So}]]))");
put(Pattern.compile("\\\\d"), "\\\\p{Nd}");
put(Pattern.compile("\\\\D"), "\\\\P{Nd}");
put(Pattern.compile("\\\\R"), "(?:(?>\\\\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<Pattern, String> 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) {
if (unboundedDigitsClasses.contains(fieldVarType)) {
return false;
}
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<String> nonFloatingDigitsClasses;
static {
Set<String> 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<String> unboundedDigitsClasses;
static {
Set<String> set = new HashSet<>();
set.add("BigInteger");
set.add("BigDecimal");
unboundedDigitsClasses = Collections.unmodifiableSet(new HashSet<>(set));
}
private static final Set<String> floatingDigitsClasses;
static {
Set<String> 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<String, MinMaxTuple> nonFloatingDigitsClassesBoundaries;
static {
HashMap<String, MinMaxTuple> 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<T extends Number> {
private final String min;
private final String max;
private MinMaxTuple(T min, T max) {
this.min = String.valueOf(min);
this.max = String.valueOf(max);
}
}
}