/*
* ====================================================================
*
* The Apache Software License, Version 1.1
*
* CopyU 6YHTFR356right (c) 1999-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowlegement:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowlegement may appear in the software itself,
* if and wherever such third-party acknowlegements normally appear.
*
* 4. The names "The Jakarta Project", "Commons", and "Apache Software
* Foundation" must not be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache"
* nor may "Apache" appear in their names without prior written
* permission of the Apache Group.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package net.sf.voruta;
import java.util.*;
import java.lang.reflect.*;
import java.lang.ref.*;
import java.sql.*;
import java.io.*;
import java.net.*;
import net.sf.cglib.*;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.*;
/** This class is the most important in Voruta API, It is factory
* for data access objects
* @author baliuka
*/
public final class Db {
private static final Map PROCEDURES = Collections.synchronizedMap(new WeakHashMap());
private static final Map PREDEFINED_HANDLERS = new Hashtable();
private static final ResultSetHandler SCALAR_HANDLER = new ScalarHandler();
private static final ResultSetHandler VECTOR_HANDLER = new VectorHandler();
private static final ResultSetHandler COLLECTION_HANDLER = new CollectionHandler();
private static final ResultSetHandler MAP_HANDLER = new MapHandler();
private static final ResultSetHandler BEAN_HANDLER = new BeanHandler();
private static final Properties properties = new Properties();
private static final String SEPARATORS = " ,[(\t=<>()";
//tags
private static final String UPDATE_TAG = "update";
private static final String QUERY_TAG = "query";
private static final String CACHE_TAG = "useCache";
private static final String FLUSH_TAG = "flushCache";
private static final String HANDLER_TAG = "handler";
private static final String EXECUTE_TAG = "execute";
private static final String LANGUAGE_TAG = "language";
private static final String SQL_LANGUAGE = "sql";
/*
Oracle:
EXPLAIN PLAN
[SET statement_id=id]
[INTO table_name]
FOR query
PostgreSQL :
EXPLAIN
Default PostgreSQL
*/
private static final String EXPLAIN_PLAN_PREFIX = "EXPLAIN";
//properties
public static final String VORUTA_EXPLAIN_PLAN_PREFIX_PROPERTY = "voruta.explainPlanPrefix";
public static final String VORUTA_AUTOTRACE = "voruta.autotrace";
public static final String VORUTA_EXECUTE_PROPERTY = "voruta.execute";
public static final String VORUTA_RELOAD_PROPERTY = "voruta.reload";
public static final String LANGUAGE_TAG_NAME = "voruta.tag.language";
public static final String UPDATE_TAG_NAME = "voruta.tag.update";
public static final String QUERY_TAG_NAME = "voruta.tag.query";
public static final String CACHE_TAG_NAME = "voruta.tag.useCache";
public static final String FLUSH_TAG_NAME = "voruta.tag.flushCache";
public static final String HANDLER_TAG_NAME = "voruta.tag.handler";
private static final String EXECUTE_TAG_NAME = "voruta.tag.execute";
private static final Hashtable languages = new Hashtable();
private static final Map resources = Collections.synchronizedMap(new WeakHashMap());
private static final List globalInterceptors = new Vector();
static{
registerLanguage(SQL_LANGUAGE, new SqlParser());
new HandlerFactory(){
{ registerHandlerFactory( "pair", this ); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return new PairHandler();
}
};
new HandlerFactory(){
{ registerHandlerFactory( "beanMap", this ); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String name){
Class cls = null;
String propertyName = null;
StringTokenizer st = new java.util.StringTokenizer(name,SEPARATORS);
st.nextToken();
if(st.hasMoreElements()){
cls = finder.findByName(st.nextToken());
if(cls == null){
throw new DbException(cls + "not found");
}
}else {
throw new IllegalArgumentException( name );
}
if(st.hasMoreElements()){
propertyName = st.nextToken();
}else {
throw new IllegalArgumentException( name );
}
return new BeanMapHandler(cls,propertyName);
}
};
new HandlerFactory(){
{ registerHandlerFactory("column",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return new ColumnHandler();
}
};
new HandlerFactory(){
{ registerHandlerFactory("collection",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return COLLECTION_HANDLER;
}
};
new HandlerFactory(){
{ registerHandlerFactory("vector",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return VECTOR_HANDLER;
}
};
new HandlerFactory(){
{ registerHandlerFactory("scalar",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return SCALAR_HANDLER;
}
};
new HandlerFactory(){
{ registerHandlerFactory("map",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return MAP_HANDLER;
}
};
new HandlerFactory(){
{ registerHandlerFactory("bean",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String handlerAttribute){
return BEAN_HANDLER;
}
};
new HandlerFactory(){
{ registerHandlerFactory("beans",this); }
public ResultSetHandler getInstance( Method method,
ClassFinder finder, String name){
StringTokenizer st = new java.util.StringTokenizer(name,"() \t<>");
st.nextToken();
if(st.hasMoreElements()){
Class cls = finder.findByName(st.nextToken());
if(cls != null){
return new BeanCollectionHandler(cls);
}else{
throw new DbException(cls + "not found");
}
}else {
throw new IllegalArgumentException( name );
}
}
};
}
private Db() {
}
public static synchronized void addInterceptor(ProcedureInterceptor i){
if( i != null ){
globalInterceptors.add(i);
}
}
public static synchronized void removeInterceptor(ProcedureInterceptor i){
if( i != null ){
globalInterceptors.remove(i);
}
}
/** Configures voruta
* @param factory Connection factory implementation
* @param properties Properties to configure Voruta
*/
public static void init( ConnectionFactory factory, Properties properties){
if( properties != null ){
Db.properties.putAll( properties );
}
ThreadLocalConnection.init(factory);
DbUtils.getLog().info("[java] properties: " + System.getProperties());
DbUtils.getLog().info("[voruta] properties: " + properties);
}
/** method to read global properties
* @return Properties object
*/
public static Properties getProperties(){
return properties;
}
/** returns global propery by name
* @param name property name
* @return property value
*/
public static String getProperty(String name){
return properties.getProperty(name) ;
}
public static String getProperty(String name,String def){
return properties.getProperty(name,def) ;
}
/** registers custom lamguage handler
* @param name language id
* @param factory language factory implementation
*/
public static void registerLanguage(String name, LanguageFactory factory){
languages.put(name,factory);
}
/** Registers custon handler
* @param name tag id
* @param factory handler factory implementation
*/
public static void registerHandlerFactory(String name, HandlerFactory factory){
PREDEFINED_HANDLERS.put(name,factory);
}
static ProcedureDescriptor compile( ClassFinder finder,
Method proc,
String sqlStr,
ResultSetHandler handler,
boolean update,
JavaMethod jmethod,
Properties tags,
String connection )throws Exception{
if( update && proc.getReturnType() != Void.TYPE &&
proc.getReturnType() != Integer.TYPE ){
throw new IllegalArgumentException( "update method " + proc +
" must return int or void " );
}
LanguageFactory factory = (LanguageFactory)languages.
get(tags.getProperty(
getProperty(LANGUAGE_TAG_NAME,LANGUAGE_TAG),SQL_LANGUAGE));
Language languge = factory.newInstance();
languge.compile( finder, proc,sqlStr,tags);
if( "on".equals(getProperty(VORUTA_AUTOTRACE,"false"))){
languge.explain(connection);
}
boolean cache = jmethod.getTagByName(
getProperty( CACHE_TAG_NAME, CACHE_TAG )) != null;
boolean flush = jmethod.getTagByName(
getProperty( FLUSH_TAG_NAME, FLUSH_TAG )) != null;
ProcedureDescriptor descriptor = new ProcedureDescriptor(tags);
descriptor.setHelper ( languge );
descriptor.setHandler( handler);
descriptor.setUpdate ( update );
descriptor.setCached ( cache );
descriptor.setFlushOnExecute( flush );
descriptor.setMethod( proc );
descriptor.setRegion( cache ? jmethod.getTagByName(
getProperty( CACHE_TAG_NAME, CACHE_TAG )).getValue() : null );
if(flush){
String regions[];
StringTokenizer tokenizer = new StringTokenizer(
jmethod.getTagByName(
getProperty( FLUSH_TAG_NAME, FLUSH_TAG )).getValue(), ", \t" );
List list = new ArrayList();
list.add("");
list.add(null);
while( tokenizer.hasMoreElements() ){
list.add(tokenizer.nextToken());
}
regions = (String[])list.toArray( new String[]{});
descriptor.setRegions( regions );
}
return descriptor;
}
private static boolean isCollection(Class cls){
return Collection.class.isAssignableFrom(cls);
}
private static boolean isMap(Class cls){
return Map.class.isAssignableFrom(cls);
}
private static boolean isVector(Class cls){
return cls.isArray() && ! cls.getComponentType().isPrimitive();
}
private static boolean isScalar(Class cls){
return cls.isPrimitive() || Number.class.isAssignableFrom(cls) ||
Character.class == cls || String.class == cls ||
Boolean.class == cls || java.util.Date.class.isAssignableFrom(cls);
}
static Method findMethod(Class cls, JavaMethod jmethod){
//find method in class by QDox model objects
Method methods[] = cls.getMethods();
LOOP:
for(int i=0; i< methods.length; i++ ){
if( methods[i].getName().equals(jmethod.getName()) ){
JavaParameter jparams[] = jmethod.getParameters();
Class params[] = methods[i].getParameterTypes();
if( params.length == jparams.length ){
for( int j = 0; j < params.length; j++ ){
if(!params[j].getName().
equals(jparams[j].getType().getValue()) ){
continue LOOP;
}
}
return methods[i];
}
}
}
throw new DbException("method not in class " + cls.getName()
+ " found for " + jmethod + "\n try to recompile source");
}
static Class resolve(ClassLoader loader,String imports[], String name){
Class result = null;
try{
result = loader.loadClass(name);
}catch (ClassNotFoundException cnfe){
//search in import
for(int i = 0; i < imports.length; i++){
try{
if(imports[i].endsWith( name )){
result = loader.loadClass(imports[i]);
break;
}else if(imports[i].endsWith("*")){
result = loader.loadClass(
imports[i].substring(0,imports[i].length() - 1 ) + name);
break;
}
}catch(ClassNotFoundException nextCnfe){
}
}
}
if(result == null){
throw new DbException(" can not resolve " + name + " to class");
}
DbUtils.getLog().debug(name + "->" + result.getName() );
return result;
}
static ResultSetHandler findHandler(Method method)throws Exception{
//tries to find known handler by return type
if( isScalar( method.getReturnType() ) ){
return SCALAR_HANDLER;
}
if( isVector( method.getReturnType() ) ){
return VECTOR_HANDLER;
}
if( isCollection( method.getReturnType() ) ){
return COLLECTION_HANDLER;
}
if( isMap( method.getReturnType() ) ){
return MAP_HANDLER;
}
return null;
}
static ResultSetHandler findHandler( final Method method,
final Properties methodAttributes,
final String [] imports )
throws Exception{
String name = methodAttributes.getProperty(
getProperty( HANDLER_TAG_NAME, HANDLER_TAG )
);
name = ( name == null || name.trim().length() == 0 ) ? null : name.trim() ;
final ClassLoader loader = method.getDeclaringClass().getClassLoader();
ResultSetHandler handler = null;
if( name != null ){
StringTokenizer tokens = new StringTokenizer(name,SEPARATORS);
String fName = tokens.nextToken();
HandlerFactory handlerFactory = (HandlerFactory)PREDEFINED_HANDLERS.get(fName);
//try factory first
if( handlerFactory != null ){
handler = handlerFactory.getInstance(method,new ClassFinder(){
public Class findByName( String cName ){
return resolve(loader,imports,cName);
}
},
name
);
}
if ( handler != null ){
return handler;
}else { //try as class name
Class cls = resolve(loader,imports,name);
if(cls != null){
return (ResultSetHandler)cls.newInstance();
}
}
} else { //handler not specified
handler = findHandler(method);
}
if(handler == null){
throw new IllegalArgumentException("handler is not found for " + method);
}
return handler;
}
/** Clears cache
* @param name cache region name
*/
public static void clearCache(String name){
DbUtils.getRegions(name).clearAll();
}
/** Clears cache
*/
public static void clearCache(){
clearCache(null);
}
private static void execute(JavaClass jclass,String connection)
throws Exception{
if("on".equals(getProperty(VORUTA_EXECUTE_PROPERTY))){
DocletTag queryTag[] = jclass.getTagsByName(
getProperty(EXECUTE_TAG_NAME, EXECUTE_TAG)
);
for( int i = 0; i < queryTag.length; i++ ){
DbUtils.executeUpdate(connection, queryTag[i].getValue(), null);
}
}
}
static Map buildProcedures(Class clasz,String connection)throws Exception{
Map desctiptors = new HashMap();
Class interfaces[] = clasz.isInterface() ?
new Class[]{clasz} : clasz.getInterfaces();
for(int k = 0; k < interfaces.length; k++ ){
final Class cls = interfaces[k];
String res = cls.getName().replace('.','/') + ".java";
InputStream is = cls.getClassLoader().getResourceAsStream(res);
JavaDocBuilder builder = new JavaDocBuilder();
try{
if( is == null ){
if(clasz.isInterface()){
throw new DbException("resource not found:" + res);
}
//abstract class must not implement "unknown" interfaces
DbUtils.getLog().warn( "resource not found:" + res );
continue;
}else{
DbUtils.getLog().debug( "loaded:" + res);
}
builder.addSource( new InputStreamReader(is) );
}finally{
if(is != null){
is.close();
}
}
JavaClass jclass = builder.getClassByName(cls.getName());
execute( jclass, connection );
Properties classTags = new Properties();
DocletTag ctags[] = jclass.getTags();
for(int i = 0; i< ctags.length; i++){
classTags.setProperty(ctags[i].getName(),
ctags[i].getValue() == null ? "": ctags[i].getValue());
}
final String importDeclarations[] = jclass.getParentSource().getImports();
final Set set = new java.util.HashSet();
set.addAll( Arrays.asList(importDeclarations) );
set.add("");
set.add(jclass.getPackage() + ".*");
final String [] imports = (String[])set.toArray(new String[]{});
final JavaMethod [] jmethods = jclass.getMethods();
for( int i = 0; i< jmethods.length; i++ ){
Method method = findMethod(clasz, jmethods[i] );
if( !Modifier.isAbstract(method.getModifiers()) ){
continue;
}
Properties properties = new Properties();
properties.putAll(classTags);
DocletTag [] tags = jmethods[i].getTags();
for(int j = 0; j < tags.length; j++){
properties.setProperty(tags[j].getName(),
tags[j].getValue() == null ? "" : tags[j].getValue());
}
DocletTag queryTag = jmethods[i].getTagByName(
getProperty( QUERY_TAG_NAME , QUERY_TAG ) );
DocletTag updateTag = jmethods[i].getTagByName(
getProperty( UPDATE_TAG_NAME, UPDATE_TAG ) );
String sql = null;
if(queryTag == null && updateTag == null ){
DbUtils.getLog().warn("no [voruta] attributes (" +
getProperty(QUERY_TAG_NAME ,QUERY_TAG) +
"," + getProperty(UPDATE_TAG_NAME, UPDATE_TAG) +
") in " + res + " " + jmethods[i] );
}else{
sql = updateTag == null ?
queryTag.getValue() : updateTag.getValue();
}
boolean update = updateTag != null;
ResultSetHandler handler = null;
if( !update ){
handler = findHandler(method,properties,imports );
}
desctiptors.put( method,
compile(new ClassFinder(){
public Class findByName( String cName ){
return resolve(cls.getClassLoader(),imports,cName);
}
}, method,
sql,
handler,
update,
jmethods[i],
properties,
connection
) );
}
}
return desctiptors;
}
/**
* commit current connection
*/
public static void commit(){
commit(null);
}
/** commit current connection
* @param name Configuration name
*/
public static void commit(String name){
ThreadLocalConnection.commit(name);
}
/** rollback current connection
*/
public static void rollback(){
rollback(null);
}
/** rollback current connection
* @param name Configuration name
*/
public static void rollback(String name){
ThreadLocalConnection.rollback(name);
}
/** Closes current connection
*/
public static void close(){
close(null);
}
/** Closes current connection
* @param name Configuration name
*/
public static void close(String name){
try{
ThreadLocalConnection.close(name);
}catch(Exception e){
DbUtils.getLog().warn(e);
}
}
/** generates data access class implementation and returns instance with current
* connetion
* @param cls data access class/interface
* @return implementation
*/
public static Object getProcedures(Class cls){
return getProcedures( cls, (String)null );
}
/** generates data access class implementation and returns instance with user
* provided connection and interceptor
* @param cls data access class/interface
* @param connection JDBC connection
* @param i interceptor
* @return implementation
*/
public static synchronized Object getProcedures( Class cls, String connection ){
try{
Map d = getDescriptors(cls,connection);
return implementProcedures(cls,
new InvocationImpl(connection, d, globalInterceptors) );
}catch(RuntimeException re){
throw re;
}catch(Exception e){
throw new DbException(e);
}
}
static boolean isReload(){
return "on".equals( getProperty(VORUTA_RELOAD_PROPERTY) );
}
public static Map registredClasses(){
return PROCEDURES;
}
static Map getDescriptors(Class cls, String connection)throws Exception{
Map descriptors;
if( isReload() ){
descriptors = buildProcedures(cls,connection);
}else{
descriptors = (Map)PROCEDURES.get(cls);
if(descriptors == null){
descriptors = buildProcedures(cls,connection);
}
}
PROCEDURES.put(cls, descriptors);
return descriptors;
}
static Object implementProcedures(Class cls, MethodInterceptor interceptor){
return Enhancer.enhance(cls.isInterface() ? null : cls,
cls.isInterface() ? new Class[]{cls} : null, interceptor,
cls.getClassLoader(), null, DbUtils.FILTER );
}
static String getExplainPrefix(){
return properties.getProperty(VORUTA_EXPLAIN_PLAN_PREFIX_PROPERTY,EXPLAIN_PLAN_PREFIX);
}
private static void explain(String connectionName, ProcedureDescriptor pd)throws Exception{
pd.getHelper().explain(connectionName);
}
/**
* Explains query plan for query declared by data access method
*/
public static void explain(String connectionName, Method m)throws Exception{
Map dm = getDescriptors(m.getDeclaringClass(),connectionName);
ProcedureDescriptor pd = (ProcedureDescriptor)dm.get(m);
if(pd == null){
throw new DbException("procedure not found " + m);
}
explain(connectionName, pd );
}
public static void explain(String connectionName, Class cls)throws Exception{
Map dm = getDescriptors(cls,connectionName);
explain( connectionName, cls, dm);
}
static void explain(String connectionName, Class cls, Map dm)throws Exception{
Method m[] = cls.getDeclaredMethods();
for(int i = 0; i < m.length; i++ ){
ProcedureDescriptor pd = (ProcedureDescriptor)dm.get(m[i]);
if(pd == null){
DbUtils.getLog().warn("procedure not found " + m[i]);
}else{
explain(connectionName, pd );
}
}
}
public static void explain(Class cls)throws Exception{
explain(null,cls);
}
}