org.jsimpledb.ant.SchemaGeneratorTask Maven / Gradle / Ivy
Show all versions of jsimpledb-ant Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.ant;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Resource;
import org.jsimpledb.DefaultStorageIdGenerator;
import org.jsimpledb.JSimpleDBFactory;
import org.jsimpledb.StorageIdGenerator;
import org.jsimpledb.annotation.JFieldType;
import org.jsimpledb.annotation.JSimpleClass;
import org.jsimpledb.core.Database;
import org.jsimpledb.core.FieldType;
import org.jsimpledb.kv.simple.SimpleKVDatabase;
import org.jsimpledb.schema.SchemaModel;
import org.jsimpledb.spring.JSimpleDBClassScanner;
import org.jsimpledb.spring.JSimpleDBFieldTypeScanner;
/**
* Ant task for schema XML generation and/or verification.
*
*
* This task scans the configured classpath for classes with {@link org.jsimpledb.annotation.JSimpleClass @JSimpleClass}
* and {@link org.jsimpledb.annotation.JFieldType @JFieldType} annotations and either writes the generated schema
* to an XML file, or verifies the schema matches an existing XML file.
*
*
* Generation of schema XML files and the use of this task is not necessary. However, it does allow certain
* schema-related problems to be detected at build time instead of runtime. In particular, it can let you know
* if any change to your model classes requires a new JSimpleDB schema version.
*
*
* This task can also check for conflicts between the schema in question and older schema versions that may still
* exist in production databases. These other schema versions are specified using nested {@code }
* elements, which work just like {@code }'s.
*
*
* The following attributes are supported by this task:
*
*
*
*
* Attribute
* Required?
* Description
*
*
* {@code mode}
* No
*
*
* Set to {@code generate} to generate a new XML file, or {@code verify} to verify an existing XML file.
*
*
*
* Default is {@code verify}.
*
*
*
*
* {@code file}
* Yes
*
*
* The XML file to generate or verify.
*
*
*
*
* {@code matchNames}
* No
*
*
* Whether to verify not only schema compatibility but also that the two schemas are identical, i.e.,
* the same names are used for object types, fields, and composite indexes.
*
*
*
* Two schemas that are equivalent except for names are considered compatible, because the core API uses
* storage ID's, not names. However, if names change then some JSimpleDB layer operations, such as index
* queries and reference path inversion, may need to be updated.
*
*
*
* Default is {@code true}. Ignored unless {@code mode} is {@code verify}.
*
*
*
*
* {@code failOnError}
* No
*
*
* Whether to fail if verification fails when {@code mode="verify"} or when older schema
* versions are specified using nested {@code } elements, which work just like
* {@code }s.
*
*
*
* Default is {@code true}.
*
*
*
*
* {@code verifiedProperty}
* No
*
*
* The name of an ant property to set to {@code true} or {@code false} depending on whether
* verification succeeded or failed. Useful when {@code failOnError} is set to {@code false}
* and you want to handle the failure elsewhere in the build file.
*
*
*
* Default is to not set any property.
*
*
*
*
* {@code classpath} or {@code classpathref}
* Yes
*
*
* Specifies the search path containing classes with {@link org.jsimpledb.annotation.JSimpleClass @JSimpleClass}
* and {@link org.jsimpledb.annotation.JFieldType @JFieldType} annotations.
*
*
*
*
* {@code packages}
* Yes, unless {@code classes} are specified
*
*
* Specifies one or more Java package names (separated by commas and/or whitespace) under which to look
* for classes with {@link org.jsimpledb.annotation.JSimpleClass @JSimpleClass}
* or {@link org.jsimpledb.annotation.JFieldType @JFieldType} annotations.
*
*
* Use of this attribute requires Spring's classpath scanning classes ({@code spring-context.jar});
* these must be on the {@code } classpath.
*
*
*
*
* {@code classes}
* Yes, unless {@code packages} are specified
*
*
* Specifies one or more Java class names (separated by commas and/or whitespace) of
* classes with {@link org.jsimpledb.annotation.JSimpleClass @JSimpleClass}
* or {@link org.jsimpledb.annotation.JFieldType @JFieldType} annotations.
*
*
*
*
* {@code storageIdGeneratorClass}
* No
*
*
* Specifies the name of an optional custom {@link StorageIdGenerator} class.
*
*
*
* By default, a {@link DefaultStorageIdGenerator} is used.
*
*
*
*
*
*
*
* Classes are found by scanning the packages listed in the {@code "packages"} attribute.
* Alternatively, or in addition, specific classes may specified using the {@code "classes"} attribute.
*
*
* To install this task into ant:
*
*
* <project xmlns:jsimpledb="urn:org.jsimpledb.ant" ... >
* ...
* <taskdef uri="urn:org.jsimpledb.ant" name="schema"
* classname="org.jsimpledb.ant.SchemaGeneratorTask" classpathref="jsimpledb.classpath"/>
*
*
*
* Example of generating a schema XML file that corresponds to the specified Java model classes:
*
*
* <jsimpledb:schema mode="generate" classpathref="myclasses.classpath"
* file="schema.xml" packages="com.example.model"/>
*
*
*
* Example of verifying that the schema generated from the Java model classes has not
* changed incompatibly (i.e., in a way that would require a new schema version):
*
*
* <jsimpledb:schema mode="verify" classpathref="myclasses.classpath"
* file="expected-schema.xml" packages="com.example.model"/>
*
*
*
* Example of doing the same thing, and also verifying the generated schema is compatible with prior schema versions
* that may still be in use in production databases:
*
*
* <jsimpledb:schema mode="verify" classpathref="myclasses.classpath"
* file="expected-schema.xml" packages="com.example.model">
* <jsimpledb:oldschemas dir="obsolete-schemas" includes="*.xml"/>
* </jsimpledb:schema>
*
*
* @see org.jsimpledb.JSimpleDB
* @see org.jsimpledb.schema.SchemaModel
*/
public class SchemaGeneratorTask extends Task {
public static final String MODE_VERIFY = "verify";
public static final String MODE_GENERATE = "generate";
private String mode = MODE_VERIFY;
private boolean matchNames = true;
private boolean failOnError = true;
private String verifiedProperty;
private File file;
private Path classPath;
private String storageIdGeneratorClassName = DefaultStorageIdGenerator.class.getName();
private final ArrayList oldSchemasList = new ArrayList<>();
private final LinkedHashSet classes = new LinkedHashSet<>();
private final LinkedHashSet packages = new LinkedHashSet<>();
public void setClasses(String classes) {
this.classes.addAll(Arrays.asList(classes.split("[\\s,]+")));
}
public void setPackages(String packages) {
this.packages.addAll(Arrays.asList(packages.split("[\\s,]+")));
}
public void setMode(String mode) {
this.mode = mode;
}
public void setMatchNames(boolean matchNames) {
this.matchNames = matchNames;
}
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public void setVerifiedProperty(String verifiedProperty) {
this.verifiedProperty = verifiedProperty;
}
public void setFile(File file) {
this.file = file;
}
public Path createClasspath() {
this.classPath = new Path(this.getProject());
return this.classPath;
}
public void setClasspath(Path classPath) {
this.classPath = classPath;
}
public void setClasspathRef(Reference ref) {
this.classPath = (Path)ref.getReferencedObject(this.getProject());
}
public void setStorageIdGeneratorClass(String storageIdGeneratorClassName) {
this.storageIdGeneratorClassName = storageIdGeneratorClassName;
}
public void addOldSchemas(OldSchemas oldSchemas) {
this.oldSchemasList.add(oldSchemas);
}
/**
* @throws BuildException if operation fails
*/
@Override
public void execute() {
// Sanity check
if (this.file == null)
throw new BuildException("`file' attribute is required specifying output/verify file");
final boolean generate;
switch (this.mode) {
case MODE_VERIFY:
generate = false;
break;
case MODE_GENERATE:
generate = true;
break;
default:
throw new BuildException("`mode' attribute must be one of `" + MODE_VERIFY + "' or `" + MODE_GENERATE + "'");
}
if (this.packages == null)
throw new BuildException("`packages' attribute is required specifying packages to scan for Java model classes");
if (this.classPath == null)
throw new BuildException("`classpath' attribute is required specifying search path for scanned classes");
// Create directory containing file
if (generate && this.file.getParent() != null && !this.file.getParentFile().exists() && !this.file.getParentFile().mkdirs())
throw new BuildException("error creating directory `" + this.file.getParentFile() + "'");
// Set up mysterious classloader stuff
final AntClassLoader loader = this.getProject().createClassLoader(this.classPath);
final ClassLoader currentLoader = this.getClass().getClassLoader();
if (currentLoader != null)
loader.setParent(currentLoader);
loader.setThreadContextLoader();
try {
// Model and field type classes
final HashSet> modelClasses = new HashSet<>();
final HashSet> fieldTypeClasses = new HashSet<>();
// Do package scanning
if (!this.packages.isEmpty()) {
// Join list
final StringBuilder buf = new StringBuilder();
for (String packageName : this.packages) {
if (buf.length() > 0)
buf.append(' ');
buf.append(packageName);
}
final String packageNames = buf.toString();
// Scan for @JSimpleClass classes
this.log("scanning for @JSimpleClass annotations in packages: " + packageNames);
for (String className : new JSimpleDBClassScanner().scanForClasses(packageNames)) {
this.log("adding JSimpleDB model class " + className);
try {
modelClasses.add(Class.forName(className, false, Thread.currentThread().getContextClassLoader()));
} catch (ClassNotFoundException e) {
throw new BuildException("failed to load class `" + className + "'", e);
}
}
// Scan for @JFieldType classes
this.log("scanning for @JFieldType annotations in packages: " + packageNames);
for (String className : new JSimpleDBFieldTypeScanner().scanForClasses(packageNames)) {
this.log("adding JSimpleDB field type class `" + className + "'");
try {
fieldTypeClasses.add(Class.forName(className, false, Thread.currentThread().getContextClassLoader()));
} catch (Exception e) {
throw new BuildException("failed to instantiate " + className, e);
}
}
}
// Do specific class scanning
for (String className : this.classes) {
// Load class
final Class> cl;
try {
cl = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new BuildException("failed to load class `" + className + "'", e);
}
// Add model classes
if (cl.isAnnotationPresent(JSimpleClass.class)) {
this.log("adding JSimpleDB model " + cl);
modelClasses.add(cl);
}
// Add field types
if (cl.isAnnotationPresent(JFieldType.class)) {
this.log("adding JSimpleDB field type " + cl);
fieldTypeClasses.add(cl);
}
}
// Instantiate StorageIdGenerator
final StorageIdGenerator storageIdGenerator;
try {
storageIdGenerator = Class.forName(this.storageIdGeneratorClassName,
false, Thread.currentThread().getContextClassLoader()).asSubclass(StorageIdGenerator.class).newInstance();
} catch (Exception e) {
throw new BuildException("failed to instantiate class `" + storageIdGeneratorClassName + "'", e);
}
// Set up database
final Database db = new Database(new SimpleKVDatabase());
// Instantiate and configure field type classes
for (Class> cl : fieldTypeClasses) {
// Instantiate field types
this.log("instantiating " + cl + " as field type instance");
final FieldType> fieldType;
try {
fieldType = this.asFieldTypeClass(cl).newInstance();
} catch (Exception e) {
throw new BuildException("failed to instantiate " + cl.getName(), e);
}
// Add field type
try {
db.getFieldTypeRegistry().add(fieldType);
} catch (Exception e) {
throw new BuildException("failed to register custom field type " + cl.getName(), e);
}
}
// Set up factory
final JSimpleDBFactory factory = new JSimpleDBFactory();
factory.setDatabase(db);
factory.setSchemaVersion(1);
factory.setStorageIdGenerator(storageIdGenerator);
factory.setModelClasses(modelClasses);
// Build schema model
this.log("generating JSimpleDB schema from schema classes");
final SchemaModel schemaModel;
try {
schemaModel = factory.newJSimpleDB().getSchemaModel();
} catch (Exception e) {
throw new BuildException("schema generation failed: " + e, e);
}
// Record schema model in database
db.createTransaction(schemaModel, 1, true).commit();
// Verify or generate
boolean verified = true;
if (generate) {
// Write schema model to file
this.log("writing JSimpleDB schema to `" + this.file + "'");
try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(this.file))) {
schemaModel.toXML(output, true);
} catch (IOException e) {
throw new BuildException("error writing schema to `" + this.file + "': " + e, e);
}
} else {
// Read file
this.log("verifying JSimpleDB schema matches `" + this.file + "'");
final SchemaModel verifyModel;
try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(this.file))) {
verifyModel = SchemaModel.fromXML(input);
} catch (IOException e) {
throw new BuildException("error reading schema from `" + this.file + "': " + e, e);
}
// Compare
final boolean matched = matchNames ? schemaModel.equals(verifyModel) : schemaModel.isCompatibleWith(verifyModel);
if (!matched)
verified = false;
this.log("schema verification " + (matched ? "succeeded" : "failed"));
if (!matched)
this.log(schemaModel.differencesFrom(verifyModel).toString());
}
// Check for conflicts with other schema versions
if (verified) {
int schemaVersion = 2;
for (OldSchemas oldSchemas : this.oldSchemasList) {
for (Iterator> i = oldSchemas.iterator(); i.hasNext(); ) {
final Resource resource = (Resource)i.next();
this.log("checking schema for conflicts with " + resource);
final SchemaModel otherSchema;
try (BufferedInputStream input = new BufferedInputStream(resource.getInputStream())) {
otherSchema = SchemaModel.fromXML(input);
} catch (IOException e) {
throw new BuildException("error reading schema from `" + resource + "': " + e, e);
}
try {
db.createTransaction(otherSchema, schemaVersion++, true).commit();
} catch (Exception e) {
this.log("schema conflicts with " + resource + ": " + e);
verified = false;
}
}
}
}
// Check verification results
if (this.verifiedProperty != null)
this.getProject().setProperty(this.verifiedProperty, "" + verified);
if (!verified && this.failOnError)
throw new BuildException("schema verification failed");
} finally {
loader.resetThreadContextLoader();
loader.cleanup();
}
}
@SuppressWarnings("unchecked")
private Class extends FieldType>> asFieldTypeClass(Class> klass) {
try {
return (Class extends FieldType>>)klass.asSubclass(FieldType.class);
} catch (ClassCastException e) {
throw new BuildException("invalid @" + JFieldType.class.getSimpleName() + " annotation on "
+ klass + ": type is not a subclass of " + FieldType.class);
}
}
}