/*
* BaseLink - Generic object relational mapping
* Copyright (C) 2011 Ulrich Hilger, http://uhilger.de
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/
*/
package de.uhilger.baselink;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Method;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Universal mapping of objects to and from a database
*
* Any class that has annotations for DBTable
and
* DBColumns
can be persisted using GenericRecord
.
*
* @author Copyright (c) Ulrich Hilger, http://uhilger.de
* @author Published under the terms and conditions of
* the GNU General Public License
*/
public class GenericRecord implements Record {
/** default getter method indicator */
public static final String GETTER_NAME = "get";
/** default setter method indicator */
public static final String SETTER_NAME = "set";
/** name of table this instance of GenericRecord references */
private String tableName;
/** list of column names that make up the primary key */
private List primaryKeyColNames;
/** reference to field setters and getters */
private Hashtable columns;
/** reference to class that this instance of GenericRecord maps to and from a database */
private Class> recordClass;
/**
* Create a new object of class GenericRecord
* @param c the class to persist
*/
public GenericRecord(Class> c) {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(c.getName());
this.recordClass = c;
DBTable table = c.getAnnotation(DBTable.class);
if(table != null) {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(tableName);
tableName = table.name();
} else {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).severe("missing table annotation");
}
primaryKeyColNames = new ArrayList();
DBPrimaryKey primaryKey = c.getAnnotation(DBPrimaryKey.class);
if(primaryKey != null) {
String[] names = primaryKey.value();
for(int i = 0; i < names.length; i++) {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(names[i]);
primaryKeyColNames.add(names[i]);
}
} else {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).severe("missing primary key annotation");
}
columns = new Hashtable();
Method[] methods = c.getMethods();
for(int i = 0; i < methods.length; i++) {
Method method = methods[i];
DBColumn field = method.getAnnotation(DBColumn.class);
if(field != null) {
Field mapper = new Field();
String fieldName = field.name();
mapper.setColumnType(field.type());
mapper.setColumnName(fieldName);
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(fieldName);
String methodName = method.getName();
if(methodName.startsWith(GETTER_NAME)) {
String fieldMethod = methodName.substring(3);
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(fieldMethod);
Class> returnType = method.getReturnType();
try {
mapper.setSetter(c.getMethod(SETTER_NAME + fieldMethod, returnType));
} catch (Exception e) {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.SEVERE, e.getLocalizedMessage(), e);
}
mapper.setGetter(method);
}
columns.put(fieldName, mapper);
}
}
}
/**
* Get a statement suitable to delete a given object from the database
* @param c the database connection to use for the delete
* @param record the object to delete
* @return the delete statement
* @throws Exception
*/
public PreparedStatement getDeleteStatment(Connection c, Object record) throws Exception {
StringBuffer sql = new StringBuffer();
sql.append("delete from ");
sql.append(tableName);
sql.append(" where ");
appendPrimaryKeyFields(sql);
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(sql.toString());
PreparedStatement ps = c.prepareStatement(sql.toString());
prepareFields(0, primaryKeyColNames, ps, record);
return ps;
}
/**
* Get a statement suitable to insert a given object to the database
* @param c the database connection to use for the insert
* @param record the object to insert
* @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned;
* one of Statement.RETURN_GENERATED_KEYS or Statement.NO_GENERATED_KEYS
* @return the insert statement
* @throws Exception
*/
public PreparedStatement getInsertStmt(Connection c, Object record, int autoGeneratedKeys) throws Exception {
StringBuffer sql = new StringBuffer();
sql.append("insert into ");
sql.append(tableName);
sql.append("(");
Enumeration fieldNames = columns.keys();
StringBuffer fieldList = new StringBuffer();
StringBuffer valueList = new StringBuffer();
while(fieldNames.hasMoreElements()) {
if(fieldList.length() > 0) {
fieldList.append(",");
valueList.append(",");
}
fieldList.append(fieldNames.nextElement());
valueList.append("?");
}
sql.append(fieldList);
sql.append(")");
sql.append(" values (");//?, ?)");
sql.append(valueList);
sql.append(")");
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(sql.toString());
PreparedStatement ps = c.prepareStatement(sql.toString(), autoGeneratedKeys);
prepareFields(0, ps, record);
return ps;
}
/**
* Get a statement suitable to insert a given object to the database
* @param c the database connection to use for the insert
* @param record the object to insert
* @return the insert statement
* @throws Exception
*/
public PreparedStatement getInsertStatment(Connection c, Object record) throws Exception {
return getInsertStmt(c, record, Statement.NO_GENERATED_KEYS);
}
/**
* Get a statement suitable to update a given object in the database
* @param c the database connection to use for the update
* @param record the object to update
* @return the update statement
* @throws Exception
*/
public PreparedStatement getUpdateStatment(Connection c, Object record) throws Exception {
StringBuffer sql = new StringBuffer();
sql.append("update ");
sql.append(tableName);
sql.append(" set ");
Enumeration fieldNames = columns.keys();
StringBuffer fieldList = new StringBuffer();
while(fieldNames.hasMoreElements()) {
if(fieldList.length() > 0) {
fieldList.append(", ");
}
fieldList.append(fieldNames.nextElement());
fieldList.append("=?");
}
sql.append(fieldList);
sql.append(" where ");
appendPrimaryKeyFields(sql);
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(sql.toString());
PreparedStatement ps = c.prepareStatement(sql.toString());
prepareFields(0, ps, record);
prepareFields(columns.size(), primaryKeyColNames, ps, record);
return ps;
}
/**
* Get contents of this record as an object
* @param resultSet a resultSet that points to the record to get as an object
* @param includeBlobs indicator whether or not to include BLOBs
* @return an object having the data of this record
* @throws Exception
*/
public Object toObject(ResultSet resultSet, boolean includeBlobs) throws Exception {
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(recordClass.getName());
Object o = recordClass.newInstance();
Enumeration fieldNames = columns.keys();
while(fieldNames.hasMoreElements()) {
String columnName = fieldNames.nextElement();
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(columnName);
Field mapper = columns.get(columnName);
Method setter = mapper.getSetter();
if(setter != null) {
Object data = null;
if(mapper.getColumnType().equals(DBColumn.Type.BLOB)) {
if(includeBlobs) {
Blob blob = resultSet.getBlob(columnName);
data = new String(blob.getBytes((long) 1, (int) blob.length()));
setter.invoke(o, data);
}
} else {
data = resultSet.getObject(columnName);
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).finest(data.toString());
setter.invoke(o, data);
}
}
}
return o;
}
/**
* append names of primary key columns to an SQL string
* @param sql the SQL string to append to
*/
private void appendPrimaryKeyFields(StringBuffer sql) {
for(int i = 0; i < primaryKeyColNames.size(); i++) {
if(i > 0) {
sql.append(" and ");
}
sql.append(primaryKeyColNames.get(i));
sql.append("=?");
}
}
/**
* Prepare fields of a PreparedStatement
* @param offset number of field to start with
* @param ps the statement to prepare
* @param record object that has the data for the statement
* @throws Exception
*/
private void prepareFields(int offset, PreparedStatement ps, Object record) throws Exception {
Enumeration fieldNames = columns.keys();
int i = 1;
while(fieldNames.hasMoreElements()) {
Field mapper = columns.get(fieldNames.nextElement());
Method getter = mapper.getGetter();
Object o = getter.invoke(record, (Object[]) null);
if(mapper.getColumnType().equals(DBColumn.Type.BLOB)) {
String content = o.toString();
ps.setBinaryStream(i+offset, new ByteArrayInputStream(content.getBytes()), content.length());
} else {
ps.setObject(i+offset, o);
}
i++;
}
}
/**
* Prepare fields of a PreparedStatement
* @param offset number of field to start with
* @param source list of field names
* @param ps the statement to prepare
* @param record object that has the data for the statement
* @throws Exception
*/
private void prepareFields(int offset, List source, PreparedStatement ps, Object record) throws Exception {
for(int i = 0; i < source.size(); i++) {
Field mapper = columns.get(source.get(i));
Method getter = mapper.getGetter();
Object o = getter.invoke(record, (Object[]) null);
ps.setObject(i+offset+1, o);
}
}
/**
* Get the name of the database table this record references
* @return the table name
*/
public String getTableName() {
return tableName;
}
}