/*
* Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
* Distributed under the terms shown in the file COPYRIGHT
* found in the root folder of this project or at
* http://eng.tada.se/osprojects/COPYRIGHT.html
*/
package org.postgresql.pljava.sqlj;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.postgresql.pljava.internal.Backend;
import org.postgresql.pljava.internal.Oid;
import org.postgresql.pljava.jdbc.SQLUtils;
/**
* @author Thomas Hallgren
*/
public class Loader extends ClassLoader
{
private final static Logger s_logger = Logger.getLogger(Loader.class.getName());
static class EntryEnumeration implements Enumeration
{
private final int[] m_entryIds;
private int m_top = 0;
EntryEnumeration(int[] entryIds)
{
m_entryIds = entryIds;
}
public boolean hasMoreElements()
{
return (m_top < m_entryIds.length);
}
public Object nextElement()
throws NoSuchElementException
{
if (m_top >= m_entryIds.length)
throw new NoSuchElementException();
return entryURL(m_entryIds[m_top++]);
}
}
private static final String PUBLIC_SCHEMA = "public";
private static final Map s_schemaLoaders = new HashMap();
private static final Map s_typeMap = new HashMap();
/**
* Removes all cached schema loaders, functions, and type maps. This
* method is called by the utility functions that manipulate the
* data that has been cached. It is not intended to be called
* from user code.
*/
public static void clearSchemaLoaders()
{
s_schemaLoaders.clear();
s_typeMap.clear();
Backend.clearFunctionCache();
}
/**
* Obtains the loader that is in effect for the current schema (i.e. the
* schema that is first in the search path).
* @return A loader
* @throws SQLException
*/
public static ClassLoader getCurrentLoader()
throws SQLException
{
String schema;
Statement stmt = SQLUtils.getDefaultConnection().createStatement();
ResultSet rs = null;
try
{
rs = stmt.executeQuery("SELECT current_schema()");
if(!rs.next())
throw new SQLException("Unable to determine current schema");
schema = rs.getString(1);
}
finally
{
SQLUtils.close(rs);
SQLUtils.close(stmt);
}
return getSchemaLoader(schema);
}
/**
* Obtain a loader that has been configured for the class path of the
* schema named <code>schemaName</code>. Class paths are defined using the
* SQL procedure <code>sqlj.set_classpath</code>.
* @param schemaName The name of the schema.
* @return A loader.
*/
public static ClassLoader getSchemaLoader(String schemaName)
throws SQLException
{
if(schemaName == null || schemaName.length() == 0)
schemaName = PUBLIC_SCHEMA;
else
schemaName = schemaName.toLowerCase();
ClassLoader loader = (ClassLoader)s_schemaLoaders.get(schemaName);
if(loader != null)
return loader;
Map classImages = new HashMap();
Connection conn = SQLUtils.getDefaultConnection();
PreparedStatement outer = null;
PreparedStatement inner = null;
try
{
// Read the entries so that the one with highest prio is read last.
//
outer = conn.prepareStatement(
"SELECT r.jarId" +
" FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c ON r.jarId = c.jarId" +
" WHERE c.schemaName = ? ORDER BY c.ordinal DESC");
inner = conn.prepareStatement(
"SELECT entryId, entryName FROM sqlj.jar_entry WHERE jarId = ?");
outer.setString(1, schemaName);
ResultSet rs = outer.executeQuery();
try
{
while(rs.next())
{
inner.setInt(1, rs.getInt(1));
ResultSet rs2 = inner.executeQuery();
try
{
while(rs2.next())
{
int entryId = rs2.getInt(1);
String entryName = rs2.getString(2);
int[] oldEntry = (int[])classImages.get(entryName);
if(oldEntry == null)
classImages.put(entryName, new int[] { entryId });
else
{
int last = oldEntry.length;
int[] newEntry = new int[last + 1];
newEntry[0] = entryId;
System.arraycopy(oldEntry, 0, newEntry, 1, last);
classImages.put(entryName, newEntry);
}
}
}
finally
{
SQLUtils.close(rs2);
}
}
}
finally
{
SQLUtils.close(rs);
}
}
finally
{
SQLUtils.close(outer);
SQLUtils.close(inner);
}
ClassLoader parent = ClassLoader.getSystemClassLoader();
if(classImages.size() == 0)
//
// No classpath defined for the schema. Default to
// classpath of public schema or to the system classloader if the
// request already is for the public schema.
//
loader = schemaName.equals(PUBLIC_SCHEMA) ? parent : getSchemaLoader(PUBLIC_SCHEMA);
else
loader = new Loader(classImages, parent);
s_schemaLoaders.put(schemaName, loader);
return loader;
}
/**
* Returns the SQL type {@link Oid} to Java {@link Class} map that contains the
* Java UDT mappings for the given <code>schema</code>.
* This method is called by the function mapping mechanisms. Application code
* should never call this method.
*
* @param schema The schema
* @return The Map, possibly empty but never <code>null</code>.
*/
public static Map getTypeMap(final String schema) throws SQLException
{
Map typesForSchema = (Map)s_typeMap.get(schema);
if(typesForSchema != null)
return typesForSchema;
s_logger.fine("Creating typeMappings for schema " + schema);
typesForSchema = new HashMap()
{
public Object get(Object key)
{
s_logger.fine("Obtaining type mapping for OID " + key + " for schema " + schema);
return super.get(key);
}
};
ClassLoader loader = Loader.getSchemaLoader(schema);
Statement stmt = SQLUtils.getDefaultConnection().createStatement();
ResultSet rs = null;
try
{
rs = stmt.executeQuery("SELECT javaName, sqlName FROM sqlj.typemap_entry");
while(rs.next())
{
try
{
String javaClassName = rs.getString(1);
String sqlName = rs.getString(2);
Class cls = loader.loadClass(javaClassName);
if(!SQLData.class.isAssignableFrom(cls))
throw new SQLException("Class " + javaClassName + " does not implement java.sql.SQLData");
Oid typeOid = Oid.forTypeName(sqlName);
typesForSchema.put(typeOid, cls);
s_logger.fine("Adding type mapping for OID " + typeOid + " -> class " + cls.getName() + " for schema " + schema);
}
catch(ClassNotFoundException e)
{
// Ignore, type is not know to this schema and that is ok
}
}
if(typesForSchema.isEmpty())
typesForSchema = Collections.EMPTY_MAP;
s_typeMap.put(schema, typesForSchema);
return typesForSchema;
}
finally
{
SQLUtils.close(rs);
SQLUtils.close(stmt);
}
}
private static URL entryURL(int entryId)
{
try
{
return new URL(
"dbf",
"localhost",
-1,
"/" + entryId,
EntryStreamHandler.getInstance());
}
catch(MalformedURLException e)
{
throw new RuntimeException(e);
}
}
private final Map m_entries;
/**
* Create a new Loader.
* @param entries
* @param parent
*/
Loader(Map entries, ClassLoader parent)
{
super(parent);
m_entries = entries;
}
protected Class findClass(final String name)
throws ClassNotFoundException
{
String path = name.replace('.', '/').concat(".class");
int[] entryId = (int[])m_entries.get(path);
if(entryId != null)
{
PreparedStatement stmt = null;
ResultSet rs = null;
try
{
// This code rely heavily on the fact that the connection
// is a singleton and that the prepared statement will live
// for the duration of the loader.
//
stmt = SQLUtils.getDefaultConnection().prepareStatement(
"SELECT entryImage FROM sqlj.jar_entry WHERE entryId = ?");
stmt.setInt(1, entryId[0]);
rs = stmt.executeQuery();
if(rs.next())
{
byte[] img = rs.getBytes(1);
rs.close();
rs = null;
return this.defineClass(name, img, 0, img.length);
}
}
catch(SQLException e)
{
Logger.getAnonymousLogger().log(Level.INFO, "Failed to load class", e);
throw new ClassNotFoundException(name + " due to: " + e.getMessage());
}
finally
{
SQLUtils.close(rs);
SQLUtils.close(stmt);
}
}
throw new ClassNotFoundException(name);
}
protected URL findResource(String name)
{
int[] entryIds = (int[])m_entries.get(name);
if(entryIds == null)
return null;
return entryURL(entryIds[0]);
}
protected Enumeration findResources(String name)
throws IOException
{
int[] entryIds = (int[])m_entries.get(name);
if(entryIds == null)
entryIds = new int[0];
return new EntryEnumeration(entryIds);
}
}