/* * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License * which accompanies this distribution, and is available at * http://opensource.org/licenses/BSD-3-Clause * * Contributors: * Tada AB * Purdue University */ package org.postgresql.pljava.management; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.ByteBuffer; import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharacterCodingException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLData; import java.sql.SQLDataException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientException; import java.sql.SQLSyntaxErrorException; import java.sql.Statement; import java.text.ParseException; import java.util.ArrayList; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.postgresql.pljava.Session; import org.postgresql.pljava.SessionManager; import org.postgresql.pljava.internal.AclId; import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.Oid; import org.postgresql.pljava.jdbc.SQLUtils; import org.postgresql.pljava.sqlj.Loader; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.Security.DEFINER; /** * This methods of this class are implementations of SQLJ commands. * <h1>SQLJ functions</h1> * <h2>install_jar</h2> * The install_jar command loads a jar file from a location appointed by an URL * or a binary image that constitutes the contents of a jar file into the SQLJ * jar repository. It is an error if a jar with the given name already exists in * the repository. * <h3>Usage 1</h3> * <blockquote><code>SELECT sqlj.install_jar(<jar_url>, <jar_name>, <deploy>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.install_jar(url...)</caption> * <tr> * <td valign="top"><b>jar_url</b></td> * <td>The URL that denotes the location of the jar that should be loaded </td> * </tr> * <tr> * <td valign="top"><b>jar_name</b></td> * <td>This is the name by which this jar can be referenced once it has been * loaded</td> * </tr> * <tr> * <td valign="top"><b>deploy</b></td> * <td>True if the jar should be deployed according to a {@link * org.postgresql.pljava.management.SQLDeploymentDescriptor deployment * descriptor}, false otherwise</td> * </tr> * </table></blockquote> * <h3>Usage 2</h3> * <blockquote><code>SELECT sqlj.install_jar(<jar_image>, <jar_name>, <deploy>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for * sqlj.install_jar(bytea...)</caption> * <tr> * <td valign="top"><b>jar_image</b></td> * <td>The byte array that constitutes the contents of the jar that should be * loaded </td> * </tr> * <tr> * <td valign="top"><b>jar_name</b></td> * <td>This is the name by which this jar can be referenced once it has been * loaded</td> * </tr> * <tr> * <td valign="top"><b>deploy</b></td> * <td>True if the jar should be deployed according to a {@link * org.postgresql.pljava.management.SQLDeploymentDescriptor deployment * descriptor}, false otherwise</td> * </tr> * </table></blockquote> * <h2>replace_jar</h2> * The replace_jar will replace a loaded jar with another jar. Use this command * to update already loaded files. It's an error if the jar is not found. * <h3>Usage 1</h3> * <blockquote><code>SELECT sqlj.replace_jar(<jar_url>, <jar_name>, <redeploy>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.replace_jar(url...)</caption> * <tr> * <td valign="top"><b>jar_url</b></td> * <td>The URL that denotes the location of the jar that should be loaded </td> * </tr> * <tr> * <td valign="top"><b>jar_name</b></td> * <td>The name of the jar to be replaced</td> * </tr> * <tr> * <td valign="top"><b>redeploy</b></td> * <td>True if the old and new jar should be undeployed and deployed according * to their respective {@link * org.postgresql.pljava.management.SQLDeploymentDescriptor deployment * descriptors}, false otherwise</td> * </tr> * </table></blockquote> * <h3>Usage 2</h3> * <blockquote><code>SELECT sqlj.replace_jar(<jar_image>, <jar_name>, <redeploy>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for * sqlj.replace_jar(bytea...)</caption> * <tr> * <td valign="top"><b>jar_image</b></td> * <td>The byte array that constitutes the contents of the jar that should be * loaded </td> * </tr> * <tr> * <td valign="top"><b>jar_name</b></td> * <td>The name of the jar to be replaced</td> * </tr> * <tr> * <td valign="top"><b>redeploy</b></td> * <td>True if the old and new jar should be undeployed and deployed according * to their respective {@link * org.postgresql.pljava.management.SQLDeploymentDescriptor deployment * descriptors}, false otherwise</td> * </tr> * </table></blockquote> * <h2>remove_jar</h2> * The remove_jar will drop the jar from the jar repository. Any classpath that * references this jar will be updated accordingly. It's an error if the jar is * not found. * <h3>Usage</h3> * <blockquote><code>SELECT sqlj.remove_jar(<jar_name>, <undeploy>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.remove_jar</caption> * <tr> * <td valign="top"><b>jar_name</b></td> * <td>The name of the jar to be removed</td> * </tr> * <tr> * <td valign="top"><b>undeploy</b></td> * <td>True if the jar should be undeployed according to its {@link * org.postgresql.pljava.management.SQLDeploymentDescriptor deployment * descriptor}, false otherwise</td> * </tr> * </table></blockquote> * <h2>get_classpath</h2> * The get_classpath will return the classpath that has been defined for the * given schema or NULL if the schema has no classpath. It's an error if the * given schema does not exist. * <h3>Usage</h3> * <blockquote><code>SELECT sqlj.get_classpath(<schema>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.get_classpath</caption> * <tr> * <td><b>schema</b></td> * <td>The name of the schema</td> * </tr> * </table></blockquote> * <h2>set_classpath</h2> * The set_classpath will define a classpath for the given schema. A classpath * consists of a colon separated list of jar names. It's an error if the given * schema does not exist or if one or more jar names references non existent * jars. * <h3>Usage</h3> * <blockquote><code>SELECT sqlj.set_classpath(<schema>, <classpath>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.set_classpath</caption> * <tr> * <td><b>schema</b></td> * <td>The name of the schema</td> * </tr> * <tr> * <td><b>classpath</b></td> * <td>The colon separated list of jar names</td> * </tr> * </table></blockquote> * <h2>add_type_mapping</h2> * The add_type_mapping defines the mapping between an SQL type and a Java * class. * <h3>Usage</h3> * <blockquote><code>SELECT sqlj.add_type_mapping(<sqlTypeName>, <className>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.add_type_mapping</caption> * <tr> * <td><b>sqlTypeName</b></td> * <td>The name of the SQL type. The name can be qualified with a * schema (namespace). If the schema is omitted, it will be resolved according * to the current setting of the search_path.</td> * </tr> * <tr> * <td><b>className</b></td> * <td>The name of the class. The class must be found in the classpath in * effect for the current schema</td> * </tr> * </table></blockquote> * <h2>drop_type_mapping</h2> * The drop_type_mapping removes the mapping between an SQL type and a Java * class. * <h3>Usage</h3> * <blockquote><code>SELECT sqlj.drop_type_mapping(<sqlTypeName>);</code> * </blockquote> * <h3>Parameters</h3> * <blockquote><table><caption>Parameters for sqlj.drop_type_mapping</caption> * <tr> * <td><b>sqlTypeName</b></td> * <td>The name of the SQL type. The name can be qualified with a * schema (namespace). If the schema is omitted, it will be resolved according * to the current setting of the search_path.</td> * </tr> * </table></blockquote> * * @author Thomas Hallgren * @author Chapman Flack */ /* * Attention: any evolution of the schema here needs to be reflected in * o.p.p.internal.InstallHelper.SchemaVariant and .recognizeSchema(). */ @SQLAction(install={ " CREATE TABLE sqlj.jar_repository(" + " jarId SERIAL PRIMARY KEY," + " jarName VARCHAR(100) UNIQUE NOT NULL," + " jarOrigin VARCHAR(500) NOT NULL," + " jarOwner NAME NOT NULL," + " jarManifest TEXT" + " )", " COMMENT ON TABLE sqlj.jar_repository IS" + " 'Information on jars loaded by PL/Java, one row per jar.'", " GRANT SELECT ON sqlj.jar_repository TO public", " CREATE TABLE sqlj.jar_entry(" + " entryId SERIAL PRIMARY KEY," + " entryName VARCHAR(200) NOT NULL," + " jarId INT NOT NULL" + " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " entryImage BYTEA NOT NULL," + " UNIQUE(jarId, entryName)" + " )", " COMMENT ON TABLE sqlj.jar_entry IS" + " 'Name and content of each entry in every jar loaded by PL/Java.'", " GRANT SELECT ON sqlj.jar_entry TO public", " CREATE TABLE sqlj.jar_descriptor(" + " jarId INT REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " ordinal INT2," + " PRIMARY KEY (jarId, ordinal)," + " entryId INT NOT NULL REFERENCES sqlj.jar_entry ON DELETE CASCADE" + " )", " COMMENT ON TABLE sqlj.jar_descriptor IS" + " 'Associates each jar with zero-or-more deployment descriptors (a row " + "for each), with ordinal indicating their order of mention in the " + "manifest.'", " GRANT SELECT ON sqlj.jar_descriptor TO public", " CREATE TABLE sqlj.classpath_entry(" + " schemaName VARCHAR(30) NOT NULL," + " ordinal INT2 NOT NULL," + " jarId INT NOT NULL" + " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " PRIMARY KEY(schemaName, ordinal)" + " )", " COMMENT ON TABLE sqlj.classpath_entry IS" + " 'Associates each schema with zero-or-more jars (a row " + "for each), with ordinal indicating their order of precedence in the " + "classpath.'", " GRANT SELECT ON sqlj.classpath_entry TO public", " CREATE TABLE sqlj.typemap_entry(" + " mapId SERIAL PRIMARY KEY," + " javaName VARCHAR(200) NOT NULL," + " sqlName NAME NOT NULL" + " )", " COMMENT ON TABLE sqlj.typemap_entry IS" + " 'A row for each SQL type <-> Java type custom mapping.'", " GRANT SELECT ON sqlj.typemap_entry TO public" }, remove={ " DROP TABLE sqlj.typemap_entry", " DROP TABLE sqlj.jar_repository CASCADE" }) public class Commands { private final static Logger s_logger = Logger.getLogger(Commands.class .getName()); /** * Reads the jar found at the specified URL and stores the entries in the * jar_entry table. * * @param jarId The id used for the foreign key to the jar_repository table * @param urlStream An InputStream (opened on what may have been a URL) * @param sz The expected size of the stream, used as a worst-case * mark/reset limit. The caller might pass -1 if the URLConnection can't * determine a size in advance (a generous guess will be made in that case). * @throws SQLException */ public static void addClassImages(int jarId, InputStream urlStream, int sz) throws SQLException { PreparedStatement stmt = null; PreparedStatement descIdFetchStmt = null; PreparedStatement descIdStoreStmt = null; ResultSet rs = null; try { byte[] buf = new byte[1024]; ByteArrayOutputStream img = new ByteArrayOutputStream(); stmt = SQLUtils .getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.jar_entry(entryName, jarId, entryImage) VALUES(?, ?, ?)"); BufferedInputStream bis = new BufferedInputStream( urlStream); String manifest = rawManifest( bis, sz); JarInputStream jis = new JarInputStream(bis); if(manifest != null) { PreparedStatement us = SQLUtils .getDefaultConnection() .prepareStatement( "UPDATE sqlj.jar_repository SET jarManifest = ? WHERE jarId = ?"); try { us.setString(1, manifest); us.setInt(2, jarId); if(us.executeUpdate() != 1) throw new SQLException( "Jar repository update did not update 1 row"); } finally { SQLUtils.close(us); } } for(;;) { JarEntry je = jis.getNextJarEntry(); if(je == null) break; if(je.isDirectory()) continue; String entryName = je.getName(); int nBytes; img.reset(); while((nBytes = jis.read(buf)) > 0) img.write(buf, 0, nBytes); jis.closeEntry(); stmt.setString(1, entryName); stmt.setInt(2, jarId); stmt.setBytes(3, img.toByteArray()); if(stmt.executeUpdate() != 1) throw new SQLException( "Jar entry insert did not insert 1 row"); } Matcher ddr = ddrSection.matcher( null != manifest ? manifest : ""); Matcher cnt = mfCont.matcher( ""); for ( int ordinal = 0; ddr.find(); ++ ordinal ) { String entryName = cnt.reset( ddr.group( 1)).replaceAll( ""); if ( descIdFetchStmt == null ) descIdFetchStmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT entryId FROM sqlj.jar_entry" + " WHERE jarId = ? AND entryName = ?"); descIdFetchStmt.setInt(1, jarId); descIdFetchStmt.setString(2, entryName); rs = descIdFetchStmt.executeQuery(); if(!rs.next()) throw new SQLException( "Failed to refetch row in sqlj.jar_entry"); int deployImageId = rs.getInt(1); if ( descIdStoreStmt == null ) descIdStoreStmt = SQLUtils.getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.jar_descriptor" + " (jarId, entryId, ordinal) VALUES" + " ( ?, ?, ? )"); descIdStoreStmt.setInt(1, jarId); descIdStoreStmt.setInt(2, deployImageId); descIdStoreStmt.setInt(3, ordinal); if ( descIdStoreStmt.executeUpdate() != 1 ) throw new SQLException( "Jar deployment descriptor insert did not insert " + "1 row"); } } catch(IOException e) { throw new SQLException("I/O exception reading jar file: " + e.getMessage(), "58030", e); } finally { SQLUtils.close(rs); SQLUtils.close(descIdStoreStmt); SQLUtils.close(descIdFetchStmt); SQLUtils.close(stmt); } } private final static Pattern ddrSection = Pattern.compile( "(?<=[\\r\\n])Name: ((?:.|(?:\\r\\n?+|\\n) )++)(?:(?:\\r\\n?+|\\n))" + "(?:[^\\r\\n]++(?:\\r\\n?+|\\n)(?![\\r\\n]))*" + "SQLJDeploymentDescriptor: (?:(?:\\r\\n?+|\\r) )*+TRUE(?!\\S)", Pattern.CASE_INSENSITIVE ); private final static Pattern mfCont = Pattern.compile( "(?:\\r\\n?+|\\n) "); /** * Read and return a manifest, rewinding the buffered input stream. * * The caller needs to construct a BufferedInputStream over its raw input * stream, and indicate its expected size (for mark/reset purposes). This * method returns the manifest as a String if there is one, else null, and * resets the buffered input stream so the caller can treat it as a * JarInputStream to read the rest of it. * * Why such an exercise? The SQL/JRT specs provide that deployment * descriptors are to be taken, for install, in the order they * are named _in the manifest_, and for remove in the reverse of * their order in the manifest ... at least according to a committee * draft 5CD2-13-JRT-2006-01 I got my hands on; see sections 11.1 * and 11.3. That's lovely, but of course Java's Manifest class * doesn't expose the order of its per-file entries! Plan B could be * to use Manifest.write() into a buffer and parse that for the * order, but the API doesn't promise to write it back out in the * original order, and in fact Oracle's implementation doesn't. That * leaves little choice but to sneak in ahead of the JarInputStream and * pluck out the original manifest as a zip entry. */ private static String rawManifest( BufferedInputStream bis, int markLimit) throws IOException { // If the caller can't say how long the stream is, this mark() limit // should be plenty bis.mark( markLimit > 0 ? markLimit : 32*1024*1024); ZipInputStream zis = new ZipInputStream( bis); for ( ZipEntry ze; null != (ze = zis.getNextEntry()); ) { if ( "META-INF/MANIFEST.MF".equals( ze.getName()) ) { StringBuilder sb = new StringBuilder(); CharsetDecoder u8 = UTF_8.newDecoder(); InputStreamReader isr = new InputStreamReader( zis, u8); char[] b = new char[512]; for ( int got; -1 != (got = isr.read(b)); ) sb.append(b, 0, got); zis.closeEntry(); bis.reset(); return sb.toString(); } zis.closeEntry(); } bis.reset(); return null; } /** * Defines the mapping between an SQL type and a Java class. * * @param sqlTypeName The name of the SQL type. The name can be * qualified with a schema (namespace). If the schema is omitted, * it will be resolved according to the current setting of the * {@code search_path}. * @param javaClassName The name of the class. The class must be found in * the classpath in effect for the current schema * @throws SQLException if the type or class cannot be found, or if the * invoking user does not own the type. */ @Function(schema="sqlj", name="add_type_mapping", security=DEFINER) public static void addTypeMapping(String sqlTypeName, String javaClassName) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.typemap_entry(javaName, sqlName)" + " VALUES(?,?)")) { ClassLoader loader = Loader.getCurrentLoader(); Class cls = loader.loadClass(javaClassName); if(!SQLData.class.isAssignableFrom(cls)) throw new SQLException("Class " + javaClassName + " does not implement java.sql.SQLData"); sqlTypeName = getFullSqlNameOwned(sqlTypeName); stmt.setString(1, javaClassName); stmt.setString(2, sqlTypeName); stmt.executeUpdate(); } catch(ClassNotFoundException e) { throw new SQLException( "No such class: " + javaClassName, "46103", e); } Loader.clearSchemaLoaders(); } /** * Drops the mapping between an SQL type and a Java class. * * @param sqlTypeName The name of the SQL type. The name can be * qualified with a schema (namespace). If the schema is omitted, * it will be resolved according to the current setting of the * {@code search_path}. * @throws SQLException if the type cannot be found, or if the * invoking user does not own the type. */ @Function(schema="sqlj", name="drop_type_mapping", security=DEFINER) public static void dropTypeMapping(String sqlTypeName) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "DELETE FROM sqlj.typemap_entry WHERE sqlName = ?")) { sqlTypeName = getFullSqlNameOwned(sqlTypeName); stmt.setString(1, sqlTypeName); stmt.executeUpdate(); } Loader.clearSchemaLoaders(); } /** * Return the classpath that has been defined for the schema named * {@code schemaName}. This method is exposed in SQL as * {@code sqlj.get_classpath(VARCHAR)}. * * @param schemaName Name of the schema for which this path is valid. * @return The defined classpath or {@code null} if this schema has * no classpath. * @throws SQLException */ @Function(schema="sqlj", name="get_classpath", security=DEFINER) public static String getClassPath(String schemaName) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT r.jarName"+ " FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c"+ " ON r.jarId = c.jarId"+ " WHERE c.schemaName = ? ORDER BY c.ordinal")) { if(schemaName == null || schemaName.length() == 0) schemaName = "public"; else schemaName = schemaName.toLowerCase(); stmt.setString(1, schemaName); StringBuffer buf = null; try(ResultSet rs = stmt.executeQuery()) { while(rs.next()) { if(buf == null) buf = new StringBuffer(); else buf.append(':'); buf.append(rs.getString(1)); } } return (buf == null) ? null : buf.toString(); } } public static String getCurrentSchema() throws SQLException { Session session = SessionManager.current(); return ((org.postgresql.pljava.internal.Session)session) .getOuterUserSchema(); } /** * Installs a new Jar in the database jar repository under name * {@code jarName}. Once installed classpaths can be defined that * refrences this jar. This method is exposed in SQL as * {@code sqlj.install_jar(BYTEA, VARCHAR, BOOLEAN)}. * * @param image The byte array that constitutes the jar content. * @param jarName The name by which the system will refer to this jar. * @param deploy If set, execute install commands found in the deployment * descriptor. * @throws SQLException if the {@code jarName} contains characters * that are invalid or if the named jar already exists in the * system. * @see #setClassPath */ @Function(schema="sqlj", name="install_jar", security=DEFINER) public static void installJar( @SQLType("bytea") byte[] image, String jarName, boolean deploy) throws SQLException { installJar("streamed byte image", jarName, deploy, image); } /** * Installs a new Jar in the database jar repository under name * {@code jarName}. Once installed classpaths can be defined that * refrences this jar. This method is exposed in SQL as * {@code sqlj.install_jar(VARCHAR, VARCHAR, BOOLEAN)}. * * @param urlString The location of the jar that will be installed. * @param jarName The name by which the system will refer to this jar. * @param deploy If set, execute install commands found in the deployment * descriptor. * @throws SQLException if the {@code jarName} contains characters * that are invalid or if the named jar already exists in the * system. * @see #setClassPath */ @Function(schema="sqlj", name="install_jar", security=DEFINER) public static void installJar(String urlString, String jarName, boolean deploy) throws SQLException { installJar(urlString, jarName, deploy, null); } /** * Removes the jar named {@code jarName} from the database jar * repository. Class path entries that references this jar will also be * removed (just the entry, not the whole path). This method is exposed in * SQL as {@code sqlj.remove_jar(VARCHAR, BOOLEAN)}. * * @param jarName The name by which the system referes this jar. * @param undeploy If set, execute remove commands found in the deployment * descriptor of the jar. * @throws SQLException if the named jar cannot be found in the repository. */ @Function(schema="sqlj", name="remove_jar", security=DEFINER) public static void removeJar(String jarName, boolean undeploy) throws SQLException { assertJarName(jarName); AclId[] ownerRet = new AclId[1]; int jarId = getJarId(jarName, ownerRet); if(jarId < 0) throw new SQLException("No Jar named '" + jarName + "' is known to the system", "4600B"); AclId user = AclId.getOuterUser(); if(!(user.isSuperuser() || user.equals(ownerRet[0]))) throw new SQLSyntaxErrorException( "Only super user or owner can remove a jar", "42501"); if(undeploy) deployRemove(jarId, jarName); PreparedStatement stmt = SQLUtils .getDefaultConnection() .prepareStatement("DELETE FROM sqlj.jar_repository WHERE jarId = ?"); try { stmt.setInt(1, jarId); if(stmt.executeUpdate() != 1) throw new SQLException( "Jar repository update did not update 1 row"); } finally { SQLUtils.close(stmt); } Loader.clearSchemaLoaders(); } /** * Replaces the image of jar named {@code jarName} in the database * jar repository. This method is exposed in SQL as * {@code sqlj.replace_jar(BYTEA, VARCHAR, BOOLEAN)}. * * @param jarImage The byte array that constitutes the jar content. * @param jarName The name by which the system referes this jar. * @param redeploy If set, execute remove commands found in the deployment * descriptor of the old jar and install commands found in the * deployment descriptor of the new jar. * @throws SQLException if the named jar cannot be found in the repository. */ @Function(schema="sqlj", name="replace_jar", security=DEFINER) public static void replaceJar( @SQLType("bytea") byte[] jarImage, String jarName, boolean redeploy) throws SQLException { replaceJar("streamed byte image", jarName, redeploy, jarImage); } /** * Replaces the image of jar named {@code jarName} in the database * jar repository. This method is exposed in SQL as * {@code sqlj.replace_jar(VARCHAR, VARCHAR, BOOLEAN)}. * * @param urlString The location of the jar that will be installed. * @param jarName The name by which the system referes this jar. * @param redeploy If set, execute remove commands found in the deployment * descriptor of the old jar and install commands found in the * deployment descriptor of the new jar. * @throws SQLException if the named jar cannot be found in the repository. */ @Function(schema="sqlj", name="replace_jar", security=DEFINER) public static void replaceJar(String urlString, String jarName, boolean redeploy) throws SQLException { replaceJar(urlString, jarName, redeploy, null); } /** * Define the class path to use for Java functions, triggers, and procedures * that are created in the schema named {@code schemaName}. This * method is exposed in SQL as * {@code sqlj.set_classpath(VARCHAR, VARCHAR)}. * * @param schemaName Name of the schema for which this path is valid. * @param path Colon separated list of names. Each name must denote the name * of a jar that is present in the jar repository. * @throws SQLException If no schema can be found with the givene name, or * if one or several names of the path denotes a nonexistant jar * file. */ @Function(schema="sqlj", name="set_classpath", security=DEFINER) public static void setClassPath(String schemaName, String path) throws SQLException { if(schemaName == null || schemaName.length() == 0) schemaName = "public"; schemaName = schemaName.toLowerCase(); if("public".equals(schemaName)) { if(!AclId.getOuterUser().isSuperuser()) throw new SQLSyntaxErrorException( // yeah, for 42501, really "Permission denied. Only a super user can set the " + "classpath of the public schema", "42501"); } else { Oid schemaId = getSchemaId(schemaName); if(schemaId == null) throw new SQLNonTransientException( "No such schema: " + schemaName, "3F000"); if(!AclId.getOuterUser().hasSchemaCreatePermission(schemaId)) throw new SQLSyntaxErrorException( "Permission denied. User must have create permission on " + "the target schema in order to set the classpath", "42501"); } ArrayList entries = null; if(path != null && path.length() > 0) { // Collect and verify that all entries in the path represents a // valid jar // entries = new ArrayList(); try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT jarId FROM sqlj.jar_repository WHERE jarName = ?")) { for(;;) { int colon = path.indexOf(':'); String jarName; if(colon >= 0) { jarName = path.substring(0, colon); path = path.substring(colon + 1); } else jarName = path; int jarId = getJarId(stmt, jarName, null); if(jarId < 0) throw new SQLNonTransientException( "No such jar: " + jarName, "46102"); entries.add(new Integer(jarId)); if(colon < 0) break; } } } // Delete the old classpath // try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "DELETE FROM sqlj.classpath_entry WHERE schemaName = ?")) { stmt.setString(1, schemaName); stmt.executeUpdate(); } if(entries != null) { // Insert the new path. // ; try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.classpath_entry("+ " schemaName, ordinal, jarId) VALUES(?, ?, ?)")) { int top = entries.size(); for(int idx = 0; idx < top; ++idx) { int jarId = ((Integer)entries.get(idx)).intValue(); stmt.setString(1, schemaName); stmt.setInt(2, idx + 1); stmt.setInt(3, jarId); stmt.executeUpdate(); } } } Loader.clearSchemaLoaders(); } private static boolean assertInPath(String jarName, String[] originalSchemaAndPath) throws SQLException { String currentSchema = getCurrentSchema(); String currentClasspath = getClassPath(currentSchema); originalSchemaAndPath[0] = currentSchema; originalSchemaAndPath[1] = currentClasspath; if(currentClasspath == null) { setClassPath(currentSchema, jarName); return true; } String[] elems = currentClasspath.split(":"); int idx = elems.length; boolean found = false; while(--idx >= 0) if(elems[idx].equals(jarName)) { found = true; break; } if(found) return false; setClassPath(currentSchema, jarName + ':' + currentClasspath); return true; } /** * Throws an exception if the given name cannot be used as the name of a * jar. * * @param jarName The name to check. * @throws IOException */ private static void assertJarName(String jarName) throws SQLException { if(jarName != null) { int len = jarName.length(); if(len > 0 && Character.isJavaIdentifierStart(jarName.charAt(0))) { int idx = 1; for(; idx < len; ++idx) if(!Character.isJavaIdentifierPart(jarName.charAt(idx))) break; if(idx == len) return; } } throw new SQLNonTransientException("The jar name '" + jarName + "' is not a valid name", "46002"); } private static void deployInstall(int jarId, String jarName) throws SQLException { SQLDeploymentDescriptor[] depDesc = getDeploymentDescriptors(jarId); String[] originalSchemaAndPath = new String[2]; boolean classpathChanged = assertInPath(jarName, originalSchemaAndPath); for ( SQLDeploymentDescriptor dd : depDesc ) dd.install(SQLUtils.getDefaultConnection()); if (classpathChanged) setClassPath(originalSchemaAndPath[0], originalSchemaAndPath[1]); } private static void deployRemove(int jarId, String jarName) throws SQLException { SQLDeploymentDescriptor[] depDesc = getDeploymentDescriptors(jarId); String[] originalSchemaAndPath = new String[2]; boolean classpathChanged = assertInPath(jarName, originalSchemaAndPath); for ( int i = depDesc.length ; i --> 0 ; ) depDesc[i].remove(SQLUtils.getDefaultConnection()); try { if (classpathChanged) setClassPath(originalSchemaAndPath[0],originalSchemaAndPath[1]); } catch ( SQLException sqle ) { if ( ! "3F000".equals(sqle.getSQLState()) ) throw sqle; } } private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) throws SQLException { ResultSet rs = null; PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT e.entryImage" + " FROM sqlj.jar_descriptor d INNER JOIN sqlj.jar_entry e" + " ON d.entryId = e.entryId" + " WHERE d.jarId = ?" + " ORDER BY d.ordinal"); try { stmt.setInt(1, jarId); rs = stmt.executeQuery(); ArrayList<SQLDeploymentDescriptor> sdds = new ArrayList<>(); while(rs.next()) { ByteBuffer bytes = ByteBuffer.wrap(rs.getBytes(1)); // According to the SQLJ standard, this entry must be // UTF8 encoded. // sdds.add( new SQLDeploymentDescriptor( UTF_8.newDecoder().decode(bytes).toString())); } return sdds.toArray( new SQLDeploymentDescriptor[sdds.size()]); } catch(CharacterCodingException e) { throw new SQLDataException( "deployment descriptor is not well-formed UTF-8", "22021", e); } catch(ParseException e) { throw new SQLSyntaxErrorException(String.format( "%1$s at %2$s", e.getMessage(), e.getErrorOffset()), "42601", e); } finally { SQLUtils.close(rs); SQLUtils.close(stmt); } } /* * In addition to resolving the type name to a fully qualified one, also * make sure the current (outer) user is, or is granted, the type's owning * role. */ private static String getFullSqlNameOwned(String sqlTypeName) throws SQLException { Oid typeId = Oid.forTypeName(sqlTypeName); s_logger.info("Type id = " + typeId.toString()); AclId invoker = AclId.getOuterUser(); try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT n.nspname, t.typname," + " pg_catalog.pg_has_role(?, t.typowner, 'USAGE')" + " FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n" + " WHERE t.oid = ? AND n.oid = t.typnamespace")) { stmt.setObject(1, invoker); stmt.setObject(2, typeId); try(ResultSet rs = stmt.executeQuery()) { if(!rs.next()) throw new SQLException("Unable to obtain type info for " + typeId); if ( ! rs.getBoolean(3) ) throw new SQLSyntaxErrorException( // yes, for 42501, really "Permission denied. Only superuser or type's owner " + "may add or drop a type mapping.", "42501"); return rs.getString(1) + '.' + rs.getString(2); } } } private static int getJarId(PreparedStatement stmt, String jarName, AclId[] ownerRet) throws SQLException { stmt.setString(1, jarName); try(ResultSet rs = stmt.executeQuery()) { if(!rs.next()) return -1; int id = rs.getInt(1); if(ownerRet != null) { String ownerName = rs.getString(2); ownerRet[0] = AclId.fromName(ownerName); } return id; } } /** * Returns the primary key identifier for the given Jar. * * @param jarName The name of the jar. * @param ownerRet a caller-supplied array in which the jar's owner will * be returned. * @return The primary key value of the given jar or <code>-1</code> if no * such jar is found. * @throws SQLException */ private static int getJarId(String jarName, AclId[] ownerRet) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT jarId, jarOwner FROM sqlj.jar_repository"+ " WHERE jarName = ?")) { return getJarId(stmt, jarName, ownerRet); } } /** * Returns the Oid for the given Schema. * * @param schemaName The name of the schema. * @return The Oid of the given schema or <code>null</code> if no such * schema is found. * @throws SQLException */ private static Oid getSchemaId(String schemaName) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?")) { stmt.setString(1, schemaName); try(ResultSet rs = stmt.executeQuery()) { if(!rs.next()) return null; return (Oid)rs.getObject(1); } } } private static void installJar(String urlString, String jarName, boolean deploy, byte[] image) throws SQLException { if ( Backend.isCreatingExtension() ) throw new SQLFeatureNotSupportedException( "A jar cannot (yet) be installed as an extension in its " + "own right.", "0A000"); assertJarName(jarName); if(getJarId(jarName, null) >= 0) throw new SQLNonTransientException("A jar named '" + jarName + "' already exists", "46002"); PreparedStatement stmt = SQLUtils .getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.jar_repository(jarName, jarOrigin, jarOwner) VALUES(?, ?, ?)"); try { stmt.setString(1, jarName); stmt.setString(2, urlString); stmt.setString(3, AclId.getOuterUser().getName()); if(stmt.executeUpdate() != 1) throw new SQLException( "Jar repository insert did not insert 1 row"); } finally { SQLUtils.close(stmt); } AclId[] ownerRet = new AclId[1]; int jarId = getJarId(jarName, ownerRet); if(jarId < 0) throw new SQLException("Unable to obtain id of '" + jarName + "'"); if(image == null) Backend.addClassImages(jarId, urlString); else { InputStream imageStream = new ByteArrayInputStream(image); addClassImages(jarId, imageStream, image.length); } Loader.clearSchemaLoaders(); if(deploy) deployInstall(jarId, jarName); } private static void replaceJar(String urlString, String jarName, boolean redeploy, byte[] image) throws SQLException { AclId[] ownerRet = new AclId[1]; int jarId = getJarId(jarName, ownerRet); if(jarId < 0) throw new SQLNonTransientException("No Jar named '" + jarName + "' is known to the system", "4600A"); AclId user = AclId.getOuterUser(); if(!(user.isSuperuser() || user.equals(ownerRet[0]))) throw new SQLSyntaxErrorException( "Only super user or owner can replace a jar", "42501"); if(redeploy) deployRemove(jarId, jarName); PreparedStatement stmt = SQLUtils .getDefaultConnection() .prepareStatement( "UPDATE sqlj.jar_repository " + "SET jarOrigin = ?, jarOwner = ?, jarManifest = NULL " + "WHERE jarId = ?"); try { stmt.setString(1, urlString); stmt.setString(2, user.getName()); stmt.setInt(3, jarId); if(stmt.executeUpdate() != 1) throw new SQLException( "Jar repository update did not update 1 row"); } finally { SQLUtils.close(stmt); } stmt = SQLUtils.getDefaultConnection().prepareStatement( "DELETE FROM sqlj.jar_entry WHERE jarId = ?"); try { stmt.setInt(1, jarId); stmt.executeUpdate(); } finally { SQLUtils.close(stmt); } if(image == null) Backend.addClassImages(jarId, urlString); else { InputStream imageStream = new ByteArrayInputStream(image); addClassImages(jarId, imageStream, image.length); } Loader.clearSchemaLoaders(); if(redeploy) deployInstall(jarId, jarName); } }