blob: 93785d73dc119141db45d2ce981260ddef4d40ff [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.testing.jaxb.beanvalidation;
import com.sun.tools.xjc.Driver;
import org.eclipse.persistence.jaxb.compiler.Generator;
import javax.tools.Diagnostic;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertFalse;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.eclipse.persistence.jaxb.javamodel.reflection.JavaModelImpl;
import org.eclipse.persistence.jaxb.javamodel.reflection.JavaModelInputImpl;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlValue;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
import javax.tools.StandardLocation;
import static org.eclipse.persistence.testing.jaxb.beanvalidation.ContentComparator.equalsXML;
/**
* Tests generation of JAXB Facets and Bean Validation annotations during Schemagen and XJC, respectively.
*
* @author Marcel Valovy - marcel.valovy@oracle.com
*/
public class BeanValidationBindingsTestCase extends junit.framework.TestCase {
private static final String GOLDEN_FILE_RESOURCE_PATH = "org/eclipse/persistence/testing/jaxb/beanvalidation/sgen_xjc/golden_file.xsd";
private static String PATH_TO_SCHEMA_DIRECTORY;
private static String GOLDEN_FILE_PATH;
private static String RICH_SCHEMA_PATH;
private static String CUSTOMIZED_SCHEMA_PATH;
private static String GENERATED_SCHEMA_PATH;
private static String TARGET_PATH;
private static final int DIRS_TO_ROOT = GOLDEN_FILE_RESOURCE_PATH.length() - GOLDEN_FILE_RESOURCE_PATH.replace("/", "").length();
public BeanValidationBindingsTestCase() {
try {
//context class loader and already exists file path is needed to run this test case from within IDE like Eclipse
PATH_TO_SCHEMA_DIRECTORY = new File(Thread.currentThread().getContextClassLoader().getResource(GOLDEN_FILE_RESOURCE_PATH).toURI()).getParentFile().getAbsolutePath();
GOLDEN_FILE_PATH = PATH_TO_SCHEMA_DIRECTORY + "/golden_file.xsd";
RICH_SCHEMA_PATH = PATH_TO_SCHEMA_DIRECTORY + "/rich_schema.xsd";
CUSTOMIZED_SCHEMA_PATH = PATH_TO_SCHEMA_DIRECTORY + "/customized_schema.xsd";
GENERATED_SCHEMA_PATH = PATH_TO_SCHEMA_DIRECTORY + "/schema1.xsd";
File alreadyExistsFile = new File(Thread.currentThread().getContextClassLoader().getResource(GOLDEN_FILE_RESOURCE_PATH).toURI());
for (int i = 0; i < DIRS_TO_ROOT + 1; i++) {
alreadyExistsFile = alreadyExistsFile.getParentFile();
}
TARGET_PATH = alreadyExistsFile.getAbsolutePath();
} catch (URISyntaxException e) {
fail(String.format("Unable to initialize %s test case", BeanValidationBindingsTestCase.class.getName()));
}
}
// Handles error case where no BV annotations would be generated on class fields and would still pass the equality test.
boolean annotationsGenerated;
private String pkg;
@Override
public void tearDown() throws Exception {
assertTrue(deleteDir(new File(pkg)));
//noinspection ResultOfMethodCallIgnored
new File(GENERATED_SCHEMA_PATH).delete();
}
/**
* A fancy test demonstrating (and testing) that it is possible to perform
* a round-trip in a lucky case where the XML Schema is identical to the
* original golden file.
* In practice, the XJC/Schemagen transformations guarantee only equality
* of post round-trip schema with the original, not identity.
* This test should still pass even if changes are done to our code.
*/
public void testGoldenFileIdentity() throws Throwable {
pkg = "gf";
roundTrip(GOLDEN_FILE_PATH, pkg);
assertTrue(equalsXML(new File(GOLDEN_FILE_PATH), new File(GENERATED_SCHEMA_PATH)));
}
/**
* Tests equality of the original schema and schema after round-trip, with
* bean validation annotations and facets.
* Also tests equality of generated Java classes from the original schema
* and from the schema after round-trip.
*/
public void testEqualitySchemaAndJava() throws Throwable {
pkg = "rs";
Class<?>[] cTenured = roundTrip(RICH_SCHEMA_PATH, pkg);
xjcGenerateJavaSources(GENERATED_SCHEMA_PATH); // Regenerate the sources.
compileGeneratedSources(createCompileList(pkg));
Class<?>[] cYoung = loadCompiledClasses(createLoadList(pkg));
assertTrue(equalsClasses(cTenured, cYoung));
}
/**
* Tests customizations, i.e. facet customizations + custom facets, i.e.
* Future, Past, AssertTrue, AssertFalse.
*/
public void testFacetCustomizationsAndCustomFacets() throws Throwable {
pkg = "cs";
xjcGenerateJavaSourcesWithCustomizations(CUSTOMIZED_SCHEMA_PATH);
compileGeneratedSources(createCompileList(pkg));
Class<?> custom = loadCompiledClasses(createLoadList(pkg))[0];
Field generic = custom.getDeclaredField("generic");
Size s = generic.getAnnotation(Size.class);
assertEquals(s.message(), "Hello, world!");
assertEquals(s.groups()[0], BindingTeam.class);
assertEquals(s.groups()[1], RocketTeam.class);
assertEquals(s.max(), 4);
Pattern p1 = generic.getAnnotation(Pattern.List.class).value()[0];
Pattern p2 = generic.getAnnotation(Pattern.List.class).value()[1];
assertEquals(p1.message(), p2.message());
assertTrue(Arrays.equals(p1.groups(), p2.groups()));
assertEquals(p1.message(), "Hello.");
assertEquals(p1.groups()[0], Object.class);
assertEquals(p1.regexp(), "10");
Future f = generic.getAnnotation(Future.class);
assertEquals(f.message(), "Welcome to the Future!");
assertEquals(f.groups()[0], BindingTeam.class);
Past p = generic.getAnnotation(Past.class);
assertEquals(p.message(), "Farewell from the ancestors.");
assertEquals(p.groups()[0], Ancestors.class);
AssertTrue at = generic.getAnnotation(AssertTrue.class);
assertEquals(at.message(), "True fan of the team!");
assertEquals(at.groups()[0], BindingTeam.class);
AssertFalse af = generic.getAnnotation(AssertFalse.class);
assertEquals(af.message(), "false");
assertEquals(af.groups()[0], Object.class);
// To test a user custom annotation.
CustomAnnotation xmlKey = generic.getAnnotation(CustomAnnotation.class);
assertNotNull(xmlKey);
}
/**
* Tests that the XJC detects all facets and generates their respective
* annotations correctly.
*/
public void testAllFacetsAndAnnotations() throws Throwable {
pkg = "rs";
Class<?>[] c = roundTrip(RICH_SCHEMA_PATH, pkg);
Class<?> Main = c[0];
Class<?> Numbers = c[1];
Class<?> NumberWithHiddenValueAttribute = c[2];
Class<?> Strings = c[4];
XmlElement xmlElement;
Size size;
DecimalMax decimalMax;
DecimalMin decimalMin;
Pattern.List patternList;
Pattern pattern;
/* Main.class */
Field numbers = Main.getDeclaredField("numbers");
assertNotNull(numbers.getAnnotation(Valid.class));
assertNotNull(numbers.getAnnotation(NotNull.class));
xmlElement = numbers.getAnnotation(XmlElement.class);
assertFalse(xmlElement.nillable());
Field strings = Main.getDeclaredField("strings");
size = strings.getAnnotation(Size.class);
assertTrue(size.min() == 1 && size.max() == 2);
assertNotNull(strings.getAnnotation(Valid.class));
assertNotNull(strings.getAnnotation(NotNull.class));
xmlElement = strings.getAnnotation(XmlElement.class);
assertFalse(xmlElement.nillable());
Field unsignedByte = Main.getDeclaredField("unsignedByte");
decimalMax = unsignedByte.getAnnotation(DecimalMax.class);
assertEquals(decimalMax.value(), "255");
assertTrue(decimalMax.inclusive());
decimalMin = unsignedByte.getAnnotation(DecimalMin.class);
assertEquals(decimalMin.value(), "0");
assertTrue(decimalMin.inclusive());
Field byteArray = Main.getDeclaredField("byteArray");
size = byteArray.getAnnotation(Size.class);
assertTrue(size.max() == 18);
Field someCollection = Main.getDeclaredField("someCollection");
size = someCollection.getAnnotation(Size.class);
assertTrue(size.min() == 1);
assertNotNull(someCollection.getAnnotation(Valid.class));
assertNotNull(someCollection.getAnnotation(NotNull.class));
Field optionalElement = Main.getDeclaredField("optionalElement");
size = optionalElement.getAnnotation(Size.class);
assertTrue(size.min() == 0);
assertNotNull(optionalElement.getAnnotation(Valid.class));
assertNull(optionalElement.getAnnotation(NotNull.class));
Field optionalElementWithSimpleType = Main.getDeclaredField("optionalElementWithSimpleType");
assertNull(optionalElementWithSimpleType.getAnnotation(NotNull.class));
decimalMin = optionalElementWithSimpleType.getAnnotation(DecimalMin.class);
assertEquals(decimalMin.value(), "1");
decimalMax = optionalElementWithSimpleType.getAnnotation(DecimalMax.class);
assertEquals(decimalMax.value(), "31");
/* Numbers.class */
Field minInclusive = Numbers.getDeclaredField("minInclusive");
decimalMin = minInclusive.getAnnotation(DecimalMin.class);
assertEquals(decimalMin.value(), "1000");
assertTrue(decimalMin.inclusive());
Field maxInclusive = Numbers.getDeclaredField("maxInclusive");
decimalMax = maxInclusive.getAnnotation(DecimalMax.class);
assertEquals(decimalMax.value(), "1000");
assertTrue(decimalMax.inclusive());
Field minExclusive = Numbers.getDeclaredField("minExclusive");
decimalMin = minExclusive.getAnnotation(DecimalMin.class);
assertEquals(decimalMin.value(), "0");
assertFalse(decimalMin.inclusive());
Field maxExclusive = Numbers.getDeclaredField("maxExclusive");
decimalMax = maxExclusive.getAnnotation(DecimalMax.class);
assertEquals(decimalMax.value(), "1000");
assertFalse(decimalMax.inclusive());
Field minMaxExclusive = Numbers.getDeclaredField("minMaxExclusive");
decimalMax = minMaxExclusive.getAnnotation(DecimalMax.class);
assertEquals(decimalMax.value(), "9223372");
assertFalse(decimalMax.inclusive());
decimalMin = minMaxExclusive.getAnnotation(DecimalMin.class);
assertEquals(decimalMin.value(), "0");
assertFalse(decimalMin.inclusive());
/* NumberWithHiddenValueAttribute.class */
Field value = NumberWithHiddenValueAttribute.getDeclaredField("value");
assertNotNull(value.getAnnotation(XmlValue.class));
size = value.getAnnotation(Size.class);
assertTrue(size.min() == 1 && size.max() == 5);
Field code = NumberWithHiddenValueAttribute.getDeclaredField("code");
assertNotNull(code.getAnnotation(NotNull.class));
Field whatNumber = NumberWithHiddenValueAttribute.getDeclaredField("whatNumber");
XmlAttribute xmlAttribute = whatNumber.getAnnotation(XmlAttribute.class);
assertTrue(xmlAttribute.required());
/* Strings.class */
Field regexShorthands = Strings.getDeclaredField("regexShorthands");
patternList = regexShorthands.getAnnotation(Pattern.List.class);
Pattern[] patterns = patternList.value();
assertEquals("[: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]", patterns[0].regexp());
assertEquals("[^: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]", patterns[1].regexp());
assertEquals("[-.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]", patterns[2].regexp());
assertEquals("[^-.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]", patterns[3].regexp());
assertEquals("\\p{Nd}", patterns[4].regexp());
assertEquals("\\P{Nd}", patterns[5].regexp());
assertEquals("[\\u0009-\\u000D\\u0020\\u0085\\u00A0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]", patterns[6].regexp());
assertEquals("[^\\u0009-\\u000D\\u0020\\u0085\\u00A0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]", patterns[7].regexp());
assertEquals("[\\u0009\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u200A\\u202F\\u205F\\u3000]", patterns[8].regexp());
assertEquals("[^\\u0009\\u0020\\u00A0\\u1680\\u180E\\u2000\\u2001-\\u200A\\u202F\\u205F\\u3000]", patterns[9].regexp());
assertEquals("[^\\u000A-\\u000D\\u0085\\u2028\\u2029]", patterns[10].regexp());
assertEquals("(?:(?>\\u000D\\u000A)|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029])", patterns[11].regexp());
Field idType = Strings.getDeclaredField("idType");
pattern = idType.getAnnotation(Pattern.class);
assertEquals(pattern.regexp(), "[[: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]-[:]][[-.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]-[:]]*");
assertTrue(idType.getAnnotation(Size.class).max() == 100);
Field genericString = Strings.getDeclaredField("genericString");
assertTrue(genericString.getAnnotation(Size.class).min() == 0);
assertTrue(genericString.getAnnotation(Size.class).max() == 1024);
Field maxLength = Strings.getDeclaredField("maxLength");
assertTrue(maxLength.getAnnotation(Size.class).max() == 1024);
Field minLength = Strings.getDeclaredField("minLength");
assertTrue(minLength.getAnnotation(Size.class).min() == 0);
}
private Class<?>[] roundTrip(String schemaPath, String pkg) throws Throwable {
xjcGenerateJavaSources(schemaPath);
compileGeneratedSources(createCompileList(pkg));
Class<?>[] classes = loadCompiledClasses(createLoadList(pkg));
generateSchema(classes); // Generates resource/PATH_TO_SCHEMA_DIRECTORY/schema1.xsd.
return classes;
}
public void xjcGenerateJavaSources(String schemaPath) throws Exception {
Driver.run(new String[] { schemaPath, "-XBeanVal" }, System.out, System.out);
}
public void xjcGenerateJavaSourcesWithCustomizations(String schemaPath) throws Exception {
Driver.run(new String[] { schemaPath, "-extension", "-XBeanVal" }, System.out, System.out);
}
private void generateSchema(Class<?>[] classes) throws FileNotFoundException {
JavaModelImpl javaModel = new JavaModelImpl(Thread.currentThread().getContextClassLoader());
JavaModelInputImpl modelInput = new JavaModelInputImpl(classes, javaModel);
modelInput.setFacets(true);
Generator gen = new Generator(modelInput);
gen.generateSchemaFiles(PATH_TO_SCHEMA_DIRECTORY, null);
}
private void compileGeneratedSources(File... compileList) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<? super JavaFileObject> diag = new DiagnosticCollector<>();
StandardJavaFileManager fm = compiler.getStandardFileManager(diag, null, null);
List<File> pathElements = getPath("jdk.module.path");
if (!pathElements.isEmpty()) {
//running on module-path:
fm.setLocation(StandardLocation.MODULE_PATH, pathElements);
pathElements = getPath("java.class.path");
if (!pathElements.isEmpty()) {
fm.setLocation(StandardLocation.CLASS_PATH, pathElements);
}
} else {
//on cp - move everythig to mp
pathElements = getPath("java.class.path");
if (!pathElements.isEmpty()) {
fm.setLocation(StandardLocation.MODULE_PATH, pathElements);
}
}
fm.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(new File(TARGET_PATH)));
Iterable<? extends JavaFileObject> compilationUnits = fm.getJavaFileObjectsFromFiles(Arrays.asList(compileList));
List<String> options = new ArrayList<>();
JavaCompiler.CompilationTask task = compiler.getTask(new OutputStreamWriter(System.out), fm, diag, options,
null, compilationUnits);
if (!task.call()) {
for (Diagnostic diagnostic : diag.getDiagnostics()) {
System.out.format("Error on line %d in %s\n", diagnostic.getLineNumber(), diagnostic);
}
fail("Compilation of generated classes failed. See the diagnostics output.");
}
}
private List<File> getPath(String property) {
List<File> cp = new ArrayList<>();
String value = System.getProperty(property, "").trim();
if (!value.isEmpty()) {
StringTokenizer st = new StringTokenizer(value, File.pathSeparator);
while (st.hasMoreTokens()) {
cp.add(new File(st.nextToken()));
}
}
return cp;
}
private File[] createCompileList(String pkg) {
return "cs".equals(pkg)
? new File[]{new File(pkg + "/Custom.java"),
new File(pkg + "/ObjectFactory.java"),
new File(pkg + "/package-info.java")}
: new File[]{new File(pkg + "/Main.java"),
new File(pkg + "/Numbers.java"),
new File(pkg + "/NumberWithHiddenValueAttribute.java"),
new File(pkg + "/ObjectFactory.java"),
new File(pkg + "/package-info.java"),
new File(pkg + "/Strings.java")};
}
private Class<?>[] loadCompiledClasses(String... loadList) throws Throwable {
ClassLoader cl = new URLClassLoader(new URL[] {new File(TARGET_PATH).toURI().toURL()},
Thread.currentThread().getContextClassLoader());
Class<?>[] loadedClasses = new Class[loadList.length];
for (int i = 0; i < loadedClasses.length; i++)
loadedClasses[i] = cl.loadClass(loadList[i]);
return loadedClasses;
}
private String[] createLoadList(String pkg) throws ClassNotFoundException {
return "cs".equals(pkg)
? new String[]{pkg + ".Custom", pkg + ".ObjectFactory" }
: new String[]{pkg + ".Main", pkg + ".Numbers",
pkg + ".NumberWithHiddenValueAttribute", pkg + ".ObjectFactory",
pkg + ".Strings"};
}
private boolean equalsClasses(Class<?>[] cTenured, Class<?>[] cYoung) {
if (cTenured.length != cYoung.length) return false;
for (int i = 0; i < cTenured.length; i++)
if (!equalsAnnotations(cTenured[i], cYoung[i])) return false;
return true;
}
private boolean equalsAnnotations(Class<?> c1, Class<?> c2) {
Field[] f1 = c1.getDeclaredFields();
Field[] f2 = c2.getDeclaredFields();
if (f1.length != f2.length) return false;
for (int i = 0; i<f2.length; i++) {
Annotation[] a1 = f1[i].getDeclaredAnnotations();
Annotation[] a2 = f2[i].getDeclaredAnnotations();
if (!Arrays.equals(a1, a2)) return false;
// Returns false if no BV annotations were generated.
if (!annotationsGenerated)
for (Annotation a : a1)
if (a.annotationType().equals(NotNull.class)) {
annotationsGenerated = true;
break;
}
}
return annotationsGenerated;
}
private static boolean deleteDir(File file) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children)
deleteDir(new File(file, child));
return file.delete();
} else
// The directory is now empty so delete it
return file.delete();
}
public interface BindingTeam{}
public @interface RocketTeam{}
public abstract class Ancestors{}
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
String value();
}
}