提交 0bedabe6 authored 作者: james.moger@gmail.com's avatar james.moger@gmail.com

JaQu annotations, model generation, model validation, extended syntax (issue #286)

上级 e0228642
......@@ -65,7 +65,9 @@ import org.h2.test.db.TestView;
import org.h2.test.db.TestViewAlterTable;
import org.h2.test.db.TestViewDropView;
import org.h2.test.jaqu.AliasMapTest;
import org.h2.test.jaqu.AnnotationsTest;
import org.h2.test.jaqu.ClobTest;
import org.h2.test.jaqu.ModelsTest;
import org.h2.test.jaqu.SamplesTest;
import org.h2.test.jaqu.UpdateTest;
import org.h2.test.jdbc.TestBatchUpdates;
......@@ -588,6 +590,8 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
new ClobTest().runTest(this);
new SamplesTest().runTest(this);
new UpdateTest().runTest(this);
new AnnotationsTest().runTest(this);
new ModelsTest().runTest(this);
// jdbc
new TestBatchUpdates().runTest(this);
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.test.jaqu;
import java.util.List;
import org.h2.constant.ErrorCode;
import org.h2.jaqu.Db;
import org.h2.jdbc.JdbcSQLException;
import org.h2.test.TestBase;
public class AnnotationsTest extends TestBase {
/**
* This object represents a database (actually a connection to the database).
*/
//## Java 1.5 begin ##
Db db;
//## Java 1.5 end ##
/**
* This method is called when executing this application from the command
* line.
*
* @param args the command line parameters
*/
public static void main(String... args) {
new AnnotationsTest().test();
}
public void test() {
//## Java 1.5 begin ##
// EventLogger.activateConsoleLogger();
db = Db.open("jdbc:h2:mem:", "sa", "sa");
db.insertAll(Product.getList());
db.insertAll(ProductAnnotationOnly.getList());
db.insertAll(ProductMixedAnnotation.getList());
testProductAnnotationOnly();
testProductMixedAnnotation();
testTrimStringAnnotation();
testCreateTableIfRequiredAnnotation();
testColumnInheritanceAnnotation();
db.close();
// EventLogger.deactivateConsoleLogger();
//## Java 1.5 end ##
}
private void testProductAnnotationOnly() {
ProductAnnotationOnly p = new ProductAnnotationOnly();
assertEquals(10, db.from(p).selectCount());
// Test JQColumn.name="cat"
assertEquals(2, db.from(p).where(p.category).is("Beverages").selectCount());
// Test JQTable.annotationsOnly=true
// public String unmappedField is ignored by JaQu
assertEquals(0, db.from(p).where(p.unmappedField).is("unmapped").selectCount());
// Test JQColumn.autoIncrement=true
// 10 objects, 10 autoIncremented unique values
assertEquals(10, db.from(p).selectDistinct(p.autoIncrement).size());
// Test JQTable.primaryKey=id
int errorCode = 0;
try {
db.insertAll(ProductAnnotationOnly.getList());
} catch (Throwable t) {
if (t.getCause() instanceof JdbcSQLException) {
JdbcSQLException s = (JdbcSQLException) t.getCause();
errorCode = s.getErrorCode();
}
}
assertEquals(errorCode, ErrorCode.DUPLICATE_KEY_1);
}
private void testProductMixedAnnotation() {
ProductMixedAnnotation p = new ProductMixedAnnotation();
// Test JQColumn.name="cat"
assertEquals(2, db.from(p).where(p.category).is("Beverages").selectCount());
// Test JQTable.annotationsOnly=false
// public String mappedField is reflectively mapped by JaQu
assertEquals(10, db.from(p).where(p.mappedField).is("mapped").selectCount());
// Test JQColumn.primaryKey=true
int errorCode = 0;
try {
db.insertAll(ProductMixedAnnotation.getList());
} catch (Throwable t) {
if (t.getCause() instanceof JdbcSQLException) {
JdbcSQLException s = (JdbcSQLException) t.getCause();
errorCode = s.getErrorCode();
}
}
assertEquals(errorCode, ErrorCode.DUPLICATE_KEY_1);
}
private void testTrimStringAnnotation() {
ProductAnnotationOnly p = new ProductAnnotationOnly();
ProductAnnotationOnly prod = db.from(p).selectFirst();
String oldValue = prod.category;
String newValue = "01234567890123456789";
prod.category = newValue; // 2 chars exceeds field max
db.update(prod);
ProductAnnotationOnly newProd = db.from(p)
.where(p.productId)
.is(prod.productId)
.selectFirst();
assertEquals(newValue.substring(0, 15), newProd.category);
newProd.category = oldValue;
db.update(newProd);
}
private void testColumnInheritanceAnnotation() {
ProductInheritedAnnotation table = new ProductInheritedAnnotation();
Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
List<ProductInheritedAnnotation> inserted = ProductInheritedAnnotation.getData();
db.insertAll(inserted);
List<ProductInheritedAnnotation> retrieved = db.from(table).select();
for (int j = 0; j < retrieved.size(); j++) {
ProductInheritedAnnotation i = inserted.get(j);
ProductInheritedAnnotation r = retrieved.get(j);
assertEquals(i.category, r.category);
assertEquals(i.mappedField, r.mappedField);
assertEquals(i.unitsInStock, r.unitsInStock);
assertEquals(i.unitPrice, r.unitPrice);
assertEquals(i.name(), r.name());
assertEquals(i.id(), r.id());
}
db.close();
}
private void testCreateTableIfRequiredAnnotation() {
// Tests JQTable.createTableIfRequired=false
int errorCode = 0;
try {
Db noCreateDb = Db.open("jdbc:h2:mem:", "sa", "sa");
noCreateDb.insertAll(ProductNoCreateTable.getList());
noCreateDb.close();
} catch (Throwable e) {
if (e.getCause() instanceof JdbcSQLException) {
JdbcSQLException error = (JdbcSQLException) e.getCause();
errorCode = error.getErrorCode();
}
}
assertTrue(errorCode == ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1);
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.test.jaqu;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.h2.jaqu.Db;
import org.h2.jaqu.DbInspector;
import org.h2.jaqu.DbUpgrader;
import org.h2.jaqu.DbVersion;
import org.h2.jaqu.Table.JQDatabase;
import org.h2.jaqu.Validation;
import org.h2.test.TestBase;
import org.h2.test.jaqu.SupportedTypes.SupportedTypes2;
public class ModelsTest extends TestBase {
/**
* This object represents a database (actually a connection to the database).
*/
//## Java 1.5 begin ##
Db db;
//## Java 1.5 end ##
/**
* This method is called when executing this application from the command
* line.
*
* @param args the command line parameters
*/
public static void main(String... args) {
new ModelsTest().test();
}
public void test() {
//## Java 1.5 begin ##
db = Db.open("jdbc:h2:mem:", "sa", "sa");
db.insertAll(Product.getList());
db.insertAll(ProductAnnotationOnly.getList());
db.insertAll(ProductMixedAnnotation.getList());
testValidateModels();
testSupportedTypes();
testModelGeneration();
testDatabaseUpgrade();
testTableUpgrade();
db.close();
//## Java 1.5 end ##
}
private void testValidateModels() {
boolean verbose = false;
DbInspector inspector = new DbInspector(db);
validateModel(inspector, new Product(), verbose);
validateModel(inspector, new ProductAnnotationOnly(), verbose);
validateModel(inspector, new ProductMixedAnnotation(), verbose);
}
private void validateModel(DbInspector inspector, Object o, boolean verbose) {
List<Validation> remarks = inspector.validateModel(o, false);
if (verbose && remarks.size() > 0) {
System.out.println("Validation Remarks for " + o.getClass().getName());
System.out.println("=============================================");
for (Validation remark:remarks)
System.out.println(remark);
System.out.println();
}
for (Validation remark:remarks)
assertFalse(remark.toString(), remark.isError());
}
private void testSupportedTypes() {
List<SupportedTypes> original = SupportedTypes.createList();
db.insertAll(original);
List<SupportedTypes> retrieved = db.from(SupportedTypes.SAMPLE).select();
assertEquals(original.size(), retrieved.size());
for (int i = 0; i < original.size(); i++) {
SupportedTypes o = original.get(i);
SupportedTypes r = retrieved.get(i);
assertTrue(o.equivalentTo(r));
}
}
private void testModelGeneration() {
DbInspector inspector = new DbInspector(db);
List<String> models = inspector.generateModel(null, "SupportedTypes",
"org.h2.test.jaqu", true, true);
assertEquals(1, models.size());
// a poor test, but a start
assertEquals(1364, models.get(0).length());
}
private void testDatabaseUpgrade() {
Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
// Insert a Database version record
db.insert(new DbVersion(1));
TestDbUpgrader dbUpgrader = new TestDbUpgrader();
db.setDbUpgrader(dbUpgrader);
List<SupportedTypes> original = SupportedTypes.createList();
db.insertAll(original);
assertEquals(1, dbUpgrader.oldVersion.get());
assertEquals(2, dbUpgrader.newVersion.get());
db.close();
}
private void testTableUpgrade() {
Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
// Insert first, this will create version record automatically
List<SupportedTypes> original = SupportedTypes.createList();
db.insertAll(original);
// Reset the dbUpgrader (clears updatecheck cache)
TestDbUpgrader dbUpgrader = new TestDbUpgrader();
db.setDbUpgrader(dbUpgrader);
SupportedTypes2 s2 = new SupportedTypes2();
List<SupportedTypes2> types = db.from(s2).select();
assertEquals(10, types.size());
assertEquals(1, dbUpgrader.oldVersion.get());
assertEquals(2, dbUpgrader.newVersion.get());
db.close();
}
@JQDatabase(version=2)
private class TestDbUpgrader implements DbUpgrader {
final AtomicInteger oldVersion = new AtomicInteger(0);
final AtomicInteger newVersion = new AtomicInteger(0);
@Override
public boolean upgradeTable(Db db, String schema, String table,
int fromVersion, int toVersion) {
// Sample DbUpgrader just claims success on upgrade request
oldVersion.set(fromVersion);
newVersion.set(toVersion);
return true;
}
@Override
public boolean upgradeDatabase(Db db, int fromVersion, int toVersion) {
// Sample DbUpgrader just claims success on upgrade request
oldVersion.set(fromVersion);
newVersion.set(toVersion);
return true;
}
};
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.test.jaqu;
// ## Java 1.5 begin ##
import java.util.Arrays;
import java.util.List;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQIndex;
import org.h2.jaqu.Table.JQTable;
/**
* A table containing product data.
*/
// ## Java 1.5 begin ##
@JQTable(name = "AnnotatedProduct", primaryKey = "id")
@JQIndex(standard = "name, cat")
public class ProductAnnotationOnly {
@JQColumn(name = "id")
Integer productId;
@JQColumn(name = "name")
private String productName;
@JQColumn(name = "cat", maxLength = 15, trimString=true)
String category;
@SuppressWarnings("unused")
@JQColumn
private Double unitPrice;
@JQColumn
private Integer unitsInStock;
@JQColumn(autoIncrement=true)
public Integer autoIncrement;
public String unmappedField;
public ProductAnnotationOnly() {
// public constructor
}
private ProductAnnotationOnly(int productId, String productName, String category, double unitPrice,
int unitsInStock, String unmappedField) {
this.productId = productId;
this.productName = productName;
this.category = category;
this.unitPrice = unitPrice;
this.unitsInStock = unitsInStock;
this.unmappedField = unmappedField;
}
private static ProductAnnotationOnly create(int productId, String productName, String category, double unitPrice,
int unitsInStock, String unmappedField) {
return new ProductAnnotationOnly(productId, productName, category, unitPrice, unitsInStock, unmappedField);
}
public static List<ProductAnnotationOnly> getList() {
String unmappedField = "unmapped";
ProductAnnotationOnly[] list = { create(1, "Chai", "Beverages", 18, 39, unmappedField),
create(2, "Chang", "Beverages", 19.0, 17, unmappedField),
create(3, "Aniseed Syrup", "Condiments", 10.0, 13, unmappedField),
create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, unmappedField),
create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, unmappedField),
create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, unmappedField),
create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, unmappedField),
create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, unmappedField),
create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, unmappedField),
create(10, "Ikura", "Seafood", 31.0, 31, unmappedField), };
return Arrays.asList(list);
}
public String toString() {
return productName + ": " + unitsInStock;
}
}
// ## Java 1.5 end ##
package org.h2.test.jaqu;
import java.util.Arrays;
import java.util.List;
import org.h2.jaqu.Table.JQTable;
/**
* This class inherits all its fields from a parent class which has annotated
* columns. The JQTable annotation of the parent class is ignored and only
* the JQTable annotation of this class matters.
* <p>
* However, this table inherits JQColumns from its super class.
*/
@JQTable(inheritColumns = true, annotationsOnly = false)
public class ProductInheritedAnnotation extends ProductMixedAnnotation {
public ProductInheritedAnnotation() {
// public constructor
}
private ProductInheritedAnnotation(int productId, String productName, String category, double unitPrice,
int unitsInStock, String mappedField) {
super(productId, productName, category, unitPrice, unitsInStock, mappedField);
}
private static ProductInheritedAnnotation create(int productId, String productName, String category,
double unitPrice, int unitsInStock, String mappedField) {
return new ProductInheritedAnnotation(productId, productName, category, unitPrice, unitsInStock, mappedField);
}
public static List<ProductInheritedAnnotation> getData() {
String mappedField = "mapped";
ProductInheritedAnnotation[] list = { create(1, "Chai", "Beverages", 18, 39, mappedField),
create(2, "Chang", "Beverages", 19.0, 17, mappedField),
create(3, "Aniseed Syrup", "Condiments", 10.0, 13, mappedField),
create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, mappedField),
create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, mappedField),
create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, mappedField),
create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, mappedField),
create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, mappedField),
create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, mappedField),
create(10, "Ikura", "Seafood", 31.0, 31, mappedField), };
return Arrays.asList(list);
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.test.jaqu;
// ## Java 1.5 begin ##
import java.util.Arrays;
import java.util.List;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQIndex;
import org.h2.jaqu.Table.JQTable;
/**
* A table containing product data.
*/
// ## Java 1.5 begin ##
@JQTable(annotationsOnly = false)
@JQIndex(standard="name,cat")
public class ProductMixedAnnotation {
@JQColumn(name = "id", primaryKey=true)
private Integer productId;
@JQColumn(name = "name")
private String productName;
@JQColumn(name = "cat", maxLength = 255)
String category;
public Double unitPrice;
public Integer unitsInStock;
public String mappedField;
public ProductMixedAnnotation() {
// public constructor
}
protected ProductMixedAnnotation(int productId, String productName, String category, double unitPrice,
int unitsInStock, String mappedField) {
this.productId = productId;
this.productName = productName;
this.category = category;
this.unitPrice = unitPrice;
this.unitsInStock = unitsInStock;
this.mappedField = mappedField;
}
private static ProductMixedAnnotation create(int productId, String productName, String category, double unitPrice,
int unitsInStock, String mappedField) {
return new ProductMixedAnnotation(productId, productName, category, unitPrice, unitsInStock, mappedField);
}
public static List<ProductMixedAnnotation> getList() {
String mappedField = "mapped";
ProductMixedAnnotation[] list = { create(1, "Chai", "Beverages", 18, 39, mappedField),
create(2, "Chang", "Beverages", 19.0, 17, mappedField),
create(3, "Aniseed Syrup", "Condiments", 10.0, 13, mappedField),
create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, mappedField),
create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, mappedField),
create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, mappedField),
create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, mappedField),
create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, mappedField),
create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, mappedField),
create(10, "Ikura", "Seafood", 31.0, 31, mappedField), };
return Arrays.asList(list);
}
public String toString() {
return productName + ": " + unitsInStock;
}
public int id() {
return productId;
}
public String name() {
return productName;
}
}
// ## Java 1.5 end ##
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.test.jaqu;
// ## Java 1.5 begin ##
import java.util.Arrays;
import java.util.List;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQTable;
/**
* A table containing product data.
*/
// ## Java 1.5 begin ##
@JQTable(createIfRequired = false)
public class ProductNoCreateTable {
@SuppressWarnings("unused")
@JQColumn(name = "id")
private Integer productId;
@SuppressWarnings("unused")
@JQColumn(name = "name")
private String productName;
public ProductNoCreateTable() {
// public constructor
}
private ProductNoCreateTable(int productId, String productName) {
this.productId = productId;
this.productName = productName;
}
private static ProductNoCreateTable create(int productId, String productName) {
return new ProductNoCreateTable(productId, productName);
}
public static List<ProductNoCreateTable> getList() {
ProductNoCreateTable[] list = { create(1, "Chai"), create(2, "Chang") };
return Arrays.asList(list);
}
}
// ## Java 1.5 end ##
......@@ -9,9 +9,14 @@ package org.h2.test.jaqu;
import static org.h2.jaqu.Function.count;
import static org.h2.jaqu.Function.isNull;
import static org.h2.jaqu.Function.length;
import static org.h2.jaqu.Function.*;
import static org.h2.jaqu.Function.max;
import static org.h2.jaqu.Function.min;
import static org.h2.jaqu.Function.not;
import static org.h2.jaqu.Function.sum;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.h2.jaqu.Db;
import org.h2.jaqu.Filter;
import org.h2.test.TestBase;
......@@ -73,6 +78,8 @@ public class SamplesTest extends TestBase {
testWhereSimple2();
testWhereSimple3();
testReverseColumns();
testLimitOffset();
testKeyRetrieval();
db.close();
//## Java 1.5 end ##
}
......@@ -265,6 +272,9 @@ public class SamplesTest extends TestBase {
deleted = db.from(p).delete();
assertEquals(9, deleted);
db.insertAll(Product.getList());
db.deleteAll(Product.getList());
assertEquals(0, db.from(p).selectCount());
db.insertAll(Product.getList());
}
private void testOrAndNot() {
......@@ -376,6 +386,25 @@ public class SamplesTest extends TestBase {
assertEquals(1, count);
}
private void testLimitOffset() {
Set<Integer> ids = new HashSet<Integer>();
Product p = new Product();
for (int i = 0; i < 5; i++) {
List<Product> products = db.from(p).limit(2).offset(2*i).select();
assertTrue(products.size() == 2);
for (Product prod:products)
assertTrue("Failed to add product id. Duplicate?", ids.add(prod.productId));
}
}
private void testKeyRetrieval() {
List<SupportedTypes> list = SupportedTypes.createList();
List<Long> keys = db.insertAllAndGetKeys(list);
Set<Long> uniqueKeys = new HashSet<Long>();
for (Long l:keys)
assertTrue("Failed to add key. Duplicate?", uniqueKeys.add(l));
}
//## Java 1.5 end ##
/**
......
package org.h2.test.jaqu;
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQTable;
import org.h2.util.New;
@JQTable(strictTypeMapping=true, version=1)
public class SupportedTypes {
@JQColumn(primaryKey=true, autoIncrement=true)
public Integer id;
@JQColumn
private Boolean myBool = false;
@JQColumn
private Byte myByte = 2;
@JQColumn
private Short myShort;
@JQColumn
private Integer myInteger;
@JQColumn
private Long myLong;
@JQColumn
private Float myFloat = 1.0f;
@JQColumn
private Double myDouble;
@JQColumn
private BigDecimal myBigDecimal;
@JQColumn
private String myString;
@JQColumn
private java.util.Date myUtilDate;
@JQColumn
private java.sql.Date mySqlDate;
@JQColumn
private java.sql.Time mySqlTime;
@JQColumn
private java.sql.Timestamp mySqlTimestamp;
static SupportedTypes SAMPLE = new SupportedTypes();
static List<SupportedTypes> createList() {
List<SupportedTypes> list = New.arrayList();
for (int i = 0; i < 10; i++)
list.add(randomValue());
return list;
}
static SupportedTypes randomValue() {
Random rand = new Random();
SupportedTypes s = new SupportedTypes();
s.myBool = new Boolean(rand.nextBoolean());
s.myByte = new Byte((byte) rand.nextInt(Byte.MAX_VALUE));
s.myShort = new Short((short) rand.nextInt(Short.MAX_VALUE));
s.myInteger = new Integer(rand.nextInt());
s.myLong = new Long(rand.nextLong());
s.myFloat = new Float(rand.nextFloat());
s.myDouble = new Double(rand.nextDouble());
s.myBigDecimal = new BigDecimal(rand.nextDouble());
s.myString = Long.toHexString(rand.nextLong());
s.myUtilDate = new java.util.Date(rand.nextLong());
s.mySqlDate = new java.sql.Date(rand.nextLong());
s.mySqlTime = new java.sql.Time(rand.nextLong());
s.mySqlTimestamp = new java.sql.Timestamp(rand.nextLong());
return s;
}
public boolean equivalentTo(SupportedTypes s) {
boolean same = true;
same &= myBool.equals(s.myBool);
same &= myByte.equals(s.myByte);
same &= myShort.equals(s.myShort);
same &= myInteger.equals(s.myInteger);
same &= myLong.equals(s.myLong);
same &= myFloat.equals(s.myFloat);
same &= myDouble.equals(s.myDouble);
same &= myBigDecimal.equals(s.myBigDecimal);
same &= myUtilDate.getTime() == s.myUtilDate.getTime();
same &= mySqlTimestamp.getTime() == s.mySqlTimestamp.getTime();
same &= mySqlDate.toString().equals(s.mySqlDate.toString());
same &= mySqlTime.toString().equals(s.mySqlTime.toString());
return same;
}
/**
* Class to demonstrate TableUpdater
*
*/
@JQTable(name="SupportedTypes", version=2, inheritColumns=true, strictTypeMapping=true)
public static class SupportedTypes2 extends SupportedTypes {
public SupportedTypes2() {
}
}
}
......@@ -6,11 +6,11 @@
*/
package org.h2.test.jaqu;
import static java.sql.Date.valueOf;
import org.h2.jaqu.Db;
import org.h2.jaqu.util.StatementLogger;
import org.h2.test.TestBase;
import static java.sql.Date.valueOf;
/**
* Tests the Db.update() function.
*
......@@ -31,6 +31,8 @@ public class UpdateTest extends TestBase {
}
public void test() throws Exception {
// EventLogger.activateConsoleLogger();
db = Db.open("jdbc:h2:mem:", "sa", "sa");
db.insertAll(Product.getList());
db.insertAll(Customer.getList());
......@@ -40,8 +42,10 @@ public class UpdateTest extends TestBase {
testSimpleUpdateWithCombinedPrimaryKey();
testSimpleMerge();
testSimpleMergeWithCombinedPrimaryKey();
testSetColumns();
db.close();
// EventLogger.deactivateConsoleLogger();
}
private void testSimpleUpdate() {
......@@ -111,5 +115,38 @@ public class UpdateTest extends TestBase {
ourOrder.orderDate = valueOf("2007-01-02");
db.merge(ourOrder);
}
private void testSetColumns() {
Product p = new Product();
Product original = db.from(p).where(p.productId).is(1).selectFirst();
// SetColumn on String and Double
db.from(p)
.set(p.productName).to("updated")
.increment(p.unitPrice).by(3.14)
.increment(p.unitsInStock).by(2)
.where(p.productId)
.is(1).
update();
// Confirm fields were properly updated
Product revised = db.from(p).where(p.productId).is(1).selectFirst();
assertEquals("updated", revised.productName);
assertEquals(original.unitPrice + 3.14, revised.unitPrice);
assertEquals(original.unitsInStock + 2, revised.unitsInStock.intValue());
// Restore fields
db.from(p)
.set(p.productName).to(original.productName)
.set(p.unitPrice).to(original.unitPrice)
.increment(p.unitsInStock).by(-2)
.where(p.productId).is(1).update();
// Confirm fields were properly restored
Product restored = db.from(p).where(p.productId).is(1).selectFirst();
assertEquals(original.productName, restored.productName);
assertEquals(original.unitPrice, restored.unitPrice);
assertEquals(original.unitsInStock, restored.unitsInStock);
}
}
......@@ -12,16 +12,21 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.sql.DataSource;
import org.h2.jaqu.DbUpgrader.DefaultDbUpgrader;
import org.h2.jaqu.SQLDialect.DefaultSQLDialect;
import org.h2.jaqu.Table.JQDatabase;
import org.h2.jaqu.Table.JQTable;
import org.h2.jaqu.util.JdbcUtils;
import org.h2.jaqu.util.StringUtils;
import org.h2.jaqu.util.Utils;
import org.h2.jaqu.util.WeakIdentityHashMap;
import org.h2.util.JdbcUtils;
//## Java 1.5 end ##
/**
* This class represents a connection to a database.
......@@ -41,9 +46,18 @@ public class Db {
private final Connection conn;
private final Map<Class<?>, TableDefinition<?>> classMap =
Utils.newHashMap();
Db(Connection conn) {
private final SQLDialect dialect;
private DbUpgrader dbUpgrader = new DefaultDbUpgrader();
private final Set<Class<?>> upgradeChecked = Utils.newConcurrentHashSet();
public Db(Connection conn) {
this.conn = conn;
dialect = getDialect(conn.getClass().getCanonicalName());
}
SQLDialect getDialect(String clazz) {
// TODO add special cases here
return new DefaultSQLDialect();
}
static <X> X registerToken(X x, Token token) {
......@@ -105,27 +119,105 @@ public class Db {
public <T> void insert(T t) {
Class<?> clazz = t.getClass();
define(clazz).createTableIfRequired(this).insert(this, t);
upgradeDb().define(clazz).createTableIfRequired(this).insert(this, t, false);
}
public <T> long insertAndGetKey(T t) {
Class<?> clazz = t.getClass();
return upgradeDb().define(clazz).createTableIfRequired(this).insert(this, t, true);
}
public <T> void merge(T t) {
Class<?> clazz = t.getClass();
define(clazz).createTableIfRequired(this).merge(this, t);
upgradeDb().define(clazz).createTableIfRequired(this).merge(this, t);
}
public <T> void update(T t) {
Class<?> clazz = t.getClass();
define(clazz).createTableIfRequired(this).update(this, t);
upgradeDb().define(clazz).createTableIfRequired(this).update(this, t);
}
public <T> void delete(T t) {
Class<?> clazz = t.getClass();
upgradeDb().define(clazz).createTableIfRequired(this).delete(this, t);
}
public <T extends Object> Query<T> from(T alias) {
Class<?> clazz = alias.getClass();
define(clazz).createTableIfRequired(this);
upgradeDb().define(clazz).createTableIfRequired(this);
return Query.from(this, alias);
}
Db upgradeDb() {
if (!upgradeChecked.contains(dbUpgrader.getClass())) {
// Flag as checked immediately because calls are nested.
upgradeChecked.add(dbUpgrader.getClass());
JQDatabase model = dbUpgrader.getClass().getAnnotation(JQDatabase.class);
if (model.version() > 0) {
DbVersion v = new DbVersion();
DbVersion dbVersion =
// (SCHEMA="" && TABLE="") == DATABASE
from(v).where(v.schema).is("").and(v.table).is("").selectFirst();
if (dbVersion == null) {
// Database has no version registration, but model specifies
// version. Insert DbVersion entry and return.
DbVersion newDb = new DbVersion(model.version());
insert(newDb);
} else {
// Database has a version registration,
// check to see if upgrade is required.
if ((model.version() > dbVersion.version)
&& (dbUpgrader != null)) {
// Database is an older version than model.
boolean success = dbUpgrader.upgradeDatabase(this,
dbVersion.version, model.version());
if (success) {
dbVersion.version = model.version();
update(dbVersion);
}
}
}
}
}
return this;
}
<T> void upgradeTable(TableDefinition<T> model) {
if (!upgradeChecked.contains(model.getModelClass())) {
// Flag as checked immediately because calls are nested.
upgradeChecked.add(model.getModelClass());
<T> void createTable(Class<T> clazz) {
define(clazz).createTableIfRequired(this);
if (model.tableVersion > 0) {
// Table is using JaQu version tracking.
DbVersion v = new DbVersion();
String schema = StringUtils.isNullOrEmpty(model.schemaName) ? "" : model.schemaName;
DbVersion dbVersion =
from(v).where(v.schema).like(schema).and(v.table)
.like(model.tableName).selectFirst();
if (dbVersion == null) {
// Table has no version registration, but model specifies
// version. Insert DbVersion entry and return.
DbVersion newTable = new DbVersion(model.tableVersion);
newTable.schema = schema;
newTable.table = model.tableName;
insert(newTable);
} else {
// Table has a version registration.
// Check to see if upgrade is required.
if ((model.tableVersion > dbVersion.version)
&& (dbUpgrader != null)) {
// Table is an older version than model.
boolean success = dbUpgrader.upgradeTable(this, schema,
model.tableName, dbVersion.version, model.tableVersion);
if (success) {
dbVersion.version = model.tableVersion;
update(dbVersion);
}
}
}
}
}
}
<T> TableDefinition<T> define(Class<T> clazz) {
......@@ -138,10 +230,32 @@ public class Db {
T t = instance(clazz);
Table table = (Table) t;
Define.define(def, table);
} else if (clazz.isAnnotationPresent(JQTable.class)) {
// Annotated Class skips Define().define() static initializer
T t = instance(clazz);
def.mapObject(t);
}
}
return def;
}
public synchronized void setDbUpgrader(DbUpgrader upgrader) {
if (upgrader == null)
throw new RuntimeException("DbUpgrader may not be NULL!");
if (!upgrader.getClass().isAnnotationPresent(JQDatabase.class))
throw new RuntimeException("DbUpgrader must be annotated with "
+ JQDatabase.class.getSimpleName() + "!");
this.dbUpgrader = upgrader;
upgradeChecked.clear();
}
SQLDialect getDialect() {
return dialect;
}
public Connection getConnection() {
return conn;
}
public void close() {
try {
......@@ -161,8 +275,30 @@ public class Db {
}
}
PreparedStatement prepare(String sql) {
public <T> List<Long> insertAllAndGetKeys(List<T> list) {
List<Long> identities = new ArrayList<Long>();
for (T t : list) {
identities.add(insertAndGetKey(t));
}
return identities;
}
public <T> void updateAll(List<T> list) {
for (T t : list) {
update(t);
}
}
public <T> void deleteAll(List<T> list) {
for (T t : list) {
delete(t);
}
}
PreparedStatement prepare(String sql, boolean returnKey) {
try {
if (returnKey)
return conn.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
return conn.prepareStatement(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.h2.jaqu.Table.JQTable;
import org.h2.jaqu.util.JdbcUtils;
import org.h2.jaqu.util.StringUtils;
import org.h2.jaqu.util.Utils;
/**
* Class to inspect a model and a database for the purposes of model validation
* and automatic model generation. This class finds the svailable schemas and
* tables and serves as the entry point for model generation and validation.
*
*/
public class DbInspector {
Db db;
DatabaseMetaData metadata;
Class<? extends java.util.Date> dateClazz = java.util.Date.class;
public DbInspector(Db db) {
this.db = db;
}
/**
* Set the preferred Date class.
* java.util.Date (default)
* java.sql.Timestamp
*
* @param dateClazz
*/
public void setPreferredDateClass(Class<? extends java.util.Date> dateClazz) {
this.dateClazz = dateClazz;
}
/**
* Generates models class skeletons for schemas and tables.
*
* @param schema (optional)
* @param table (required)
* @param packageName (optional)
* @param annotateSchema (includes schema name in annotation)
* @param trimStrings (trims strings to maxLength of column)
* @return List<String> source code models as strings
*/
public List<String> generateModel(String schema, String table,
String packageName, boolean annotateSchema, boolean trimStrings) {
try {
List<String> models = Utils.newArrayList();
List<TableInspector> tables = findTables(schema, table);
for (TableInspector t : tables) {
t.read(metadata);
String model = t.generateModel(packageName, annotateSchema,
trimStrings);
models.add(model);
}
return models;
} catch (SQLException s) {
throw new RuntimeException(s);
}
}
/**
* Validates a model.
*
* @param <T> type of model
* @param model class
* @param throwOnError
* @return
*/
public <T> List<Validation> validateModel(T model, boolean throwOnError) {
try {
TableInspector inspector = findTable(model);
inspector.read(metadata);
Class clazz = model.getClass();
TableDefinition<T> def = db.define(clazz);
return inspector.validate(def, throwOnError);
} catch (SQLException s) {
throw new RuntimeException(s);
}
}
private DatabaseMetaData metadata() throws SQLException {
if (metadata == null)
metadata = db.getConnection().getMetaData();
return metadata;
}
/**
* Attempts to find a table in the database based on the model definition.
*
* @param <T>
* @param model
* @return
* @throws SQLException
*/
private <T> TableInspector findTable(T model) throws SQLException {
Class clazz = model.getClass();
TableDefinition<T> def = db.define(clazz);
boolean forceUpperCase = metadata().storesUpperCaseIdentifiers();
String sname = (forceUpperCase && def.schemaName != null) ?
def.schemaName.toUpperCase() : def.schemaName;
String tname = forceUpperCase ? def.tableName.toUpperCase() : def.tableName;
List<TableInspector> tables = findTables(sname, tname);
return tables.get(0);
}
/**
* Returns a list of tables
*
* @param schema
* @param table
* @return
* @throws SQLException
*/
private List<TableInspector> findTables(String schema, String table) throws SQLException {
ResultSet rs = null;
try {
rs = metadata().getSchemas();
ArrayList<String> schemaList = Utils.newArrayList();
while (rs.next())
schemaList.add(rs.getString("TABLE_SCHEM"));
JdbcUtils.closeSilently(rs);
// Get JaQu Tables table name.
String jaquTables = DbVersion.class.getAnnotation(JQTable.class).name();
List<TableInspector> tables = Utils.newArrayList();
if (schemaList.size() == 0)
schemaList.add(null);
for (String s : schemaList) {
rs = metadata().getTables(null, s, null, new String[] { "TABLE" });
while (rs.next()) {
String t = rs.getString("TABLE_NAME");
if (!t.equalsIgnoreCase(jaquTables))
// Ignore JaQu versions table
tables.add(new TableInspector(s, t,
metadata().storesUpperCaseIdentifiers(), dateClazz));
}
}
if (StringUtils.isNullOrEmpty(schema) && StringUtils.isNullOrEmpty(table)) {
// All schemas and tables
return tables;
} else {
// schema subset OR table subset OR exact match
List<TableInspector> matches = Utils.newArrayList();
for (TableInspector t : tables) {
if (t.matches(schema, table))
matches.add(t);
}
if (matches.size() == 0)
throw new RuntimeException(
MessageFormat.format("Failed to find schema={0} table={1}",
schema == null ? "" : schema, table == null ? "" : table));
return matches;
}
} finally {
JdbcUtils.closeSilently(rs);
}
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: James Moger
*/
package org.h2.jaqu;
import org.h2.jaqu.Table.JQDatabase;
/**
* Interface which defines a class to handle table changes based on model
* versions.
* <p>
* An implementation of <i>DbUpgrader</i> <b>MUST</b> be annotated with the
* <i>JQDatabase</i> annotation. This annotation defines the expected database
* version number.
*
*/
public interface DbUpgrader {
/**
* Defines method interface to handle database upgrades. This method is only
* called if your <i>DbUpgrader</i> implementation is annotated with
* JQDatabase.
*
* @param db
* @param fromVersion
* @param toVersion
* @return Returns <b>true</b> for successful upgrade.<br>
* If update is successful, JaQu automatically updates its version
* registry.
*/
public boolean upgradeDatabase(Db db, int fromVersion, int toVersion);
/**
* Defines method interface to handle table upgrades.
*
* @param db
* @param schema
* @param table
* @param fromVersion
* @param toVersion
* @return Returns <b>true</b> for successful upgrade.<br>
* If update is successful, JaQu automatically updates its version
* registry.
*/
public boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion);
/**
* Default Db Upgrader.
* <p>
* Does <b>NOT</b> handle upgrade requests. Instead, this throws
* RuntimeExceptions.
*/
@JQDatabase(version = 0)
public static class DefaultDbUpgrader implements DbUpgrader {
@Override
public boolean upgradeDatabase(Db db, int fromVersion, int toVersion) {
throw new RuntimeException("Please provide your own DbUpgrader implementation.");
}
@Override
public boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion) {
throw new RuntimeException("Please provide your own DbUpgrader implementation.");
}
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQTable;
/**
* Model class for JaQu to track db and table versions.
*
*/
@JQTable(name="_jq_versions", primaryKey="schemaName tableName", memoryTable=true)
public class DbVersion {
@JQColumn(name="schemaName", allowNull=false)
String schema = "";
@JQColumn(name="tableName", allowNull = false)
String table = "";
@JQColumn(name="version")
Integer version;
public DbVersion() {
}
/**
* Constructor for defining a version entry.
* (SCHEMA="" && TABLE="") == DATABASE
*
* @param version
*/
public DbVersion(int version) {
this.schema = "";
this.table = "";
this.version = version;
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
/**
* Classes implementing this interface can be used as a declaration in a statement.
*/
public interface Declaration {
/**
* Append the SQL to the given statement using the given query.
*
* @param stat the statement to append the SQL to
*/
//## Java 1.5 begin ##
void appendSQL(SQLStatement stat);
//## Java 1.5 end ##
}
......@@ -6,6 +6,8 @@
*/
package org.h2.jaqu;
import org.h2.jaqu.Table.IndexType;
/**
* This class provides utility methods to define primary keys, indexes, and set
* the name of the table.
......@@ -23,7 +25,22 @@ public class Define {
public static void index(Object... columns) {
checkInDefine();
currentTableDefinition.addIndex(columns);
currentTableDefinition.addIndex(IndexType.STANDARD, columns);
}
public static void uniqueIndex(Object... columns) {
checkInDefine();
currentTableDefinition.addIndex(IndexType.UNIQUE, columns);
}
public static void hashIndex(Object column) {
checkInDefine();
currentTableDefinition.addIndex(IndexType.HASH, new Object [] { column });
}
public static void uniqueHashIndex(Object column) {
checkInDefine();
currentTableDefinition.addIndex(IndexType.UNIQUE_HASH, new Object [] { column });
}
public static void maxLength(Object column, int length) {
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
/**
* This class represents a "column=(column + 1)" token for a SET statement.
*
* @param <A> the new value data type
*/
//## Java 1.5 begin ##
public class IncrementColumn<T, A> implements Declaration {
private Query<T> query;
private A x;
private A y;
IncrementColumn(Query<T> query, A x) {
this.query = query;
this.x = x;
}
public Query<T> by(A y) {
query.addDeclarationToken(this);
this.y = y;
return query;
}
@Override
public void appendSQL(SQLStatement stat) {
query.appendSQL(stat, x);
stat.appendSQL("=(");
query.appendSQL(stat, x);
if (y instanceof Number) {
Number n = (Number) y;
if (n.doubleValue() > 0)
stat.appendSQL("+");
}
stat.appendSQL(y.toString());
stat.appendSQL(")");
}
}
//## Java 1.5 end ##
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import static org.h2.jaqu.util.StringUtils.isNullOrEmpty;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.h2.jaqu.TableDefinition.FieldDefinition;
/**
* Utility methods for models related to type mapping, default value validation,
* and class or field name creation.
*/
public class ModelUtils {
/**
* Returns a SQL type mapping for a Java class.
*
* @param field the field to map
* @param strictTypeMapping throws a RuntimeException if type is unsupported
* @return
*/
public static String getDataType(FieldDefinition fieldDef, boolean strictTypeMapping) {
Class<?> fieldClass = fieldDef.field.getType();
if (supportedTypes.containsKey(fieldClass)) {
String sqltype = supportedTypes.get(fieldClass);
if (sqltype.equals("VARCHAR") && fieldDef.maxLength <= 0)
// Unspecified length strings are TEXT, not VARCHAR
return "TEXT";
return sqltype;
}
if (!strictTypeMapping)
return "VARCHAR";
else
throw new RuntimeException("Unsupported type " + fieldClass.getName());
}
@SuppressWarnings("serial")
// Used by Runtime Mapping for CREATE statements
static Map<Class<?>, String>
supportedTypes = new HashMap<Class<?>, String>() {
{
put(String.class, "VARCHAR");
put(Boolean.class, "BIT");
put(Byte.class, "TINYINT");
put(Short.class, "SMALLINT");
put(Integer.class, "INT");
put(Long.class, "BIGINT");
put(Float.class, "REAL");
put(Double.class, "DOUBLE");
put(BigDecimal.class, "DECIMAL");
put(java.sql.Timestamp.class, "TIMESTAMP");
put(java.util.Date.class, "TIMESTAMP");
put(java.sql.Date.class, "DATE");
put(java.sql.Time.class, "TIME");
// TODO add blobs, binary types, custom types?
}
};
/**
* Returns the Java class type for a given SQL type.
*
* @param sqlType
* @param dateClazz the preferred date class (java.util.Date or java.sql.Timestamp)
* @return
*/
public static Class<?> getClassType(String sqlType,
Class<? extends java.util.Date> dateClazz) {
sqlType = sqlType.toUpperCase();
// FIXME dropping "UNSIGNED" or parts like that. could be trouble.
sqlType = sqlType.split(" ")[0].trim();
if (sqlTypes.containsKey(sqlType))
// Marshall sqlType to a standard type
sqlType = sqlTypes.get(sqlType);
Class<?> mappedClazz = null;
for (Class<?> clazz : supportedTypes.keySet())
if (supportedTypes.get(clazz).equalsIgnoreCase(sqlType)) {
mappedClazz = clazz;
break;
}
if (mappedClazz != null) {
if (mappedClazz.equals(java.util.Date.class)
|| mappedClazz.equals(java.sql.Timestamp.class))
return dateClazz;
return mappedClazz;
}
return null;
}
// Marshall SQL type aliases to the list of supported types.
// Used by Generation and Validation
static Map<String, String> sqlTypes = new HashMap<String, String>() {
{
// Strings
put("CHAR", "VARCHAR");
put("CHARACTER", "VARCHAR");
put("NCHAR", "VARCHAR");
put("VARCHAR_CASESENSITIVE", "VARCHAR");
put("VARCHAR_IGNORECASE", "VARCHAR");
put("LONGVARCHAR", "VARCHAR");
put("VARCHAR2", "VARCHAR");
put("NVARCHAR", "VARCHAR");
put("NVARCHAR2", "VARCHAR");
put("TEXT", "VARCHAR");
put("NTEXT", "VARCHAR");
put("TINYTEXT", "VARCHAR");
put("MEDIUMTEXT", "VARCHAR");
put("LONGTEXT", "VARCHAR");
put("CLOB", "VARCHAR");
put("NCLOB", "VARCHAR");
// Logic
put("BOOL", "BIT");
put("BOOLEAN", "BIT");
// Whole Numbers
put("BYTE", "TINYINT");
put("INT2", "SMALLINT");
put("YEAR", "SMALLINT");
put("INTEGER", "INT");
put("MEDIUMINT", "INT");
put("INT4", "INT");
put("SIGNED", "INT");
put("INT8", "BIGINT");
put("IDENTITY", "BIGINT");
// Decimals
put("NUMBER", "DECIMAL");
put("DEC", "DECIMAL");
put("NUMERIC", "DECIMAL");
put("FLOAT", "DOUBLE");
put("FLOAT4", "DOUBLE");
put("FLOAT8", "DOUBLE");
// Dates
put("DATETIME", "TIMESTAMP");
put("SMALLDATETIME", "TIMESTAMP");
}
};
/**
* Tries to create a CamelCase class name from a table.
*
* @param name
* @return
*/
public static String createClassName(String name) {
String[] chunks = name.split("_");
StringBuilder newName = new StringBuilder();
for (String chunk : chunks) {
if (chunk.length() == 0)
// leading or trailing _
continue;
newName.append(Character.toUpperCase(chunk.charAt(0)));
newName.append(chunk.substring(1).toLowerCase());
}
return newName.toString();
}
/**
* Ensures that table column names don't collide with Java keywords.
*
* @param col
* @return
*/
public static String createFieldName(String col) {
String cn = col.toLowerCase();
if (keywords.contains(cn))
cn += "_value";
return cn;
}
@SuppressWarnings("serial")
static List<String> keywords = new ArrayList<String>() {
{
add("abstract");
add("assert");
add("boolean");
add("break");
add("byte");
add("case");
add("catch");
add("char");
add("class");
add("const");
add("continue");
add("default");
add("do");
add("double");
add("else");
add("enum");
add("extends");
add("final");
add("finally");
add("float");
add("for");
add("goto");
add("if");
add("implements");
add("import");
add("instanceof");
add("int");
add("interface");
add("long");
add("native");
add("new");
add("package");
add("private");
add("protected");
add("public");
add("return");
add("short");
add("static");
add("strictfp");
add("super");
add("switch");
add("synchronized");
add("this");
add("throw");
add("throws");
add("transient");
add("try");
add("void");
add("volatile");
add("while");
add("false");
add("null");
add("true");
}
};
/**
* Checks the formatting of JQColumn.defaultValue()
* @param defaultValue
* @return
*/
public static boolean isProperlyFormattedDefaultValue(String defaultValue) {
if (isNullOrEmpty(defaultValue))
return true;
Pattern literalDefault = Pattern.compile("'.*'");
Pattern functionDefault = Pattern.compile("[^'].*[^']");
return literalDefault.matcher(defaultValue).matches()
|| functionDefault.matcher(defaultValue).matches();
}
/**
* Checks to see if the defaultValue matches the Class.
*
* @param modelClazz
* @param defaultValue
* @return
*/
public static boolean isValidDefaultValue(Class<?> modelClazz,
String defaultValue) {
if (defaultValue == null)
// NULL
return true;
if (defaultValue.trim().length() == 0)
// NULL (effectively)
return true;
// FIXME H2 single-quotes literal values. Very Useful.
// MySQL does not single-quote literal values so its hard to
// differentiate a FUNCTION/VARIABLE from a literal value.
// Function/Variable
Pattern functionDefault = Pattern.compile("[^'].*[^']");
if (functionDefault.matcher(defaultValue).matches())
// Hard to validate this since its in the DB. Assume its good.
return true;
// STRING
if (modelClazz == String.class) {
Pattern stringDefault = Pattern.compile("'(.|\\n)*'");
return stringDefault.matcher(defaultValue).matches();
}
String dateRegex = "[0-9]{1,4}[-/\\.][0-9]{1,2}[-/\\.][0-9]{1,2}";
String timeRegex = "[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}";
// TIMESTAMPs
if (modelClazz == java.util.Date.class
|| modelClazz == java.sql.Timestamp.class){
// This may be a little loose....
// 00-00-00 00:00:00
// 00/00/00T00:00:00
// 00.00.00T00:00:00
Pattern pattern = Pattern.compile("'" + dateRegex + "." + timeRegex + "'");
return pattern.matcher(defaultValue).matches();
}
// DATE
if (modelClazz == java.sql.Date.class) {
// This may be a little loose....
// 00-00-00
// 00/00/00
// 00.00.00
Pattern pattern = Pattern.compile("'" + dateRegex + "'");
return pattern.matcher(defaultValue).matches();
}
// TIME
if (modelClazz == java.sql.Time.class) {
// 00:00:00
Pattern pattern = Pattern.compile("'" + timeRegex + "'");
return pattern.matcher(defaultValue).matches();
}
// NUMBER
if (Number.class.isAssignableFrom(modelClazz)) {
// Strip single quotes
String unquoted = defaultValue;
if (unquoted.charAt(0) == '\'')
unquoted = unquoted.substring(1);
if (unquoted.charAt(unquoted.length() - 1) == '\'')
unquoted = unquoted.substring(0, unquoted.length() - 1);
try {
// Delegate to static valueOf() method to parse string
Method m = modelClazz.getMethod("valueOf", String.class);
Object o = m.invoke(null, unquoted);
} catch (NumberFormatException nex) {
return false;
} catch (Throwable t) {
}
}
return true;
}
}
......@@ -8,6 +8,7 @@ package org.h2.jaqu;
//## Java 1.5 begin ##
import java.lang.reflect.Field;
import java.sql.Clob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
......@@ -15,9 +16,10 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import org.h2.jaqu.bytecode.ClassReader;
import org.h2.jaqu.util.StatementLogger;
import org.h2.jaqu.util.JdbcUtils;
import org.h2.jaqu.util.Utils;
import org.h2.util.JdbcUtils;
import org.h2.util.New;
//import org.h2.util.JdbcUtils;
//## Java 1.5 end ##
/**
......@@ -31,10 +33,13 @@ public class Query<T> {
private Db db;
private SelectTable<T> from;
private ArrayList<Token> conditions = Utils.newArrayList();
private ArrayList<Declaration> declarations = Utils.newArrayList();
private ArrayList<SelectTable<?>> joins = Utils.newArrayList();
private final IdentityHashMap<Object, SelectColumn<T>> aliasMap = Utils.newIdentityHashMap();
private ArrayList<OrderExpression<T>> orderByList = Utils.newArrayList();
private Object[] groupByExpressions;
private long limit = 0;
private long offset = 0;
Query(Db db) {
this.db = db;
......@@ -60,7 +65,7 @@ public class Query<T> {
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(rs);
JdbcUtils.closeSilently(rs, true);
}
}
......@@ -102,7 +107,7 @@ public class Query<T> {
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(rs);
JdbcUtils.closeSilently(rs, true);
}
return result;
}
......@@ -112,8 +117,36 @@ public class Query<T> {
stat.appendSQL("DELETE FROM ");
from.appendSQL(stat);
appendWhere(stat);
StatementLogger.delete(stat.getSQL());
return stat.executeUpdate();
}
public <A> SetColumn<T, A> set(A field) {
return new SetColumn<T, A>(this, field);
}
public <A> IncrementColumn<T, A> increment(A field) {
return new IncrementColumn<T, A>(this, field);
}
public int update() {
if (declarations.size() == 0)
throw new RuntimeException("Please specify SET or INCREMENT before calling Update!");
SQLStatement stat = new SQLStatement(db);
stat.appendSQL("UPDATE ");
from.appendSQL(stat);
stat.appendSQL(" SET ");
int i = 0;
for (Declaration declaration:declarations) {
if (i++ > 0) {
stat.appendSQL(", ");
}
declaration.appendSQL(stat);
}
appendWhere(stat);
StatementLogger.update(stat.getSQL());
return stat.executeUpdate();
}
public <X, Z> List<X> selectDistinct(Z x) {
return select(x, true);
......@@ -147,7 +180,7 @@ public class Query<T> {
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(rs);
JdbcUtils.closeSilently(rs, true);
}
return result;
}
......@@ -161,7 +194,12 @@ public class Query<T> {
try {
while (rs.next()) {
try {
X value = (X) rs.getObject(1);
X value;
Object o = rs.getObject(1);
if (Clob.class.isAssignableFrom(o.getClass())) {
value = (X) Utils.convert(o, String.class);
} else
value = (X) o;
result.add(value);
} catch (Exception e) {
throw new RuntimeException(e);
......@@ -170,7 +208,7 @@ public class Query<T> {
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(rs);
JdbcUtils.closeSilently(rs, true);
}
return result;
}
......@@ -180,7 +218,7 @@ public class Query<T> {
}
public <A> QueryWhere<T> where(Filter filter) {
HashMap<String, Object> fieldMap = New.hashMap();
HashMap<String, Object> fieldMap = Utils.newHashMap();
for (Field f : filter.getClass().getDeclaredFields()) {
f.setAccessible(true);
try {
......@@ -212,6 +250,23 @@ public class Query<T> {
}
//## Java 1.5 end ##
/**
* Sets the Limit and Offset of a query.
*
* @return the query
*/
//## Java 1.5 begin ##
public Query<T> limit(long limit) {
this.limit = limit;
return this;
}
public Query<T> offset(long offset) {
this.offset = offset;
return this;
}
//## Java 1.5 end ##
/**
* Order by a number of columns.
*
......@@ -268,6 +323,10 @@ public class Query<T> {
void addConditionToken(Token condition) {
conditions.add(condition);
}
void addDeclarationToken(Declaration declaration) {
declarations.add(declaration);
}
void appendWhere(SQLStatement stat) {
if (!conditions.isEmpty()) {
......@@ -317,6 +376,11 @@ public class Query<T> {
stat.appendSQL(" ");
}
}
if (limit > 0)
db.getDialect().appendLimit(stat, limit);
if (offset > 0)
db.getDialect().appendOffset(stat, offset);
StatementLogger.select(stat.getSQL());
return stat;
}
//## Java 1.5 end ##
......
......@@ -33,6 +33,16 @@ public class QueryWhere<T> {
query.addConditionToken(ConditionAndOr.OR);
return new QueryCondition<T, A>(query, x);
}
public QueryWhere<T> limit(long limit) {
query.limit(limit);
return this;
}
public QueryWhere<T> offset(long offset) {
query.offset(offset);
return this;
}
public <X, Z> List<X> select(Z x) {
return query.select(x);
......@@ -123,6 +133,10 @@ public class QueryWhere<T> {
return query.delete();
}
public int update() {
return query.update();
}
public long selectCount() {
return query.selectCount();
}
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import java.text.MessageFormat;
import org.h2.jaqu.TableDefinition.IndexDefinition;
import org.h2.jaqu.util.StatementBuilder;
import org.h2.jaqu.util.StringUtils;
/**
* Interface that defines points where JaQu can build different statements for
* DB-specific SQL.
*
*/
public interface SQLDialect {
public String tableName(String schema, String table);
public String createIndex(String schema, String table, IndexDefinition index);
public void appendLimit(SQLStatement stat, long limit);
public void appendOffset(SQLStatement stat, long offset);
/**
* Default implementation of an SQL dialect.
* Designed for an H2 database. May be suitable for others.
*/
public static class DefaultSQLDialect implements SQLDialect {
@Override
public String tableName(String schema, String table) {
if (StringUtils.isNullOrEmpty(schema))
return table;
return schema + "." + table;
}
@Override
public String createIndex(String schema, String table, IndexDefinition index) {
StatementBuilder cols = new StatementBuilder();
for (String col:index.columnNames) {
cols.appendExceptFirst(", ");
cols.append(col);
}
String type;
switch(index.type) {
case UNIQUE:
type = " UNIQUE ";
break;
case HASH:
type = " HASH ";
break;
case UNIQUE_HASH:
type = " UNIQUE HASH ";
break;
case STANDARD:
default:
type = " ";
break;
}
return MessageFormat.format("CREATE{0}INDEX IF NOT EXISTS {1} ON {2}({3})",
type, index.indexName, table, cols);
}
@Override
public void appendLimit(SQLStatement stat, long limit) {
stat.appendSQL(" LIMIT " + limit);
}
@Override
public void appendOffset(SQLStatement stat, long offset) {
stat.appendSQL(" OFFSET " + offset);
}
}
}
......@@ -11,6 +11,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import org.h2.jaqu.util.JdbcUtils;
//## Java 1.5 end ##
/**
......@@ -37,6 +38,10 @@ public class SQLStatement {
sql = null;
return this;
}
public SQLStatement appendTable(String schema, String table) {
return appendSQL(db.getDialect().tableName(schema, table));
}
String getSQL() {
if (sql == null) {
......@@ -49,20 +54,42 @@ public class SQLStatement {
params.add(o);
return this;
}
ResultSet executeQuery() {
try {
return prepare().executeQuery();
return prepare(false).executeQuery();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
int executeUpdate() {
PreparedStatement ps = null;
try {
ps = prepare(false);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(ps);
}
}
long executeInsert() {
PreparedStatement ps = null;
try {
return prepare().executeUpdate();
ps = prepare(true);
ps.executeUpdate();
long identity = -1;
ResultSet rs = ps.getGeneratedKeys();
if (rs != null && rs.next())
identity = rs.getLong(1);
JdbcUtils.closeSilently(rs);
return identity;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.closeSilently(ps);
}
}
......@@ -74,8 +101,8 @@ public class SQLStatement {
}
}
private PreparedStatement prepare() {
PreparedStatement prep = db.prepare(getSQL());
private PreparedStatement prepare(boolean returnIdentity) {
PreparedStatement prep = db.prepare(getSQL(), returnIdentity);
for (int i = 0; i < params.size(); i++) {
Object o = params.get(i);
setValue(prep, i + 1, o);
......
......@@ -55,9 +55,9 @@ class SelectTable <T> {
void appendSQL(SQLStatement stat) {
if (query.isJoin()) {
stat.appendSQL(aliasDef.tableName + " AS " + as);
stat.appendTable(aliasDef.schemaName, aliasDef.tableName).appendSQL(" AS " + as);
} else {
stat.appendSQL(aliasDef.tableName);
stat.appendTable(aliasDef.schemaName, aliasDef.tableName);
}
}
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
/**
* This class represents a "column=value" token for a SET statement.
*
* @param <A> the new value data type
*/
//## Java 1.5 begin ##
public class SetColumn<T, A> implements Declaration {
private Query<T> query;
private A x;
private A y;
SetColumn(Query<T> query, A x) {
this.query = query;
this.x = x;
}
public Query<T> to(A y) {
query.addDeclarationToken(this);
this.y = y;
return query;
}
@Override
public void appendSQL(SQLStatement stat) {
query.appendSQL(stat, x);
stat.appendSQL("=?");
stat.addParameter(y);
}
}
//## Java 1.5 end ##
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.jaqu;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A class that implements this interface can be used as a database table.
* A class that implements the JaQu model mapping options.
* <p>
* You may implement the Table interface on your model object and optionally use
* JQColumn annotations.<br>
* <i>This imposes a compile-time and runtime-dependency on JaQu.</i>
* <p>
* <u>OR</u>
* <p>
* You may choose to use the JQTable and JQColumn annotations only.<br>
* <i>This imposes a compile-time and runtime-dependency on this file only.</i>
* <p>
* <b>NOTE</b><br>
* Classes that are annotated with JQTable <b>and</b> implement Table will NOT
* call the define() method.
* <p>
* <b>Supported Data Types</b>
* <table>
* <tr>
* <td>java.lang.String</td>
* <td>VARCHAR (maxLength > 0) / TEXT (maxLength == 0)</td>
* </tr>
* <tr>
* <td>java.lang.Boolean</td>
* <td>BIT</td>
* </tr>
* <tr>
* <td>java.lang.Byte</td>
* <td>TINYINT</td>
* </tr>
* <tr>
* <td>java.lang.Short</td>
* <td>SMALLINT</td>
* </tr>
* <tr>
* <td>java.lang.Integer</td>
* <td>INT</td>
* </tr>
* <tr>
* <td>java.lang.Long</td>
* <td>BIGINT</td>
* </tr>
* <tr>
* <td>java.lang.Float</td>
* <td>REAL</td>
* </tr>
* <tr>
* <td>java.lang.Double</td>
* <td>DOUBLE</td>
* </tr>
* <tr>
* <td>java.math.BigDecimal</td>
* <td>DECIMAL</td>
* </tr>
* <tr>
* <td>java.util.Date</td>
* <td>TIMESTAMP</td>
* </tr>
* <tr>
* <td>java.sql.Date</td>
* <td>DATE</td>
* </tr>
* <tr>
* <td>java.sql.Time</td>
* <td>TIME</td>
* </tr>
* <tr>
* <td>java.sql.Timestamp</td>
* <td>TIMESTAMP</td>
* </tr>
* </table>
* <p>
* <b>Unsupported Data Types</b>
* <ul>
* <li>Binary types (BLOB, etc)
* <li>Custom types
* </ul>
* <p>
* <b>Table and Field Mapping</b>
* <p>
* By default, the mapped table name is the class name and the <i>public</i>
* fields are reflectively mapped, by their name, to columns.
* <p>
* As an alternative, you may specify both the table and column definition by
* annotations.
* <p>
* <b>Table Interface</b>
* <p>
* You may set additional parameters such as table name, primary key, and
* indexes in the <i>define()</i> method.
* <p>
* <b>Annotations</b>
* <p>
* You may use the annotations with or without implementing the Table interface.
* <br>
* The annotations allow you to decouple your model completely from JaQu other
* than this file.
* <p>
* <b>Automatic Model Generation</b>
* <p>
* You may automatically generate model classes as strings with the <i>Db</i>
* and <i>DbInspector</i> objects.
*
* <pre>
* Db db = Db.open(&quot;jdbc:h2:mem:&quot;, &quot;sa&quot;, &quot;sa&quot;);
* DbInspector inspector = new DbInspector(db);
* List&lt;String&gt; models = inspector.generateModel(schema, table, packageName,
* annotateSchema, trimStrings)
* </pre>
*
* OR you may use the <i>GenerateModels</i> tool to generate and save your
* classes to the filesystem.
*
* <pre>
* java -cp h2jaqu.jar org.h2.jaqu.util.GenerateModels
* -url &quot;jdbc:h2:mem:&quot;
* -user sa -password sa -schema schemaName -table tableName
* -package packageName -folder destination
* -annotateSchema false -trimStrings true
* </pre>
*
* <b>Model Validation</b>
* <p>
* You may validate your model class with <i>DbInspector</i> object.<br>
* The DbInspector will report ERRORS, WARNINGS, and SUGGESTIONS to help you.
*
* <pre>
* Db db = Db.open(&quot;jdbc:h2:mem:&quot;, &quot;sa&quot;, &quot;sa&quot;);
* DbInspector inspector = new DbInspector(db);
* List&lt;Validation&gt; remarks = inspector.validateModel(new MyModel(), throwOnError);
* for (Validation remark : remarks)
* System.out.println(remark);
* </pre>
*/
public interface Table {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JQDatabase {
/**
* If <b>version</b> is set to a <i>non-zero</i> value, JaQu will
* maintain a "_jq_versions" table within your database. The
* <i>version</i> number will be used to call to a registered
* <i>DbUpgrader</i> implementation to perform relevant ALTERs or
* whatever.
* <p>
* <b>Default: <i>0</i></b>
* <p>
* <b>NOTE:</b><br>
* You must specify a <i>DbUpgrader</i> on your <i>Db</i> object to
* use this parameter.
*/
int version() default 0;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JQSchema {
/**
* <b>schema</b> may be optionally specified. If it is not specified the
* schema will be ignored.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String name() default "";
}
/**
* Enumeration defining the 4 index types.
*
*/
public static enum IndexType {
STANDARD, UNIQUE, HASH, UNIQUE_HASH;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JQIndex {
/**
* <b>standard</b> indexes may be optionally specified. If not specified,
* these values will be ignored.
* <ul>
* <li>standard = "id, name"
* <li>standard = "id name"
* <li>standard = { "id name", "date" }
* </ul>
* Standard indexes may still be added in the <i>define()</i> method if
* the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String[] standard() default {};
/**
* <b>unique</b> indexes may be optionally specified. If not specified,
* these values will be ignored.
* <ul>
* <li>unique = "id, name"
* <li>unique = "id name"
* <li>unique = { "id name", "date" }
* </ul>
* Unique indexes may still be added in the <i>define()</i> method if
* the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String[] unique() default {};
/**
* <b>hash</b> indexes may be optionally specified. If not specified,
* these values will be ignored.
* <ul>
* <li>hash = "name"
* <li>hash = { "name", "date" }
* </ul>
* Hash indexes may still be added in the <i>define()</i> method if
* the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String[] hash() default {};
/**
* <b>uniqueHash</b> indexes may be optionally specified. If not specified,
* these values will be ignored.
* <ul>
* <li>uniqueHash = "id"
* <li>uniqueHash = "name"
* <li>uniqueHash = { "id", "name" }
* </ul>
* UniqueHash indexes may still be added in the <i>define()</i> method if
* the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String[] uniqueHash() default {};
}
/**
* Annotation to define a table.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JQTable {
/**
* <b>name</b> may be optionally specified. If it is not specified the
* class name will be used as the table name.
* <p>
* The table name may still be overridden in the <i>define()</i> method
* if the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String name() default "";
/**
* <b>primaryKey</b> may be optionally specified. If it is not
* specified, then no primary key will be set by the JQTable annotation.
* You may specify a composite primary key.
* <ul>
* <li>primaryKey = "id, name"
* <li>primaryKey = "id name"
* </ul>
* The primaryKey may still be overridden in the <i>define()</i> method
* if the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>Unspecified</i></b>
*/
String primaryKey() default "";
/**
* <b>inheritColumns</b> allows this model class to inherit columns from
* its super class. Any JQTable annotation present on the super class is
* ignored.<br>
* <p>
* <b>Default: <i>false</i></b>
*/
boolean inheritColumns() default false;
/**
* <b>createIfRequired</b> allows user to control whether or not
* JaQu tries to create the table and indexes.
* <p>
* <b>Default: <i>true</i></b>
*/
boolean createIfRequired() default true;
/**
* <b>strictTypeMapping</b> allows user to specify that only supported
* types are mapped.<br>
* If set <i>true</i>, unsupported mapped types will throw a
* RuntimeException.<br>
* If set <i>false</i>, unsupported mapped types will default to
* VARCHAR.
* <p>
* <b>Default: <i>true</i></b>
*/
boolean strictTypeMapping() default true;
/**
* <b>annotationsOnly</b> controls reflective field mapping on your
* model object. If set <i>true</i>, only fields that are explicitly
* annotated as JQColumn are mapped.
* <p>
* <b>Default: <i>true</i></b>
*/
boolean annotationsOnly() default true;
/**
* If <b>memoryTable</b> is set <i>true</i>, this table is created as a
* memory table where data is persistent, but index data is kept in main
* memory.<br>
* The JDBC Connection class is verified before applying this property
* in the CREATE phase.
* <p>
* <b>Default: <i>false</i></b>
* <p>
* <u>Valid only for H2 databases.</u>
*/
boolean memoryTable() default false;
/**
* If <b>version</b> is set to a <i>non-zero</i> value, JaQu will
* maintain a "_jq_versions" table within your database. The
* <i>version</i> number will be used to call to a registered
* <i>DbUpgrader</i> implementation to perform relevant ALTERs or
* whatever.
* <p>
* <b>Default: <i>0</i></b>
* <p>
* <bNOTE:</b><br>
* You must specify a <i>DbUpgrader</i> on your <i>Db</i> object to
* use this parameter.
*/
int version() default 0;
}
/**
* Annotation to define a Column. Annotated fields may have any Scope with
* the understanding that under some circumstances, the JVM may raise a
* SecurityException.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JQColumn {
/**
* If <b>name</b> is not specified the instance variable field name will
* be used as the column name.
* <p>
* <b>Default: <i>reflective field name mapping</i></b>
*/
String name() default "";
/**
* If <b>primaryKey</b> is true, this column will be the PrimaryKey.
* <p>
* <b>Default: <i>false</i></b>
*/
boolean primaryKey() default false;
/**
* If <b>autoIncrement</b> is true, the column will be created with a
* sequence as the default value.
* <p>
* <b>Default: <i>false</i></b>
*/
boolean autoIncrement() default false;
/**
* If <b>maxLength</b> > 0 it is used during the CREATE TABLE phase. It
* may also be optionally used to prevent database exceptions on INSERT
* and UPDATE statements (see <i>trimString</i>).
* <p>
* Any maxLength set in <i>define()</i> may override this annotation
* setting if the model class is not annotated with JQTable.
* <p>
* <b>Default: <i>0</i></b>
*/
int maxLength() default 0;
/**
* If <b>trimString</b> is <i>true</i> JaQu will automatically trim the
* string if it exceeds <b>maxLength</b>.
* <p>
* e.g. stringValue = stringValue.substring(0, maxLength)
* <p>
* <b>Default: <i>false</i></b>
*/
boolean trimString() default false;
/**
* If <b>allowNull</b> is <i>false</i> then JaQu will set
* the column NOT NULL during the CREATE TABLE phase.
* <p>
* <b>Default: <i>false</i></b>
*/
boolean allowNull() default false;
/**
* <b>defaultValue</b> is the value assigned to the column during the
* CREATE TABLE phase.
* <p>
* To set <b>null</b>, defaultValue="" (default)
* <p>
* This field could contain a literal <u>single-quoted value</u>.<br>
* Or a function call.<br>
* Empty strings will be considered NULL.
* <ul>
* <li>defaultValue="" (null)
* <li>defaultValue="CURRENT_TIMESTAMP" (H2 current_timestamp())
* <li>defaultValue="''" (default empty string)
* <li>defaultValue="'0'" (default number)
* <li>defaultValue="'1970-01-01 00:00:01'" (default date)
* </ul>
* if (
* <ul>
* <li>defaultValue is properly specified
* <li>AND <i>autoIncrement</i> == false
* <li>AND <i>primaryKey</i> == false
* </ul>
* )<br>
* then this value will be included in the "DEFAULT ..." phrase of a
* column during the CREATE TABLE process.
* <p>
* <b>Default: <i>unspecified, null</i></b>
*/
String defaultValue() default "";
}
/**
* This method is called to let the table define the primary key, indexes,
* and the table name.
......
......@@ -8,15 +8,24 @@ package org.h2.jaqu;
//## Java 1.5 begin ##
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.h2.jaqu.Table.IndexType;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQIndex;
import org.h2.jaqu.Table.JQSchema;
import org.h2.jaqu.Table.JQTable;
import org.h2.jaqu.util.StatementLogger;
import org.h2.jaqu.util.StatementBuilder;
import org.h2.jaqu.util.StringUtils;
import org.h2.jaqu.util.Utils;
import org.h2.util.StatementBuilder;
/**
* A table definition contains the index definitions of a table, the field
......@@ -33,9 +42,9 @@ class TableDefinition<T> {
*/
//## Java 1.5 begin ##
static class IndexDefinition {
boolean unique;
IndexType type;
String indexName;
List<String> columnNames;
Set<String> columnNames;
}
//## Java 1.5 end ##
......@@ -49,6 +58,10 @@ class TableDefinition<T> {
String dataType;
int maxLength;
boolean isPrimaryKey;
boolean isAutoIncrement;
boolean trimString;
boolean allowNull;
String defaultValue;
Object getValue(Object obj) {
try {
......@@ -65,6 +78,8 @@ class TableDefinition<T> {
void setValue(Object obj, Object o) {
try {
if (!field.isAccessible())
field.setAccessible(true);
o = Utils.convert(o, field.getType());
field.set(obj, o);
} catch (Exception e) {
......@@ -80,24 +95,37 @@ class TableDefinition<T> {
}
}
}
private boolean createTableIfRequired = true;
String schemaName;
String tableName;
private Class<T> clazz;
private ArrayList<FieldDefinition> fields = Utils.newArrayList();
private IdentityHashMap<Object, FieldDefinition> fieldMap =
Utils.newIdentityHashMap();
private List<String> primaryKeyColumnNames;
private Set<String> primaryKeyColumnNames;
private ArrayList<IndexDefinition> indexes = Utils.newArrayList();
private boolean memoryTable = false;
int tableVersion = 0;
TableDefinition(Class<T> clazz) {
this.clazz = clazz;
tableName = clazz.getSimpleName();
schemaName = null;
tableName = clazz.getSimpleName();
}
Class<T> getModelClass() {
return clazz;
}
List<FieldDefinition> getFields() {
return fields;
}
void setSchemaName(String schemaName) {
this.schemaName = schemaName;
}
void setTableName(String tableName) {
this.tableName = tableName;
}
......@@ -111,23 +139,41 @@ class TableDefinition<T> {
}
}
void setPrimaryKey(List<String> primaryKeyColumnNames) {
this.primaryKeyColumnNames = Utils.newHashSet(primaryKeyColumnNames);
// set isPrimaryKey flag for all field definitions
for (FieldDefinition fieldDefinition : fieldMap.values()) {
fieldDefinition.isPrimaryKey = this.primaryKeyColumnNames
.contains(fieldDefinition.columnName);
}
}
<A> String getColumnName(A fieldObject) {
FieldDefinition def = fieldMap.get(fieldObject);
return def == null ? null : def.columnName;
}
private List<String> mapColumnNames(Object[] columns) {
List<String> columnNames = Utils.newArrayList();
private Set<String> mapColumnNames(Object[] columns) {
Set<String> columnNames = Utils.newHashSet();
for (Object column : columns) {
columnNames.add(getColumnName(column));
}
return columnNames;
}
void addIndex(Object[] columns) {
void addIndex(IndexType type, Object[] columns) {
IndexDefinition index = new IndexDefinition();
index.indexName = tableName + "_" + indexes.size();
index.columnNames = mapColumnNames(columns);
index.type = type;
indexes.add(index);
}
void addIndex(IndexType type, List<String> columnNames) {
IndexDefinition index = new IndexDefinition();
index.indexName = tableName + "_" + indexes.size();
index.columnNames = Utils.newHashSet(columnNames);
index.type = type;
indexes.add(index);
}
......@@ -142,43 +188,89 @@ class TableDefinition<T> {
}
void mapFields() {
Field[] classFields = clazz.getFields();
boolean byAnnotationsOnly = false;
boolean inheritColumns = false;
boolean strictTypeMapping = false;
if (clazz.isAnnotationPresent(JQTable.class)) {
JQTable tableAnnotation = clazz.getAnnotation(JQTable.class);
byAnnotationsOnly = tableAnnotation.annotationsOnly();
inheritColumns = tableAnnotation.inheritColumns();
strictTypeMapping = tableAnnotation.strictTypeMapping();
}
List<Field> classFields = Utils.newArrayList();
classFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
if (inheritColumns) {
Class<?> superClazz = clazz.getSuperclass();
classFields.addAll(Arrays.asList(superClazz.getDeclaredFields()));
}
for (Field f : classFields) {
FieldDefinition fieldDef = new FieldDefinition();
fieldDef.field = f;
fieldDef.columnName = f.getName();
fieldDef.dataType = getDataType(f);
fields.add(fieldDef);
String columnName = f.getName(); // default to field name
boolean isAutoIncrement = false;
boolean isPrimaryKey = false;
int maxLength = 0;
boolean trimString = false;
boolean allowNull = true;
String defaultValue = "";
boolean hasAnnotation = f.isAnnotationPresent(JQColumn.class);
if (hasAnnotation) {
JQColumn col = f.getAnnotation(JQColumn.class);
if (!StringUtils.isNullOrEmpty(col.name()))
columnName = col.name();
isAutoIncrement = col.autoIncrement();
isPrimaryKey = col.primaryKey();
maxLength = col.maxLength();
trimString = col.trimString();
allowNull = col.allowNull();
defaultValue = col.defaultValue();
}
boolean isPublic = Modifier.isPublic(f.getModifiers());
boolean reflectiveMatch = isPublic && !byAnnotationsOnly;
if (reflectiveMatch || hasAnnotation) {
FieldDefinition fieldDef = new FieldDefinition();
fieldDef.field = f;
fieldDef.columnName = columnName;
fieldDef.isAutoIncrement = isAutoIncrement;
fieldDef.isPrimaryKey = isPrimaryKey;
fieldDef.maxLength = maxLength;
fieldDef.trimString = trimString;
fieldDef.allowNull = allowNull;
fieldDef.defaultValue= defaultValue;
fieldDef.dataType = ModelUtils.getDataType(fieldDef, strictTypeMapping);
fields.add(fieldDef);
}
}
List<String> primaryKey = Utils.newArrayList();
for (FieldDefinition fieldDef : fields) {
if (fieldDef.isPrimaryKey)
primaryKey.add(fieldDef.columnName);
}
if (primaryKey.size() > 0)
setPrimaryKey(primaryKey);
}
private static String getDataType(Field field) {
Class<?> fieldClass = field.getType();
if (fieldClass == Integer.class) {
return "INT";
} else if (fieldClass == String.class) {
return "VARCHAR";
} else if (fieldClass == Double.class) {
return "DOUBLE";
} else if (fieldClass == java.math.BigDecimal.class) {
return "DECIMAL";
} else if (fieldClass == java.util.Date.class) {
return "DATE";
} else if (fieldClass == java.sql.Date.class) {
return "DATE";
} else if (fieldClass == java.sql.Time.class) {
return "TIME";
} else if (fieldClass == java.sql.Timestamp.class) {
return "TIMESTAMP";
}
return "VARCHAR";
// TODO add more data types
// Optionally truncates strings to maxLength
private Object getValue(Object obj, FieldDefinition field) {
Object value = field.getValue(obj);
if (field.trimString && field.maxLength > 0) {
if (value instanceof String) {
// Clip Strings
String s = (String) value;
if (s.length() > field.maxLength)
return s.substring(0, field.maxLength);
return s;
}
return value;
} else
// Standard JaQu behavior
return value;
}
void insert(Db db, Object obj) {
long insert(Db db, Object obj, boolean returnKey) {
SQLStatement stat = new SQLStatement(db);
StatementBuilder buff = new StatementBuilder("INSERT INTO ");
buff.append(tableName).append('(');
buff.append(db.getDialect().tableName(schemaName, tableName)).append('(');
for (FieldDefinition field : fields) {
buff.appendExceptFirst(", ");
buff.append(field.columnName);
......@@ -188,12 +280,15 @@ class TableDefinition<T> {
for (FieldDefinition field : fields) {
buff.appendExceptFirst(", ");
buff.append('?');
Object value = field.getValue(obj);
Object value = getValue(obj, field);
stat.addParameter(value);
}
buff.append(')');
stat.setSQL(buff.toString());
stat.executeUpdate();
StatementLogger.insert(stat.getSQL());
if (returnKey)
return stat.executeInsert();
return stat.executeUpdate();
}
void merge(Db db, Object obj) {
......@@ -203,7 +298,7 @@ class TableDefinition<T> {
}
SQLStatement stat = new SQLStatement(db);
StatementBuilder buff = new StatementBuilder("MERGE INTO ");
buff.append(tableName).append(" (");
buff.append(db.getDialect().tableName(schemaName, tableName)).append(" (");
buff.resetCount();
for (FieldDefinition field : fields) {
buff.appendExceptFirst(", ");
......@@ -223,11 +318,12 @@ class TableDefinition<T> {
for (FieldDefinition field : fields) {
buff.appendExceptFirst(", ");
buff.append('?');
Object value = field.getValue(obj);
Object value = getValue(obj, field);
stat.addParameter(value);
}
buff.append(')');
stat.setSQL(buff.toString());
StatementLogger.merge(stat.getSQL());
stat.executeUpdate();
}
......@@ -238,20 +334,52 @@ class TableDefinition<T> {
}
SQLStatement stat = new SQLStatement(db);
StatementBuilder buff = new StatementBuilder("UPDATE ");
buff.append(tableName).append(" SET ");
buff.append(db.getDialect().tableName(schemaName, tableName)).append(" SET ");
buff.resetCount();
for (FieldDefinition field : fields) {
if (!field.isPrimaryKey) {
buff.appendExceptFirst(", ");
buff.append(field.columnName);
buff.append(" = ?");
Object value = field.getValue(obj);
Object value = getValue(obj, field);
stat.addParameter(value);
}
}
Object alias = Utils.newObject(obj.getClass());
Query<Object> query = Query.from(db, alias);
boolean firstCondition = true;
for (FieldDefinition field : fields) {
if (field.isPrimaryKey) {
Object aliasValue = field.getValue(alias);
Object value = field.getValue(obj);
if (!firstCondition) {
query.addConditionToken(ConditionAndOr.AND);
}
firstCondition = false;
query.addConditionToken(
new Condition<Object>(
aliasValue, value, CompareType.EQUAL));
}
}
stat.setSQL(buff.toString());
query.appendWhere(stat);
StatementLogger.update(stat.getSQL());
stat.executeUpdate();
}
void delete(Db db, Object obj) {
if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) {
throw new IllegalStateException("No primary key columns defined "
+ "for table " + obj.getClass() + " - no update possible");
}
SQLStatement stat = new SQLStatement(db);
StatementBuilder buff = new StatementBuilder("DELETE FROM ");
buff.append(db.getDialect().tableName(schemaName, tableName));
buff.resetCount();
Object alias = Utils.newObject(obj.getClass());
Query<Object> query = Query.from(db, alias);
boolean firstCondition = true;
for (FieldDefinition field : fields) {
if (field.isPrimaryKey) {
Object aliasValue = field.getValue(alias);
......@@ -267,21 +395,60 @@ class TableDefinition<T> {
}
stat.setSQL(buff.toString());
query.appendWhere(stat);
StatementLogger.delete(stat.getSQL());
stat.executeUpdate();
}
TableDefinition<T> createTableIfRequired(Db db) {
if (!createTableIfRequired) {
// Skip table and index creation
// But still check for upgrades
db.upgradeTable(this);
return this;
}
SQLStatement stat = new SQLStatement(db);
StatementBuilder buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS ");
buff.append(tableName).append('(');
StatementBuilder buff;
if (memoryTable &&
db.getConnection().getClass()
.getCanonicalName().equals("org.h2.jdbc.JdbcConnection"))
buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS ");
else
buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS ");
buff.append(db.getDialect().tableName(schemaName, tableName)).append('(');
for (FieldDefinition field : fields) {
buff.appendExceptFirst(", ");
buff.appendExceptFirst(", ");
buff.append(field.columnName).append(' ').append(field.dataType);
if (field.maxLength != 0) {
// FIELD LENGTH
if (field.maxLength > 0) {
buff.append('(').append(field.maxLength).append(')');
}
// AUTO_INCREMENT
if (field.isAutoIncrement) {
buff.append(" AUTO_INCREMENT");
}
// NOT NULL
if (!field.allowNull) {
buff.append(" NOT NULL");
}
// DEFAULT...
if (!field.isAutoIncrement && !field.isPrimaryKey) {
String dv = field.defaultValue;
if (!StringUtils.isNullOrEmpty(dv)) {
if (ModelUtils.isProperlyFormattedDefaultValue(dv)
&& ModelUtils.isValidDefaultValue(field.field.getType(), dv)) {
buff.append(" DEFAULT " + dv);
}
}
}
}
if (primaryKeyColumnNames != null) {
// PRIMARY KEY...
if (primaryKeyColumnNames != null && primaryKeyColumnNames.size() > 0) {
buff.append(", PRIMARY KEY(");
buff.resetCount();
for (String n : primaryKeyColumnNames) {
......@@ -292,14 +459,94 @@ class TableDefinition<T> {
}
buff.append(')');
stat.setSQL(buff.toString());
StatementLogger.create(stat.getSQL());
stat.executeUpdate();
// TODO create indexes
// Create Indexes
for (IndexDefinition index:indexes) {
String sql = db.getDialect().createIndex(schemaName, tableName, index);
stat.setSQL(sql);
StatementLogger.create(stat.getSQL());
stat.executeUpdate();
}
// Table is created IF NOT EXISTS, otherwise statement is ignored
// But we still need to process potential Upgrade
db.upgradeTable(this);
return this;
}
// Retrieve list of columns from CSV whitespace notated index
private List<String> getColumns(String index) {
List<String> cols = Utils.newArrayList();
if (index == null || index.length() == 0)
return null;
String[] cs = index.split("(,|\\s)");
for (String c : cs)
if (c != null && c.trim().length() > 0)
cols.add(c.trim());
if (cols.size() == 0)
return null;
return cols;
}
void mapObject(Object obj) {
fieldMap.clear();
initObject(obj, fieldMap);
if (clazz.isAnnotationPresent(JQSchema.class)) {
JQSchema schemaAnnotation = clazz.getAnnotation(JQSchema.class);
// Setup Schema name mapping, if properly annotated
if (!StringUtils.isNullOrEmpty(schemaAnnotation.name()))
schemaName = schemaAnnotation.name();
}
if (clazz.isAnnotationPresent(JQTable.class)) {
JQTable tableAnnotation = clazz.getAnnotation(JQTable.class);
// Setup Table name mapping, if properly annotated
if (!StringUtils.isNullOrEmpty(tableAnnotation.name()))
tableName = tableAnnotation.name();
// Allow control over createTableIfRequired()
createTableIfRequired = tableAnnotation.createIfRequired();
// Model Version
if (tableAnnotation.version() > 0)
tableVersion = tableAnnotation.version();
// Setup the Primary Index, if properly annotated
List<String> primaryKey = getColumns(tableAnnotation.primaryKey());
if (primaryKey != null)
setPrimaryKey(primaryKey);
}
if (clazz.isAnnotationPresent(JQIndex.class)) {
JQIndex indexAnnotation = clazz.getAnnotation(JQIndex.class);
// Setup the indexes, if properly annotated
addIndexes(IndexType.STANDARD, indexAnnotation.standard());
addIndexes(IndexType.UNIQUE, indexAnnotation.unique());
addIndexes(IndexType.HASH, indexAnnotation.hash());
addIndexes(IndexType.UNIQUE_HASH, indexAnnotation.uniqueHash());
}
}
void addIndexes(IndexType type, String [] indexes) {
for (String index:indexes) {
List<String> validatedColumns = getColumns(index);
if (validatedColumns == null)
return;
addIndex(type, validatedColumns);
}
}
List<IndexDefinition> getIndexes(IndexType type) {
List<IndexDefinition> list = Utils.newArrayList();
for (IndexDefinition def:indexes)
if (def.type.equals(type))
list.add(def);
return list;
}
void initObject(Object obj, Map<Object, FieldDefinition> map) {
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import static java.text.MessageFormat.format;
import static org.h2.jaqu.Validation.CONSIDER;
import static org.h2.jaqu.Validation.ERROR;
import static org.h2.jaqu.Validation.WARN;
import static org.h2.jaqu.util.JdbcUtils.closeSilently;
import static org.h2.jaqu.util.StringUtils.isNullOrEmpty;
import java.lang.reflect.Modifier;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.h2.jaqu.Table.IndexType;
import org.h2.jaqu.Table.JQColumn;
import org.h2.jaqu.Table.JQIndex;
import org.h2.jaqu.Table.JQSchema;
import org.h2.jaqu.Table.JQTable;
import org.h2.jaqu.TableDefinition.FieldDefinition;
import org.h2.jaqu.TableDefinition.IndexDefinition;
import org.h2.jaqu.util.StatementBuilder;
import org.h2.jaqu.util.Utils;
/**
* Class to inspect the contents of a particular table including its indexes.
* This class does the bulk of the work in terms of model generation and model
* validation.
*/
public class TableInspector {
String schema;
String table;
boolean forceUpperCase;
Class<? extends java.util.Date> dateClazz;
List<String> primaryKeys = Utils.newArrayList();
Map<String, IndexInspector> indexes;
Map<String, ColumnInspector> columns;
final String eol = "\n";
TableInspector(String schema, String table, boolean forceUpperCase,
Class<? extends java.util.Date> dateClazz) {
this.schema = schema;
this.table = table;
this.forceUpperCase = forceUpperCase;
this.dateClazz = dateClazz;
}
/**
* Tests to see if this TableInspector represents schema.table.
* <p>
* @param schema
* @param table
* @return
*/
boolean matches(String schema, String table) {
if (isNullOrEmpty(schema))
// table name matching
return this.table.equalsIgnoreCase(table);
else if (isNullOrEmpty(table))
// schema name matching
return this.schema.equalsIgnoreCase(schema);
else
// exact table matching
return this.schema.equalsIgnoreCase(schema)
&& this.table.equalsIgnoreCase(table);
}
/**
* Reads the DatabaseMetaData for the details of this table including
* primary keys and indexes.
* <p>
* @param metadata
* @throws SQLException
*/
void read(DatabaseMetaData metadata) throws SQLException {
ResultSet rs = null;
// Primary Keys
try {
rs = metadata.getPrimaryKeys(null, schema, table);
while (rs.next()) {
String c = rs.getString("COLUMN_NAME");
primaryKeys.add(c);
}
closeSilently(rs);
// Indexes
rs = metadata.getIndexInfo(null, schema, table, false, true);
indexes = Utils.newHashMap();
while (rs.next()) {
IndexInspector info = new IndexInspector(rs);
if (info.type.equals(IndexType.UNIQUE)
&& info.name.toLowerCase().startsWith("primary"))
// Skip PrimaryKey indexes
continue;
if (indexes.containsKey(info.name))
indexes.get(info.name).addColumn(rs);
else
indexes.put(info.name, info);
}
closeSilently(rs);
// Columns
rs = metadata.getColumns(null, schema, table, null);
columns = Utils.newHashMap();
while (rs.next()) {
ColumnInspector col = new ColumnInspector();
col.name = rs.getString("COLUMN_NAME");
col.type = rs.getString("TYPE_NAME");
col.clazz = ModelUtils.getClassType(col.type, dateClazz);
col.size = rs.getInt("COLUMN_SIZE");
// Allow Null
try {
col.allowNull = (rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
} catch (SQLException x) {
}
// AutoIncrement
try {
col.isAutoIncrement = rs.getBoolean("IS_AUTOINCREMENT");
} catch (SQLException x) {
}
// Primary Key
if (primaryKeys.size() == 1) {
if (col.name.equalsIgnoreCase(primaryKeys.get(0)))
col.isPrimaryKey = true;
}
// Default Value
if (!col.isAutoIncrement) {
try {
col.defaultValue = rs.getString("COLUMN_DEFAULT");
} catch (SQLException t) {
try {
col.defaultValue = rs.getString("COLUMN_DEF");
} catch (SQLException x) {
}
}
}
columns.put(col.name, col);
}
} finally {
closeSilently(rs);
}
}
/**
* Generates a model (class definition) from this table.
* The model includes indexes, primary keys, default values, maxLengths,
* and allowNull information.
* <p>
* The caller may optionally set a destination package name, whether or not
* ot include the schema name (setting schema can be a problem when using
* the model between databases), and if to automatically trim strings for
* those that have a maximum length.
* <p>
* @param packageName
* @param annotateSchema
* @param trimStrings
* @return
*/
String generateModel(String packageName, boolean annotateSchema,
boolean trimStrings) {
// Set of imports
Set<String> imports = Utils.newHashSet();
imports.add(JQSchema.class.getCanonicalName());
imports.add(JQTable.class.getCanonicalName());
imports.add(JQIndex.class.getCanonicalName());
imports.add(JQColumn.class.getCanonicalName());
// Table Fields
StringBuilder fields = new StringBuilder();
List<ColumnInspector> sortedColumns = Utils.newArrayList(columns.values());
Collections.sort(sortedColumns);
for (ColumnInspector col : sortedColumns)
fields.append(generateColumn(imports, col, trimStrings));
// Build Complete Class Definition
StringBuilder model = new StringBuilder();
if (!isNullOrEmpty(packageName)) {
// Package
model.append("package " + packageName + ";");
model.append(eol).append(eol);
}
// Imports
List<String> sortedImports = new ArrayList<String>(imports);
Collections.sort(sortedImports);
for (String imp : sortedImports)
model.append("import ").append(imp).append(';').append(eol);
model.append(eol);
// @JQSchema
if (annotateSchema && !isNullOrEmpty(schema)) {
model.append('@').append(JQSchema.class.getSimpleName());
model.append('(');
AnnotationBuilder ap = new AnnotationBuilder();
ap.addParameter("name", schema);
model.append(ap);
model.append(')').append(eol);
}
// @JQTable
model.append('@').append(JQTable.class.getSimpleName());
model.append('(');
// JQTable Annotation Parameters
AnnotationBuilder ap = new AnnotationBuilder();
ap.addParameter("name", table);
if (primaryKeys.size() > 1) {
StringBuilder pk = new StringBuilder();
for (String key : primaryKeys)
pk.append(key).append(' ');
pk.trimToSize();
ap.addParameter("primaryKey", pk.toString());
}
// Finish @JQTable annotation
model.append(ap);
model.append(')').append(eol);
// @JQIndex
ap = new AnnotationBuilder();
generateIndexAnnotations(ap, "standard", IndexType.STANDARD);
generateIndexAnnotations(ap, "unique", IndexType.UNIQUE);
generateIndexAnnotations(ap, "hash", IndexType.HASH);
generateIndexAnnotations(ap, "uniqueHash", IndexType.UNIQUE_HASH);
if (ap.length() > 0) {
model.append('@').append(JQIndex.class.getSimpleName());
model.append('(');
model.append(ap);
model.append(')').append(eol);
}
// Class Declaration
String clazzName = ModelUtils.createClassName(table);
model.append(format("public class {0} '{'", clazzName)).append(eol);
model.append(eol);
// Field Declarations
model.append(fields);
// Default Constructor
model.append("\tpublic ").append(clazzName).append("() {").append(eol);
model.append("\t}").append(eol);
// End of Class Body
model.append('}');
model.trimToSize();
return model.toString();
}
/**
* Generates the specified index annotation.
* @param ap
*/
void generateIndexAnnotations(AnnotationBuilder ap, String parameter, IndexType type) {
List<IndexInspector> list = getIndexes(type);
if (list.size() == 0)
// No matching indexes
return;
if (list.size() == 1) {
ap.addParameter(parameter, list.get(0).getColumnsString());
} else {
List<String> parameters = Utils.newArrayList();
for (IndexInspector index:list)
parameters.add(index.getColumnsString());
ap.addParameter(parameter, parameters);
}
}
/**
* Returns indexes of a specific type from the map.
* <p>
* @param type
* @return
*/
List<IndexInspector> getIndexes(IndexType type) {
List<IndexInspector> list = Utils.newArrayList();
for (IndexInspector index:indexes.values())
if (index.type.equals(type))
list.add(index);
return list;
}
/**
* Generates a column field definition with annotations.
* <p>
* @param imports
* @param col
* @param trimStrings
* @return
*/
StatementBuilder generateColumn(Set<String> imports, ColumnInspector col,
boolean trimStrings) {
StatementBuilder sb = new StatementBuilder();
Class<?> clazz = col.clazz;
String cname = ModelUtils.createFieldName(col.name.toLowerCase());
sb.append('\t');
if (clazz == null) {
// Unsupported Type
clazz = Object.class;
sb.append("// Unsupported type " + col.type);
} else {
// @JQColumn
imports.add(clazz.getCanonicalName());
sb.append('@').append(JQColumn.class.getSimpleName());
// JQColumn Annotation Parameters
AnnotationBuilder ap = new AnnotationBuilder();
// JQColumn.name
if (!col.name.equalsIgnoreCase(cname)) {
ap.addParameter("name", col.name);
}
// JQColumn.primaryKey
// Composite Primary Keys are annotated on the Table
if (col.isPrimaryKey && primaryKeys.size() == 1) {
ap.addParameter("primaryKey=true");
}
// JQColumn.maxLength
if ((clazz == String.class) && (col.size > 0)
&& (col.size < Integer.MAX_VALUE)) {
ap.addParameter("maxLength", col.size);
// JQColumn.trimStrings
if (trimStrings) {
ap.addParameter("trimString=true");
}
} else {
// JQColumn.AutoIncrement
if (col.isAutoIncrement) {
ap.addParameter("autoIncrement=true");
}
}
// JQColumn.allowNull
if (!col.allowNull) {
ap.addParameter("allowNull=false");
}
// JQColumn.defaultValue
if (!isNullOrEmpty(col.defaultValue))
ap.addParameter("defaultValue=\"" + col.defaultValue + "\"");
// Add leading and trailing ()
if (ap.length() > 0) {
ap.insert(0, '(');
ap.append(')');
}
sb.append(ap);
}
sb.append(eol);
// Variable Declaration
sb.append("\tpublic ");
sb.append(clazz.getSimpleName());
sb.append(' ');
sb.append(cname);
sb.append(';');
sb.append(eol).append(eol);
return sb;
}
/**
* Validates that a table definition (annotated, interface, or both) matches
* the current state of the table and indexes in the database.
* <p>
* Results are returned as a List&lt;Validation&gt; which includes recommendations,
* warnings, and errors about the model.
* <p>
* The caller may choose to have validate throw an exception on any validation
* ERROR.
* <p>
* @param <T>
* @param def
* @param throwError
* @return List&lt;Validation&gt;
*/
<T> List<Validation> validate(TableDefinition<T> def,
boolean throwError) {
List<Validation> remarks = Utils.newArrayList();
// Model Class Definition Validation
if (!Modifier.isPublic(def.getModelClass().getModifiers())) {
remarks.add(ERROR(table, "SCHEMA",
format("Class {0} MUST BE PUBLIC!",
def.getModelClass().getCanonicalName())).throwError(throwError));
}
// Schema Validation
if (!isNullOrEmpty(schema)) {
if (isNullOrEmpty(def.schemaName)) {
remarks.add(CONSIDER(table, "SCHEMA",
format("@{0}(name={1})",
JQSchema.class.getSimpleName(), schema)));
} else if (!schema.equalsIgnoreCase(def.schemaName)) {
remarks.add(ERROR(table, "SCHEMA",
format("@{0}(name={1}) != {2}",
JQSchema.class.getSimpleName(), def.schemaName,
schema)).throwError(throwError));
}
}
// Index Validation
for (IndexInspector index:indexes.values())
validate(remarks, def, index, throwError);
// Field Column Validation
List<FieldDefinition> fieldDefs = def.getFields();
for (FieldDefinition fieldDef : fieldDefs)
validate(remarks, fieldDef, throwError);
return remarks;
}
/**
* Validates an inspected index from the database against the IndexDefinition
* within the TableDefinition.
* <p>
* <b>TODO</b>: Complete index validation
* <p>
* @param <T>
* @param remarks
* @param def
* @param index
* @param throwError
*/
<T> void validate(List<Validation> remarks, TableDefinition<T> def,
IndexInspector index, boolean throwError) {
List<IndexDefinition> defIndexes = def.getIndexes(IndexType.STANDARD);
List<IndexInspector> dbIndexes = getIndexes(IndexType.STANDARD);
if (defIndexes.size() > dbIndexes.size()) {
remarks.add(WARN(table, IndexType.STANDARD.name(), "# of Model Indexes > DB Indexes!"));
} else if (defIndexes.size() < dbIndexes.size()) {
remarks.add(WARN(table, IndexType.STANDARD.name(), "Model class is missing indexes!"));
}
// TODO Complete index validation.
// Need to actually compare index types and columns within each index.
// At this point my head was starting to hurt.
}
/**
* Validates a column against the model's field definition. Checks for
* existence, supported type, type mapping, default value, defined lengths,
* primary key, autoincrement.
* <p>
* @param remarks
* @param fieldDef
* @param throwError
*/
void validate(List<Validation> remarks, FieldDefinition fieldDef,
boolean throwError) {
// Unknown Field
String fname = forceUpperCase ?
fieldDef.columnName.toUpperCase() : fieldDef.columnName;
if (!columns.containsKey(fname)) {
// Unknown column mapping!
remarks.add(ERROR(table, fieldDef,
"Does not exist in database!").throwError(throwError));
return;
}
ColumnInspector col = columns.get(fname);
Class<?> fieldClazz = fieldDef.field.getType();
Class<?> jdbcClazz = ModelUtils.getClassType(col.type, dateClazz);
// Supported Type Check
// JaQu maps to VARCHAR for unsupported types.
if (fieldDef.dataType.equals("VARCHAR")
&& (fieldClazz != String.class))
remarks.add(ERROR(table, fieldDef,
"JaQu does not currently implement support for "
+ fieldClazz.getName()).throwError(throwError));
// Number Types
if (!fieldClazz.equals(jdbcClazz)) {
if (Number.class.isAssignableFrom(fieldClazz)) {
remarks.add(WARN(table, col,
format("Precision Mismatch: ModelObject={0}, ColumnObject={1}",
fieldClazz.getSimpleName(), jdbcClazz.getSimpleName())));
} else {
if (!Date.class.isAssignableFrom(jdbcClazz)) {
remarks.add(WARN(table, col,
format("Object Mismatch: ModelObject={0}, ColumnObject={1}",
fieldClazz.getSimpleName(), jdbcClazz.getSimpleName())));
}
}
}
// String Types
if (fieldClazz == String.class) {
if ((fieldDef.maxLength != col.size)
&& (col.size < Integer.MAX_VALUE))
remarks.add(WARN(table, col,
format("{0}.maxLength={1}, ColumnMaxLength={2}",
JQColumn.class.getSimpleName(),
fieldDef.maxLength, col.size)));
if (fieldDef.maxLength > 0 && !fieldDef.trimString)
remarks.add(CONSIDER(table, col,
format("{0}.truncateToMaxLength=true"
+ " will prevent RuntimeExceptions on"
+ " INSERTs or UPDATEs, but will clip data!",
JQColumn.class.getSimpleName())));
}
// Numeric AutoIncrement
if (fieldDef.isAutoIncrement != col.isAutoIncrement)
remarks.add(WARN(table, col, format("{0}.isAutoIncrement={1}"
+ " while Column autoIncrement={2}",
JQColumn.class.getSimpleName(), fieldDef.isAutoIncrement,
col.isAutoIncrement)));
// Last Check
// Default Value...
if (!col.isAutoIncrement && !col.isPrimaryKey) {
// Check Model.defaultValue Format
if (!ModelUtils.isProperlyFormattedDefaultValue(fieldDef.defaultValue)) {
remarks.add(ERROR(table, col, format("{0}.defaultValue=\"{1}\""
+ " is improperly formatted!",
JQColumn.class.getSimpleName(),
fieldDef.defaultValue)).throwError(throwError));
// Next field
return;
}
// Compare Model.defaultValue to Column.defaultValue
if (isNullOrEmpty(fieldDef.defaultValue)
&& !isNullOrEmpty(col.defaultValue)) {
// Model.defaultValue is NULL, Column.defaultValue is NOT NULL
remarks.add(WARN(table, col, format("{0}.defaultValue=\"\""
+ " while Column default=\"{1}\"",
JQColumn.class.getSimpleName(), col.defaultValue)));
} else if (!isNullOrEmpty(fieldDef.defaultValue)
&& isNullOrEmpty(col.defaultValue)) {
// Column.defaultValue is NULL, Model.defaultValue is NOT NULL
remarks.add(WARN(table, col, format("{0}.defaultValue=\"{1}\""
+ " while Column default=\"\"",
JQColumn.class.getSimpleName(), fieldDef.defaultValue)));
} else if (!isNullOrEmpty(fieldDef.defaultValue)
&& !isNullOrEmpty(col.defaultValue)) {
if (!fieldDef.defaultValue.equals(col.defaultValue)) {
// Model.defaultValue != Column.defaultValue
remarks.add(WARN(table, col, format("{0}.defaultValue=\"{1}\""
+ " while Column default=\"{2}\"",
JQColumn.class.getSimpleName(), fieldDef.defaultValue,
col.defaultValue)));
}
}
// Sanity Check Model.defaultValue Literal Value
if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(),
fieldDef.defaultValue)) {
remarks.add(ERROR(table, col,
format("{0}.defaultValue=\"{1}\" is invalid!!",
JQColumn.class.getSimpleName(),
fieldDef.defaultValue)));
}
}
}
/**
* Represents an index as it exists in the database.
*
*/
public static class IndexInspector {
String name;
IndexType type;
private List<String> columns = new ArrayList<String>();
public IndexInspector(ResultSet rs) throws SQLException {
name = rs.getString("INDEX_NAME");
// Determine Index Type
boolean hash = rs.getInt("TYPE") == DatabaseMetaData.tableIndexHashed;
boolean unique = !rs.getBoolean("NON_UNIQUE");
if (!hash && !unique)
type = IndexType.STANDARD;
else if (hash && unique)
type = IndexType.UNIQUE_HASH;
else if (unique)
type = IndexType.UNIQUE;
else if (hash)
type = IndexType.HASH;
columns.add(rs.getString("COLUMN_NAME"));
}
public void addColumn(ResultSet rs) throws SQLException {
columns.add(rs.getString("COLUMN_NAME"));
}
public String getColumnsString() {
StatementBuilder sb = new StatementBuilder();
for (String col : columns) {
sb.appendExceptFirst(", ");
sb.append(col);
}
return sb.toString().trim();
}
}
/**
* Represents a column as it exists in the database.
*
*/
public static class ColumnInspector implements Comparable<ColumnInspector> {
String name = null;
String type = null;
int size = 0;
boolean allowNull = false;
Class<?> clazz = null;
boolean isPrimaryKey = false;
boolean isAutoIncrement = false;
String defaultValue = null;
@Override
public int compareTo(ColumnInspector o) {
if (isPrimaryKey && o.isPrimaryKey)
// both primary sort by name
return name.compareTo(o.name);
else if (isPrimaryKey && !o.isPrimaryKey)
// primary first
return -1;
else if (!isPrimaryKey && o.isPrimaryKey)
// primary first
return 1;
else
// Neither primary, sort by name
return name.compareTo(o.name);
}
}
/**
* Convenience class based on StatementBuilder for creating the
* annotation parameter list.
*
*/
private static class AnnotationBuilder extends StatementBuilder {
AnnotationBuilder() {
super();
}
void addParameter(String parameter) {
appendExceptFirst(", ");
append(parameter);
}
<T> void addParameter(String parameter, T value) {
appendExceptFirst(", ");
append(parameter);
append('=');
if (value instanceof List) {
append("{ ");
List list = (List) value;
StatementBuilder flat = new StatementBuilder();
for (Object o:list) {
flat.appendExceptFirst(", ");
if (o instanceof String)
flat.append('\"');
flat.append(o.toString().trim());
if (o instanceof String)
flat.append('\"');
}
append(flat);
append(" }");
} else {
if (value instanceof String)
append('\"');
append(value.toString().trim());
if (value instanceof String)
append('\"');
}
}
}
}
\ No newline at end of file
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu;
import org.h2.jaqu.TableDefinition.FieldDefinition;
import org.h2.jaqu.TableInspector.ColumnInspector;
import org.h2.jaqu.util.StringUtils;
/**
* A Validation Remark is a result of running a model validation.
* <p>
* Each remark has a level, associated component (schema, table, column, index),
* and a message.
*
*/
public class Validation {
public static Validation CONSIDER(String table, String type, String message) {
return new Validation(Level.CONSIDER, table, type, message);
}
public static Validation CONSIDER(String table, ColumnInspector col, String message) {
return new Validation(Level.CONSIDER, table, col, message);
}
public static Validation WARN(String table, ColumnInspector col, String message) {
return new Validation(Level.WARN, table, col, message);
}
public static Validation WARN(String table, String type, String message) {
return new Validation(Level.WARN, table, type, message);
}
public static Validation ERROR(String table, ColumnInspector col, String message) {
return new Validation(Level.ERROR, table, col, message);
}
public static Validation ERROR(String table, String type, String message) {
return new Validation(Level.ERROR, table, type, message);
}
public static Validation ERROR(String table, FieldDefinition field, String message) {
return new Validation(Level.ERROR, table, field, message);
}
public static enum Level {
CONSIDER, WARN, ERROR;
}
Level level;
String table;
String fieldType;
String fieldName;
String message;
private Validation(Level level, String table, String type, String message) {
this.level = level;
this.table = table;
this.fieldType = type;
this.fieldName = "";
this.message = message;
}
private Validation(Level level, String table, FieldDefinition field, String message) {
this.level = level;
this.table = table;
this.fieldType = field.dataType;
this.fieldName = field.columnName;
this.message = message;
}
private Validation(Level level, String table, ColumnInspector col, String message) {
this.level = level;
this.table = table;
this.fieldType = col.type;
this.fieldName = col.name;
this.message = message;
}
public Validation throwError(boolean throwOnError) {
if (throwOnError && isError())
throw new RuntimeException(toString());
return this;
}
public boolean isError() {
return level.equals(Level.ERROR);
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(StringUtils.pad(level.name(), 9, " ", true));
sb.append(StringUtils.pad(table, 25, " ", true));
sb.append(StringUtils.pad(fieldName, 20, " ", true));
sb.append(' ');
sb.append(message);
return sb.toString();
}
public String toCSVString() {
StringBuilder sb = new StringBuilder();
sb.append(level.name()).append(',');
sb.append(table).append(',');
sb.append(fieldType).append(',');
sb.append(fieldName).append(',');
sb.append(message);
return sb.toString();
}
}
......@@ -8,7 +8,7 @@ package org.h2.jaqu.bytecode;
import org.h2.jaqu.Query;
import org.h2.jaqu.SQLStatement;
import org.h2.util.StringUtils;
import org.h2.jaqu.util.StringUtils;
/**
* A string constant.
......
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu.util;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.h2.jaqu.Db;
import org.h2.jaqu.DbInspector;
/**
* Generates JaQu models.
*
*/
public class GenerateModels {
/**
* The output stream where this tool writes to.
*/
protected PrintStream out = System.out;
public static void main(String... args) throws SQLException {
new GenerateModels().runTool(args);
}
public void runTool(String... args) throws SQLException {
String url = null;
String user = "sa";
String password = "";
String schema = null;
String table = null;
String packageName = "";
String folder = null;
boolean annotateSchema = true;
boolean trimStrings = false;
for (int i = 0; args != null && i < args.length; i++) {
String arg = args[i];
if (arg.equals("-url")) {
url = args[++i];
} else if (arg.equals("-user")) {
user = args[++i];
} else if (arg.equals("-password")) {
password = args[++i];
} else if (arg.equals("-schema")) {
schema = args[++i];
} else if (arg.equals("-table")) {
table = args[++i];
} else if (arg.equals("-package")) {
packageName = args[++i];
} else if (arg.equals("-folder")) {
folder = args[++i];
} else if (arg.equals("-annotateSchema")) {
try {
annotateSchema = Boolean.parseBoolean(args[++i]);
} catch (Throwable t) {
throw new SQLException("Can not parse -annotateSchema value");
}
} else if (arg.equals("-trimStrings")) {
try {
trimStrings = Boolean.parseBoolean(args[++i]);
} catch (Throwable t) {
throw new SQLException("Can not parse -trimStrings value");
}
} else {
throwUnsupportedOption(arg);
}
}
if (url == null) {
throw new SQLException("URL not set");
}
execute(url, user, password, schema, table, packageName, folder,
annotateSchema, trimStrings);
}
/**
* Generates models from the database.
*
* @param url the database URL
* @param user the user name
* @param password the password
* @param schema the schema to read from. null for all schemas.
* @param table the table to model. null for all tables within schema.
* @param packageName the package name of the model classes.
* @param folder destination folder for model classes (package path not included)
* @param annotateSchema includes the schema in the table model annotations
* @param trimStrings automatically trim strings that exceed maxLength
*/
public static void execute(String url, String user, String password,
String schema, String table, String packageName, String folder,
boolean annotateSchema, boolean trimStrings)
throws SQLException {
Connection conn = null;
try {
org.h2.Driver.load();
conn = DriverManager.getConnection(url, user, password);
Db db = Db.open(url, user, password.toCharArray());
DbInspector inspector = new DbInspector(db);
List<String> models = inspector.generateModel(schema, table,
packageName, annotateSchema, trimStrings);
File parentFile;
if (StringUtils.isNullOrEmpty(folder))
parentFile = new File(System.getProperty("user.dir"));
else
parentFile = new File(folder);
parentFile.mkdirs();
Pattern p = Pattern.compile("class ([a-zA-Z0-9]+)");
for (String model : models) {
Matcher m = p.matcher(model);
if (m.find()) {
String className = m.group().substring("class".length()).trim();
File classFile = new File(parentFile, className + ".java");
Writer o = new FileWriter(classFile, false);
PrintWriter writer = new PrintWriter(new BufferedWriter(o));
writer.write(model);
writer.close();
System.out.println("Generated " + classFile.getAbsolutePath());
}
}
} catch (SQLException s) {
throw s;
} catch (IOException i) {
throw new SQLException(i);
} finally {
JdbcUtils.closeSilently(conn);
}
}
/**
* Throw a SQLException saying this command line option is not supported.
*
* @param option the unsupported option
* @return this method never returns normally
*/
protected SQLException throwUnsupportedOption(String option) throws SQLException {
showUsage();
throw new SQLException("Unsupported option: " + option);
}
protected void showUsage() {
out.println("GenerateModels");
out.println("Usage: java "+getClass().getName());
out.println();
out.println("(*) -url jdbc:h2:~test");
out.println(" -user <string>");
out.println(" -password <string>");
out.println(" -schema <string>");
out.println(" -table <string>");
out.println(" -package <string>");
out.println(" -folder <string>");
out.println(" -annotateSchema <boolean>");
out.println(" -trimStrings <boolean>");
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.jaqu.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.naming.Context;
import javax.sql.DataSource;
import javax.sql.XAConnection;
/**
* This is a utility class with JDBC helper functions.
*/
public class JdbcUtils {
private static final String[] DRIVERS = {
"h2:", "org.h2.Driver",
"Cache:", "com.intersys.jdbc.CacheDriver",
"daffodilDB://", "in.co.daffodil.db.rmi.RmiDaffodilDBDriver",
"daffodil", "in.co.daffodil.db.jdbc.DaffodilDBDriver",
"db2:", "COM.ibm.db2.jdbc.net.DB2Driver",
"derby:net:", "org.apache.derby.jdbc.ClientDriver",
"derby://", "org.apache.derby.jdbc.ClientDriver",
"derby:", "org.apache.derby.jdbc.EmbeddedDriver",
"FrontBase:", "com.frontbase.jdbc.FBJDriver",
"firebirdsql:", "org.firebirdsql.jdbc.FBDriver",
"hsqldb:", "org.hsqldb.jdbcDriver",
"informix-sqli:", "com.informix.jdbc.IfxDriver",
"jtds:", "net.sourceforge.jtds.jdbc.Driver",
"microsoft:", "com.microsoft.jdbc.sqlserver.SQLServerDriver",
"mimer:", "com.mimer.jdbc.Driver",
"mysql:", "com.mysql.jdbc.Driver",
"odbc:", "sun.jdbc.odbc.JdbcOdbcDriver",
"oracle:", "oracle.jdbc.driver.OracleDriver",
"pervasive:", "com.pervasive.jdbc.v2.Driver",
"pointbase:micro:", "com.pointbase.me.jdbc.jdbcDriver",
"pointbase:", "com.pointbase.jdbc.jdbcUniversalDriver",
"postgresql:", "org.postgresql.Driver",
"sybase:", "com.sybase.jdbc3.jdbc.SybDriver",
"sqlserver:", "com.microsoft.sqlserver.jdbc.SQLServerDriver",
"teradata:", "com.ncr.teradata.TeraDriver",
};
private JdbcUtils() {
// utility class
}
/**
* Close a statement without throwing an exception.
*
* @param stat the statement or null
*/
public static void closeSilently(Statement stat) {
if (stat != null) {
try {
stat.close();
} catch (SQLException e) {
// ignore
}
}
}
/**
* Close a connection without throwing an exception.
*
* @param conn the connection or null
*/
public static void closeSilently(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// ignore
}
}
}
/**
* Close a result set without throwing an exception.
*
* @param rs the result set or null
*/
public static void closeSilently(ResultSet rs) {
closeSilently(rs, false);
}
/**
* Close a result set, and optionally its statement without throwing an
* exception.
*
* @param rs the result set or null
*/
public static void closeSilently(ResultSet rs, boolean closeStatement) {
if (rs != null) {
Statement stat = null;
if (closeStatement) {
try {
stat = rs.getStatement();
} catch (SQLException e) {
//ignore
}
}
try {
rs.close();
} catch (SQLException e) {
// ignore
}
closeSilently(stat);
}
}
/**
* Close an XA connection set without throwing an exception.
*
* @param conn the XA connection or null
*/
//## Java 1.4 begin ##
public static void closeSilently(XAConnection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// ignore
}
}
}
//## Java 1.4 end ##
/**
* Open a new database connection with the given settings.
*
* @param driver the driver class name
* @param url the database URL
* @param user the user name
* @param password the password
* @return the database connection
*/
public static Connection getConnection(String driver, String url, String user, String password) throws SQLException {
Properties prop = new Properties();
if (user != null) {
prop.setProperty("user", user);
}
if (password != null) {
prop.setProperty("password", password);
}
return getConnection(driver, url, prop);
}
/**
* Escape table or schema patterns used for DatabaseMetaData functions.
*
* @param pattern the pattern
* @return the escaped pattern
*/
public static String escapeMetaDataPattern(String pattern) {
if (pattern == null || pattern.length() == 0) {
return pattern;
}
return StringUtils.replaceAll(pattern, "\\", "\\\\");
}
/**
* Open a new database connection with the given settings.
*
* @param driver the driver class name
* @param url the database URL
* @param prop the properties containing at least the user name and password
* @return the database connection
*/
public static Connection getConnection(String driver, String url, Properties prop) throws SQLException {
if (StringUtils.isNullOrEmpty(driver)) {
JdbcUtils.load(url);
} else {
Class<?> d = ClassUtils.loadClass(driver);
if (java.sql.Driver.class.isAssignableFrom(d)) {
return DriverManager.getConnection(url, prop);
//## Java 1.4 begin ##
} else if (javax.naming.Context.class.isAssignableFrom(d)) {
// JNDI context
try {
Context context = (Context) d.newInstance();
DataSource ds = (DataSource) context.lookup(url);
String user = prop.getProperty("user");
String password = prop.getProperty("password");
if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) {
return ds.getConnection();
}
return ds.getConnection(user, password);
} catch (Exception e) {
throw Message.convert(e);
}
//## Java 1.4 end ##
} else {
// Don't know, but maybe it loaded a JDBC Driver
return DriverManager.getConnection(url, prop);
}
}
return DriverManager.getConnection(url, prop);
}
/**
* Get the driver class name for the given URL, or null if the URL is
* unknown.
*
* @param url the database URL
* @return the driver class name
*/
public static String getDriver(String url) {
if (url.startsWith("jdbc:")) {
url = url.substring("jdbc:".length());
for (int i = 0; i < DRIVERS.length; i += 2) {
String prefix = DRIVERS[i];
if (url.startsWith(prefix)) {
return DRIVERS[i + 1];
}
}
}
return null;
}
/**
* Load the driver class for the given URL, if the database URL is known.
*
* @param url the database URL
*/
public static void load(String url) {
String driver = getDriver(url);
if (driver != null) {
ClassUtils.loadClass(driver);
}
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.jaqu.util;
/**
* A utility class to build a statement. In addition to the methods supported by
* StringBuilder, it allows to add a text only in the second iteration. This
* simplified constructs such as:
* <pre>
* StringBuilder buff = new StringBuilder();
* for (int i = 0; i &lt; args.length; i++) {
* if (i &gt; 0) {
* buff.append(&quot;, &quot;);
* }
* buff.append(args[i]);
* }
* </pre>
* to
* <pre>
* StatementBuilder buff = new StatementBuilder();
* for (String s : args) {
* buff.appendExceptFirst(&quot;, &quot;);
* buff.append(a);
* }
*</pre>
*/
public class StatementBuilder {
private final StringBuilder builder = new StringBuilder();
private int index;
/**
* Create a new builder.
*/
public StatementBuilder() {
// nothing to do
}
/**
* Create a new builder.
*
* @param string the initial string
*/
public StatementBuilder(String string) {
builder.append(string);
}
/**
* Append a text.
*
* @param s the text to append
* @return itself
*/
public StatementBuilder append(String s) {
builder.append(s);
return this;
}
/**
* Append a character.
*
* @param c the character to append
* @return itself
*/
public StatementBuilder append(char c) {
builder.append(c);
return this;
}
/**
* Append a number.
*
* @param x the number to append
* @return itself
*/
public StatementBuilder append(long x) {
builder.append(x);
return this;
}
/**
* Reset the loop counter.
*
* @return itself
*/
public StatementBuilder resetCount() {
index = 0;
return this;
}
/**
* Append a text, but only if appendExceptFirst was never called.
*
* @param s the text to append
*/
public void appendOnlyFirst(String s) {
if (index == 0) {
builder.append(s);
}
}
/**
* Append a text, except when this method is called the first time.
*
* @param s the text to append
*/
public void appendExceptFirst(String s) {
if (index++ > 0) {
builder.append(s);
}
}
public void append(StatementBuilder sb) {
builder.append(sb);
}
public void insert(int offset, char c) {
builder.insert(offset, c);
}
public String toString() {
return builder.toString();
}
/**
* Get the length.
*
* @return the length
*/
public int length() {
return builder.length();
}
}
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: James Moger
*/
package org.h2.jaqu.util;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.concurrent.atomic.AtomicLong;
/**
* Utility class to optionally log generated statements to an output stream.<br>
* Default output stream is System.out.<br>
* Statement logging is disabled by default.
* <p>
* This class also tracks the counts for generated statements by major type.
*
*/
public class StatementLogger {
public static boolean logStatements = false;
public static PrintWriter out = new PrintWriter(System.out);
public final static AtomicLong selectCount = new AtomicLong(0);
public final static AtomicLong createCount = new AtomicLong(0);
public final static AtomicLong insertCount = new AtomicLong(0);
public final static AtomicLong updateCount = new AtomicLong(0);
public final static AtomicLong mergeCount = new AtomicLong(0);
public final static AtomicLong deleteCount = new AtomicLong(0);
public static void create(String statement) {
createCount.incrementAndGet();
log(statement);
}
public static void insert(String statement) {
insertCount.incrementAndGet();
log(statement);
}
public static void update(String statement) {
updateCount.incrementAndGet();
log(statement);
}
public static void merge(String statement) {
mergeCount.incrementAndGet();
log(statement);
}
public static void delete(String statement) {
deleteCount.incrementAndGet();
log(statement);
}
public static void select(String statement) {
selectCount.incrementAndGet();
log(statement);
}
private static void log(String statement) {
if (logStatements)
out.println(statement);
}
public static void printStats() {
out.println("JaQu Runtime Stats");
out.println("=======================");
printStat("CREATE", createCount);
printStat("INSERT", insertCount);
printStat("UPDATE", updateCount);
printStat("MERGE", mergeCount);
printStat("DELETE", deleteCount);
printStat("SELECT", selectCount);
}
private static void printStat(String name, AtomicLong value) {
if (value.get() > 0) {
DecimalFormat df = new DecimalFormat("###,###,###,###");
out.println(name + "=" + df.format(createCount.get()));
}
}
}
\ No newline at end of file
/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.jaqu.util;
public class StringUtils {
/**
* Replace all occurrences of the before string with the after string.
*
* @param s the string
* @param before the old text
* @param after the new text
* @return the string with the before string replaced
*/
public static String replaceAll(String s, String before, String after) {
int next = s.indexOf(before);
if (next < 0) {
return s;
}
StringBuilder buff = new StringBuilder(s.length() - before.length() + after.length());
int index = 0;
while (true) {
buff.append(s.substring(index, next)).append(after);
index = next + before.length();
next = s.indexOf(before, index);
if (next < 0) {
buff.append(s.substring(index));
break;
}
}
return buff.toString();
}
/**
* Check if a String is null or empty (the length is null).
*
* @param s the string to check
* @return true if it is null or empty
*/
public static boolean isNullOrEmpty(String s) {
return s == null || s.length() == 0;
}
/**
* Convert a string to a Java literal using the correct escape sequences.
* The literal is not enclosed in double quotes. The result can be used in
* properties files or in Java source code.
*
* @param s the text to convert
* @return the Java representation
*/
public static String javaEncode(String s) {
int length = s.length();
StringBuilder buff = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
switch (c) {
// case '\b':
// // BS backspace
// // not supported in properties files
// buff.append("\\b");
// break;
case '\t':
// HT horizontal tab
buff.append("\\t");
break;
case '\n':
// LF linefeed
buff.append("\\n");
break;
case '\f':
// FF form feed
buff.append("\\f");
break;
case '\r':
// CR carriage return
buff.append("\\r");
break;
case '"':
// double quote
buff.append("\\\"");
break;
case '\\':
// backslash
buff.append("\\\\");
break;
default:
int ch = c & 0xffff;
if (ch >= ' ' && (ch < 0x80)) {
buff.append(c);
// not supported in properties files
// } else if(ch < 0xff) {
// buff.append("\\");
// // make sure it's three characters (0x200 is octal 1000)
// buff.append(Integer.toOctalString(0x200 |
// ch).substring(1));
} else {
buff.append("\\u");
// make sure it's four characters
buff.append(Integer.toHexString(0x10000 | ch).substring(1));
}
}
}
return buff.toString();
}
/**
* Pad a string. This method is used for the SQL function RPAD and LPAD.
*
* @param string the original string
* @param n the target length
* @param padding the padding string
* @param right true if the padding should be appended at the end
* @return the padded string
*/
public static String pad(String string, int n, String padding, boolean right) {
if (n < 0) {
n = 0;
}
if (n < string.length()) {
return string.substring(0, n);
} else if (n == string.length()) {
return string;
}
char paddingChar;
if (padding == null || padding.length() == 0) {
paddingChar = ' ';
} else {
paddingChar = padding.charAt(0);
}
StringBuilder buff = new StringBuilder(n);
n -= string.length();
if (right) {
buff.append(string);
}
for (int i = 0; i < n; i++) {
buff.append(paddingChar);
}
if (!right) {
buff.append(string);
}
return buff.toString();
}
/**
* Convert a string to a SQL literal. Null is converted to NULL. The text is
* enclosed in single quotes. If there are any special characters, the method
* STRINGDECODE is used.
*
* @param s the text to convert.
* @return the SQL literal
*/
public static String quoteStringSQL(String s) {
if (s == null) {
return "NULL";
}
int length = s.length();
StringBuilder buff = new StringBuilder(length + 2);
buff.append('\'');
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
if (c == '\'') {
buff.append(c);
} else if (c < ' ' || c > 127) {
// need to start from the beginning because maybe there was a \
// that was not quoted
return "STRINGDECODE(" + quoteStringSQL(javaEncode(s)) + ")";
}
buff.append(c);
}
buff.append('\'');
return buff.toString();
}
}
......@@ -7,19 +7,24 @@
package org.h2.jaqu.util;
//## Java 1.5 begin ##
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Clob;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.h2.util.IOUtils;
//## Java 1.5 end ##
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Generic utility methods.
......@@ -27,13 +32,31 @@ import org.h2.util.IOUtils;
public class Utils {
//## Java 1.5 begin ##
private static volatile long counter;
private static final AtomicLong counter = new AtomicLong(0);
private static final boolean MAKE_ACCESSIBLE = true;
private static final int BUFFER_BLOCK_SIZE = 4 * 1024;
public static <T> ArrayList<T> newArrayList() {
return new ArrayList<T>();
}
public static <T> ArrayList<T> newArrayList(Collection<T> c) {
return new ArrayList<T>(c);
}
public static <T> HashSet<T> newHashSet() {
return new HashSet<T>();
}
public static <T> HashSet<T> newHashSet(Collection<T> list) {
return new HashSet<T>(list);
}
public static <T> Set<T> newConcurrentHashSet() {
return Collections.newSetFromMap(new ConcurrentHashMap<T,Boolean>());
}
public static <A, B> HashMap<A, B> newHashMap() {
return new HashMap<A, B>();
......@@ -52,35 +75,35 @@ public class Utils {
public static <T> T newObject(Class<T> clazz) {
// must create new instances
if (clazz == Integer.class) {
return (T) new Integer((int) counter++);
return (T) new Integer((int) counter.incrementAndGet());
} else if (clazz == String.class) {
return (T) ("" + counter++);
return (T) ("" + counter.incrementAndGet());
} else if (clazz == Long.class) {
return (T) new Long(counter++);
return (T) new Long(counter.incrementAndGet());
} else if (clazz == Short.class) {
return (T) new Short((short) counter++);
return (T) new Short((short) counter.incrementAndGet());
} else if (clazz == Byte.class) {
return (T) new Byte((byte) counter++);
return (T) new Byte((byte) counter.incrementAndGet());
} else if (clazz == Float.class) {
return (T) new Float(counter++);
return (T) new Float(counter.incrementAndGet());
} else if (clazz == Double.class) {
return (T) new Double(counter++);
return (T) new Double(counter.incrementAndGet());
} else if (clazz == Boolean.class) {
return (T) new Boolean(false);
} else if (clazz == BigDecimal.class) {
return (T) new BigDecimal(counter++);
return (T) new BigDecimal(counter.incrementAndGet());
} else if (clazz == BigInteger.class) {
return (T) new BigInteger("" + counter++);
return (T) new BigInteger("" + counter.incrementAndGet());
} else if (clazz == java.sql.Date.class) {
return (T) new java.sql.Date(counter++);
return (T) new java.sql.Date(counter.incrementAndGet());
} else if (clazz == java.sql.Time.class) {
return (T) new java.sql.Time(counter++);
return (T) new java.sql.Time(counter.incrementAndGet());
} else if (clazz == java.sql.Timestamp.class) {
return (T) new java.sql.Timestamp(counter++);
return (T) new java.sql.Timestamp(counter.incrementAndGet());
} else if (clazz == java.util.Date.class) {
return (T) new java.util.Date(counter++);
return (T) new java.util.Date(counter.incrementAndGet());
} else if (clazz == List.class) {
return (T) new ArrayList();
return (T) newArrayList();
}
try {
return clazz.newInstance();
......@@ -137,7 +160,7 @@ public class Utils {
Clob c = (Clob) o;
try {
Reader r = c.getCharacterStream();
return IOUtils.readStringAndClose(r, -1);
return readStringAndClose(r, -1);
} catch (Exception e) {
throw new RuntimeException("Error converting CLOB to String: " + e.toString(), e);
}
......@@ -146,7 +169,11 @@ public class Utils {
}
if (Number.class.isAssignableFrom(currentType)) {
Number n = (Number) o;
if (targetType == Integer.class) {
if (targetType == Byte.class) {
return n.byteValue();
} else if (targetType == Short.class) {
return n.shortValue();
} else if (targetType == Integer.class) {
return n.intValue();
} else if (targetType == Long.class) {
return n.longValue();
......@@ -159,6 +186,37 @@ public class Utils {
throw new RuntimeException("Can not convert the value " + o +
" from " + currentType + " to " + targetType);
}
/**
* Read a number of characters from a reader and close it.
*
* @param in the reader
* @param length the maximum number of characters to read, or -1 to read
* until the end of file
* @return the string read
*/
public static String readStringAndClose(Reader in, int length) throws IOException {
try {
if (length <= 0) {
length = Integer.MAX_VALUE;
}
int block = Math.min(BUFFER_BLOCK_SIZE, length);
StringWriter out = new StringWriter(length == Integer.MAX_VALUE ? block : length);
char[] buff = new char[block];
while (length > 0) {
int len = Math.min(block, length);
len = in.read(buff, 0, len);
if (len < 0) {
break;
}
out.write(buff, 0, len);
length -= len;
}
return out.toString();
} finally {
in.close();
}
}
//## Java 1.5 end ##
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论