/* * Copyright (c) 2015 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: * Chapman Flack */ package org.postgresql.pljava.internal; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLNonTransientException; import java.sql.Savepoint; import java.sql.Statement; import java.text.ParseException; import java.util.Scanner; import static java.nio.charset.StandardCharsets.UTF_8; import static java.sql.Types.VARCHAR; import org.postgresql.pljava.jdbc.SQLUtils; import org.postgresql.pljava.management.SQLDeploymentDescriptor; import static org.postgresql.pljava.sqlgen.DDRWriter.eQuote; /** * Group of methods intended to streamline the PL/Java installation/startup * experience. * * @author Chapman Flack */ public class InstallHelper { private static void setPropertyIfNull( String property, String value) { if ( null == System.getProperty( property) ) System.setProperty( property, value); } public static String hello( String nativeVer, String user, String dbname, String clustername, String datadir, String libdir, String sharedir, String etcdir) { String implVersion = InstallHelper.class.getPackage().getImplementationVersion(); /* * visualvm.display.name is not really used as a property. jvisualvm * picks it up by looking for -Dvisualvm.display.name=something in the * JVM invocation arguments, not by looking at actual properties. */ System.clearProperty( "visualvm.display.name"); System.setProperty( "user.name", user); setPropertyIfNull( "java.awt.headless", "true"); setPropertyIfNull( "org.postgresql.database", dbname); if ( null != clustername ) setPropertyIfNull( "org.postgresql.cluster", clustername); setPropertyIfNull( "org.postgresql.datadir", datadir); setPropertyIfNull( "org.postgresql.libdir", libdir); setPropertyIfNull( "org.postgresql.sharedir", sharedir); setPropertyIfNull( "org.postgresql.sysconfdir", etcdir); setPropertyIfNull( "org.postgresql.pljava.version", implVersion); setPropertyIfNull( "org.postgresql.pljava.native.version", nativeVer); setPropertyIfNull( "org.postgresql.version", Backend.getConfigOption( "server_version")); /* * As stipulated by JRT-2003 */ setPropertyIfNull( "sqlj.defaultconnection", "jdbc:default:connection"); /* * Set the org.postgresql.pljava.udt.byteorder.{scalar,mirror}.{p2j,j2p} * properties. For shorthand, defaults can be given in shorter property * keys org.postgresql.pljava.udt.byteorder.{scalar,mirror} or even just * org.postgresql.pljava.udt.byteorder for an overall default. These * shorter keys are then removed from the system properties. */ String orderKey = "org.postgresql.pljava.udt.byteorder"; String orderAll = System.getProperty(orderKey); String orderScalar = System.getProperty(orderKey + ".scalar"); String orderMirror = System.getProperty(orderKey + ".mirror"); if ( null == orderScalar ) orderScalar = null != orderAll ? orderAll : "big_endian"; if ( null == orderMirror ) orderMirror = null != orderAll ? orderAll : "native"; setPropertyIfNull(orderKey + ".scalar.p2j", orderScalar); setPropertyIfNull(orderKey + ".scalar.j2p", orderScalar); setPropertyIfNull(orderKey + ".mirror.p2j", orderMirror); setPropertyIfNull(orderKey + ".mirror.j2p", orderMirror); System.clearProperty(orderKey); System.clearProperty(orderKey + ".scalar"); System.clearProperty(orderKey + ".mirror"); /* * Construct the strings announcing the versions in use. */ String jreName = System.getProperty( "java.runtime.name"); String jreVer = System.getProperty( "java.runtime.version"); if ( null == jreName || null == jreVer ) { jreName = System.getProperty( "java.vendor"); jreVer = System.getProperty( "java.version"); } String vmName = System.getProperty( "java.vm.name"); String vmVer = System.getProperty( "java.vm.version"); String vmInfo = System.getProperty( "java.vm.info"); StringBuilder sb = new StringBuilder(); sb.append( "PL/Java native code (").append( nativeVer).append( ")\n"); sb.append( "PL/Java common code (").append( implVersion).append( ")\n"); sb.append( jreName).append( " (").append( jreVer).append( ")\n"); sb.append( vmName).append( " (").append( vmVer); if ( null != vmInfo ) sb.append( ", ").append( vmInfo); sb.append( ')'); return sb.toString(); } public static void groundwork( String module_pathname, String loadpath_tbl, String loadpath_tbl_quoted, boolean asExtension, boolean exNihilo) throws SQLException, ParseException, IOException { try(Connection c = SQLUtils.getDefaultConnection(); Statement s = c.createStatement()) { schema(c, s); SchemaVariant sv = recognizeSchema(c, s, loadpath_tbl); if ( null == sv ) throw new SQLNonTransientException( "Failed to recognize schema of PL/Java installation", "55000"); if ( asExtension && exNihilo && SchemaVariant.EMPTY != sv ) throw new SQLNonTransientException( "sqlj schema not empty for CREATE EXTENSION pljava", "55000"); handlers(c, s, module_pathname); languages(c, s); deployment(c, s, sv); if ( asExtension ) /* * Extension scripts (which create this table before issuing a * LOAD command) need a way to confirm something happened (it * won't, if the library was already loaded in the same * session). As simple confirmation, drop the table here. * Although a simple SQL script can't raise arbitrary errors, * it can certainly confirm this table is gone by trying to * create it again, which will cause a (cryptic, but reliable) * error if this code didn't execute. */ s.execute("DROP TABLE sqlj." + loadpath_tbl_quoted); } } private static void schema( Connection c, Statement s) throws SQLException { Savepoint p = null; try { p = c.setSavepoint(); s.execute("CREATE SCHEMA sqlj"); s.execute("COMMENT ON SCHEMA sqlj IS '"+ "Schema for objects pertaining to PL/Java, as specified by " + "\"SQL/JRT\" part 13 of the SQL standard, Java Routines and Types.'" ); s.execute("GRANT USAGE ON SCHEMA sqlj TO public"); c.releaseSavepoint(p); } catch ( SQLException sqle ) { c.rollback(p); if ( ! "42P06".equals(sqle.getSQLState()) ) throw sqle; } } private static void handlers( Connection c, Statement s, String module_path) throws SQLException { s.execute( "CREATE OR REPLACE FUNCTION sqlj.java_call_handler()" + " RETURNS language_handler" + " AS " + eQuote(module_path) + " LANGUAGE C"); s.execute("REVOKE ALL PRIVILEGES" + " ON FUNCTION sqlj.java_call_handler() FROM public"); ResultSet rs = s.executeQuery( "SELECT pg_catalog.obj_description(CAST(" + "'sqlj.java_call_handler()' AS pg_catalog.regprocedure), " + "'pg_proc')"); rs.next(); rs.getString(1); boolean noComment = rs.wasNull(); rs.close(); if ( noComment ) s.execute( "COMMENT ON FUNCTION sqlj.java_call_handler() IS '" + "Function-call handler for PL/Java''s trusted/sandboxed " + "language.'"); s.execute( "CREATE OR REPLACE FUNCTION sqlj.javau_call_handler()" + " RETURNS language_handler" + " AS " + eQuote(module_path) + " LANGUAGE C"); s.execute("REVOKE ALL PRIVILEGES" + " ON FUNCTION sqlj.javau_call_handler() FROM public"); rs = s.executeQuery( "SELECT pg_catalog.obj_description(CAST(" + "'sqlj.javau_call_handler()' AS pg_catalog.regprocedure), " + "'pg_proc')"); rs.next(); rs.getString(1); noComment = rs.wasNull(); rs.close(); if ( noComment ) s.execute( "COMMENT ON FUNCTION sqlj.javau_call_handler() IS '" + "Function-call handler for PL/Java''s untrusted/unsandboxed " + "language.'"); } private static void languages( Connection c, Statement s) throws SQLException { Savepoint p = null; try { p = c.setSavepoint(); s.execute( "CREATE TRUSTED LANGUAGE java HANDLER sqlj.java_call_handler"); s.execute( "COMMENT ON LANGUAGE java IS '" + "Trusted/sandboxed language for routines and types in " + "Java; http://tada.github.io/pljava/'"); s.execute("REVOKE USAGE ON LANGUAGE java FROM PUBLIC"); c.releaseSavepoint(p); } catch ( SQLException sqle ) { c.rollback(p); if ( ! "42710".equals(sqle.getSQLState()) ) throw sqle; } try { p = c.setSavepoint(); s.execute( "CREATE LANGUAGE javaU HANDLER sqlj.javau_call_handler"); s.execute( "COMMENT ON LANGUAGE javau IS '" + "Untrusted/unsandboxed language for routines and types in " + "Java; http://tada.github.io/pljava/'"); c.releaseSavepoint(p); } catch ( SQLException sqle ) { c.rollback(p); if ( ! "42710".equals(sqle.getSQLState()) ) throw sqle; } } /** * Execute the deployment descriptor for PL/Java itself, creating the * expected tables, functions, etc. Will be skipped if tables conforming * to the currently expected schema already seem to be there. If an earlier * schema variant is detected, attempt to migrate to the current one. */ private static void deployment( Connection c, Statement s, SchemaVariant sv) throws SQLException, ParseException, IOException { if ( currentSchema == sv ) return; // assume (optimistically) that means there's nothing to do if ( SchemaVariant.EMPTY != sv ) { currentSchema.migrateFrom( sv, c, s); return; } StringBuilder sb; try(InputStream is = InstallHelper.class.getResourceAsStream("/pljava.ddr"); InputStreamReader isr = new InputStreamReader(is, UTF_8.newDecoder())) { sb = new StringBuilder(); char[] buf = new char[512]; for ( int got; -1 != (got = isr.read(buf)); ) sb.append(buf, 0, got); } SQLDeploymentDescriptor sdd = new SQLDeploymentDescriptor(sb.toString()); sdd.install(c); } /** * Detect an existing PL/Java sqlj schema. Tests for changes between schema * variants that have appeared in PL/Java's git history and will return a * correct result if the schema actually is any of those, but does no * further verification. So, a known SchemaVariant could be returned for a * messed up schema that never appeared in the git history, if it happened * to match on the tested parts. The variant EMPTY is returned if nothing is * in the schema (based on a direct query of pg_depend, which ought to be * reliable) except an entry for the extension if applicable. A null return * indicates that whatever is there didn't match the tests for any known * variant. */ private static SchemaVariant recognizeSchema( Connection c, Statement s, String loadpath_tbl) throws SQLException { DatabaseMetaData md = c.getMetaData(); ResultSet rs = md.getColumns( null, "sqlj", "jar_descriptor", null); boolean seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.UNREL20130301b; rs = md.getColumns( null, "sqlj", "jar_descriptors", null); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.UNREL20130301a; rs = md.getColumns( null, "sqlj", "jar_repository", "jarmanifest"); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.REL_1_3_0; rs = md.getColumns( null, "sqlj", "typemap_entry", null); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.UNREL20060212; rs = md.getColumns( null, "sqlj", "jar_repository", "jarowner"); if ( rs.next() ) { int t = rs.getInt("DATA_TYPE"); rs.close(); if ( VARCHAR == t ) return SchemaVariant.UNREL20060125; return SchemaVariant.REL_1_1_0; } rs.close(); rs = md.getColumns( null, "sqlj", "jar_repository", "deploymentdesc"); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.REL_1_0_0; rs = md.getColumns( null, "sqlj", "jar_entry", null); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.UNREL20040121; rs = md.getColumns( null, "sqlj", "jar_repository", "jarimage"); seen = rs.next(); rs.close(); if ( seen ) return SchemaVariant.UNREL20040120; PreparedStatement ps = c.prepareStatement( "SELECT count(*) " + "FROM pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + "WHERE refclassid = 'pg_catalog.pg_namespace'::regclass " + "AND refobjid = n.oid AND nspname = 'sqlj' " + "AND deptype = 'n' " + "AND NOT EXISTS ( " + " SELECT 1 FROM " + " pg_catalog.pg_class sqc JOIN pg_catalog.pg_namespace sqn " + " ON relnamespace = sqn.oid " + " WHERE " + " nspname = 'pg_catalog' AND relname = 'pg_extension' " + " AND classid = sqc.oid " + " OR " + " nspname = 'sqlj' AND relname = ?" + " AND classid = 'pg_catalog.pg_class'::regclass " + " AND objid = sqc.oid)"); ps.setString(1, loadpath_tbl); rs = ps.executeQuery(); if ( rs.next() && 0 == rs.getInt(1) ) { rs.close(); ps.close(); return SchemaVariant.EMPTY; } rs.close(); ps.close(); return null; } /** * The SchemaVariant that is used and expected by the current code. * Define additional variants as the schema evolves, and keep this field * up to date. */ private static final SchemaVariant currentSchema = SchemaVariant.REL_1_5_0; private enum SchemaVariant { REL_1_5_0 ("c51cffa34acd5a228325143ec29563174891a873") { @Override void migrateFrom( SchemaVariant sv, Connection c, Statement s) throws SQLException { switch ( sv ) { case REL_1_3_0: s.execute( "CREATE TABLE sqlj.jar_descriptor " + "(jarId, ordinal, entryId) AS SELECT " + "CAST(jarId AS INT), CAST(0 AS INT2), " + "deploymentDesc FROM sqlj.jar_repository " + "WHERE deploymentDesc IS NOT NULL"); s.execute( "ALTER TABLE sqlj.jar_repository " + "DROP deploymentDesc"); s.execute( "ALTER TABLE sqlj.jar_descriptor " + "ADD FOREIGN KEY (jarId) " + "REFERENCES sqlj.jar_repository ON DELETE CASCADE, " + "ADD PRIMARY KEY (jarId, ordinal), " + "ALTER COLUMN entryId SET NOT NULL, " + "ADD FOREIGN KEY (entryId) REFERENCES sqlj.jar_entry " + "ON DELETE CASCADE"); s.execute( "GRANT SELECT ON sqlj.jar_descriptor TO PUBLIC"); break; case UNREL20130301a: s.execute( "ALTER TABLE sqlj.jar_descriptors " + "RENAME TO jar_descriptor"); break; default: super.migrateFrom( sv, c, s); } } }, UNREL20130301a ("624d78ca98d80ff2ded215eeca92035da5126bc0"), REL_1_3_0 ("d23804a7e1154de58181a8aa48bfbbb2c8adf68b"), UNREL20060212 ("671eadf7f13a7996af31f1936946bf6677ecdc73"), UNREL20060125 ("8afd33ccb8a2a56e92dee9c9ced81185ff0bb34d"), REL_1_1_0 ("039db412fa91a23b67ceb8d90d30bc540fef7c5d"), REL_1_0_0 ("94e23ba02b55e8008a935fcf3e397db0adb4671b"), UNREL20040121 ("67eea979bcd4575f285c30c581fd0d674c13c1fa"), UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); static final SchemaVariant REL_1_5_0_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA2 = REL_1_5_0_BETA3; static final SchemaVariant REL_1_5_0_BETA1 = REL_1_5_0_BETA2; static final SchemaVariant UNREL20130301b = REL_1_5_0_BETA1; String sha; SchemaVariant( String sha) { this.sha = sha; } void migrateFrom( SchemaVariant sv, Connection c, Statement s) throws SQLException { throw new SQLNonTransientException( "Detected older PL/Java SQLJ schema " + sv.name() + ", from which no automatic migration is implemented", "55000"); } } }