/* * 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 { private static final Logger logger = Logger.getLogger(GenericRecord.class.getName()); /** 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.finest(c.getName()); this.recordClass = c; DBTable table = c.getAnnotation(DBTable.class); if(table != null) { logger.finest(tableName); tableName = table.name(); } else { logger.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.finest(names[i]); primaryKeyColNames.add(names[i]); } } else { logger.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.finest(fieldName); String methodName = method.getName(); if(methodName.startsWith(GETTER_NAME)) { String fieldMethod = methodName.substring(3); logger.finest(fieldMethod); Class returnType = method.getReturnType(); try { mapper.setSetter(c.getMethod(SETTER_NAME + fieldMethod, returnType)); } catch (Exception e) { logger.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.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.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.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.finest(recordClass.getName()); Object o = recordClass.newInstance(); Enumeration fieldNames = columns.keys(); while(fieldNames.hasMoreElements()) { String columnName = fieldNames.nextElement(); logger.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.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); int nr = i + offset; logger.finest(nr + " " + o.toString()); } 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; } }