--- /dev/null
+/*\r
+\r
+ Derby - Class org.apache.derby.impl.sql.execute.JarUtil\r
+\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to you under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+\r
+ */\r
+\r
+package org.apache.derby.impl.sql.execute;\r
+\r
+import java.io.FileInputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.net.MalformedURLException;\r
+import java.net.URL;\r
+import java.security.AccessController;\r
+import java.security.PrivilegedActionException;\r
+\r
+import org.apache.derby.iapi.error.StandardException;\r
+import org.apache.derby.iapi.reference.Property;\r
+import org.apache.derby.iapi.reference.SQLState;\r
+import org.apache.derby.iapi.services.loader.ClassFactory;\r
+import org.apache.derby.iapi.services.property.PropertyUtil;\r
+import org.apache.derby.iapi.sql.conn.LanguageConnectionContext;\r
+import org.apache.derby.iapi.sql.depend.DependencyManager;\r
+import org.apache.derby.iapi.sql.dictionary.DataDescriptorGenerator;\r
+import org.apache.derby.iapi.sql.dictionary.DataDictionary;\r
+import org.apache.derby.iapi.sql.dictionary.FileInfoDescriptor;\r
+import org.apache.derby.iapi.sql.dictionary.SchemaDescriptor;\r
+import org.apache.derby.iapi.store.access.FileResource;\r
+import org.apache.derby.iapi.util.IdUtil;\r
+\r
+\r
+public class JarUtil\r
+{\r
+ //\r
+ //State passed in by the caller\r
+ private LanguageConnectionContext lcc;\r
+ private String schemaName;\r
+ private String sqlName;\r
+\r
+ //Derived state\r
+ \r
+ private FileResource fr;\r
+ private DataDictionary dd;\r
+ private DataDescriptorGenerator ddg;\r
+ \r
+ //\r
+ //State derived from the caller's context\r
+ private JarUtil(LanguageConnectionContext lcc,\r
+ String schemaName, String sqlName)\r
+ throws StandardException\r
+ {\r
+ this.schemaName = schemaName;\r
+ this.sqlName = sqlName;\r
+\r
+ this.lcc = lcc;\r
+ fr = lcc.getTransactionExecute().getFileHandler();\r
+ dd = lcc.getDataDictionary();\r
+ ddg = dd.getDataDescriptorGenerator();\r
+ }\r
+\r
+ /**\r
+ install a jar file to the current connection's database.\r
+\r
+ @param schemaName the name for the schema that holds the jar file.\r
+ @param sqlName the sql name for the jar file.\r
+ @param externalPath the path for the jar file to add.\r
+ @return The generationId for the jar file we add.\r
+\r
+ @exception StandardException Opps\r
+ */\r
+ public static long\r
+ install(LanguageConnectionContext lcc,\r
+ String schemaName, String sqlName, String externalPath)\r
+ throws StandardException\r
+ {\r
+ JarUtil jutil = new JarUtil(lcc, schemaName, sqlName);\r
+ InputStream is = null;\r
+ \r
+ try {\r
+ is = openJarURL(externalPath);\r
+ return jutil.add(is);\r
+ } catch (java.io.IOException fnfe) {\r
+ throw StandardException.newException(SQLState.SQLJ_INVALID_JAR, fnfe, externalPath);\r
+ }\r
+ finally {\r
+ try {if (is != null) is.close();}\r
+ catch (IOException ioe) {}\r
+ }\r
+ }\r
+\r
+ /**\r
+ Add a jar file to the current connection's database.\r
+\r
+ <P> The reason for adding the jar file in this private instance\r
+ method is that it allows us to share set up logic with drop and\r
+ replace.\r
+ @param is A stream for reading the content of the file to add.\r
+ @exception StandardException Opps\r
+ */\r
+ private long add(final InputStream is) throws StandardException\r
+ {\r
+ //\r
+ //Like create table we say we are writing before we read the dd\r
+ dd.startWriting(lcc);\r
+ FileInfoDescriptor fid = getInfo();\r
+ if (fid != null)\r
+ throw\r
+ StandardException.newException(SQLState.LANG_OBJECT_ALREADY_EXISTS_IN_OBJECT, \r
+ fid.getDescriptorType(), sqlName, fid.getSchemaDescriptor().getDescriptorType(), schemaName);\r
+\r
+ SchemaDescriptor sd = dd.getSchemaDescriptor(schemaName, null, true);\r
+ try {\r
+ notifyLoader(false);\r
+ dd.invalidateAllSPSPlans();\r
+ final String jarExternalName = JarUtil.mkExternalName(schemaName,\r
+ sqlName, fr.getSeparatorChar());\r
+\r
+ long generationId = setJar(jarExternalName, is, true, 0L);\r
+\r
+ fid = ddg.newFileInfoDescriptor(/*DJD*/null, sd, sqlName, generationId);\r
+ dd.addDescriptor(fid, sd, DataDictionary.SYSFILES_CATALOG_NUM,\r
+ false, lcc.getTransactionExecute());\r
+ return generationId;\r
+ } finally {\r
+ notifyLoader(true);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Drop a jar file from the current connection's database.\r
+ * \r
+ * @param schemaName\r
+ * the name for the schema that holds the jar file.\r
+ * @param sqlName\r
+ * the sql name for the jar file.\r
+ * \r
+ * @exception StandardException\r
+ * Opps\r
+ */\r
+ public static void\r
+ drop(LanguageConnectionContext lcc, String schemaName, String sqlName)\r
+ throws StandardException\r
+ {\r
+ JarUtil jutil = new JarUtil(lcc, schemaName,sqlName);\r
+ jutil.drop();\r
+ }\r
+\r
+ /**\r
+ Drop a jar file from the current connection's database.\r
+\r
+ <P> The reason for dropping the jar file in this private instance\r
+ method is that it allows us to share set up logic with add and\r
+ replace.\r
+\r
+ @exception StandardException Opps\r
+ */\r
+ private void drop() throws StandardException\r
+ {\r
+ //\r
+ //Like create table we say we are writing before we read the dd\r
+ dd.startWriting(lcc);\r
+ FileInfoDescriptor fid = getInfo();\r
+ if (fid == null)\r
+ throw StandardException.newException(SQLState.LANG_FILE_DOES_NOT_EXIST, sqlName,schemaName);\r
+\r
+ String dbcp_s = PropertyUtil.getServiceProperty(lcc.getTransactionExecute(),Property.DATABASE_CLASSPATH);\r
+ if (dbcp_s != null)\r
+ {\r
+ String[][]dbcp= IdUtil.parseDbClassPath(dbcp_s);\r
+ boolean found = false;\r
+ //\r
+ //Look for the jar we are dropping on our database classpath.\r
+ //We don't concern ourselves with 3 part names since they may\r
+ //refer to a jar file in another database and may not occur in\r
+ //a database classpath that is stored in the propert congomerate.\r
+ for (int ix=0;ix<dbcp.length;ix++)\r
+ if (dbcp.length == 2 &&\r
+ dbcp[ix][0].equals(schemaName) && dbcp[ix][1].equals(sqlName))\r
+ found = true;\r
+ if (found)\r
+ throw StandardException.newException(SQLState.LANG_CANT_DROP_JAR_ON_DB_CLASS_PATH_DURING_EXECUTION, \r
+ IdUtil.mkQualifiedName(schemaName,sqlName),\r
+ dbcp_s);\r
+ }\r
+\r
+ try {\r
+ \r
+ notifyLoader(false);\r
+ dd.invalidateAllSPSPlans();\r
+ DependencyManager dm = dd.getDependencyManager();\r
+ dm.invalidateFor(fid, DependencyManager.DROP_JAR, lcc);\r
+\r
+ dd.dropFileInfoDescriptor(fid);\r
+\r
+ fr.remove(JarUtil.mkExternalName(schemaName, sqlName, fr.getSeparatorChar()),\r
+ fid.getGenerationId());\r
+ } finally {\r
+ notifyLoader(true);\r
+ }\r
+ }\r
+\r
+ /**\r
+ Replace a jar file from the current connection's database with the content of an\r
+ external file. \r
+\r
+\r
+ @param schemaName the name for the schema that holds the jar file.\r
+ @param sqlName the sql name for the jar file.\r
+ @param externalPath the path for the jar file to add.\r
+ @return The new generationId for the jar file we replace.\r
+\r
+ @exception StandardException Opps\r
+ */\r
+ public static long\r
+ replace(LanguageConnectionContext lcc, String schemaName, String sqlName,\r
+ String externalPath)\r
+ throws StandardException\r
+ {\r
+ JarUtil jutil = new JarUtil(lcc, schemaName,sqlName);\r
+ InputStream is = null;\r
+ \r
+\r
+ try {\r
+ is = openJarURL(externalPath);\r
+\r
+ return jutil.replace(is);\r
+ } catch (java.io.IOException fnfe) {\r
+ throw StandardException.newException(SQLState.SQLJ_INVALID_JAR, fnfe, externalPath);\r
+ }\r
+ finally {\r
+ try {if (is != null) is.close();}\r
+ catch (IOException ioe) {}\r
+ }\r
+ }\r
+\r
+ /**\r
+ Replace a jar file in the current connection's database with the\r
+ content of an external file.\r
+\r
+ <P> The reason for adding the jar file in this private instance\r
+ method is that it allows us to share set up logic with add and\r
+ drop.\r
+ @param is An input stream for reading the new content of the jar file.\r
+ @exception StandardException Opps\r
+ */\r
+ private long replace(InputStream is) throws StandardException\r
+ {\r
+ //\r
+ //Like create table we say we are writing before we read the dd\r
+ dd.startWriting(lcc);\r
+\r
+ //\r
+ //Temporarily drop the FileInfoDescriptor from the data dictionary.\r
+ FileInfoDescriptor fid = getInfo();\r
+ if (fid == null)\r
+ throw StandardException.newException(SQLState.LANG_FILE_DOES_NOT_EXIST, sqlName,schemaName);\r
+\r
+ try {\r
+ // disable loads from this jar\r
+ notifyLoader(false);\r
+ dd.invalidateAllSPSPlans();\r
+ dd.dropFileInfoDescriptor(fid);\r
+ final String jarExternalName =\r
+ JarUtil.mkExternalName(schemaName, sqlName, fr.getSeparatorChar());\r
+\r
+ //\r
+ //Replace the file.\r
+ long generationId = setJar(jarExternalName, is, false,\r
+ fid.getGenerationId());\r
+ \r
+ //\r
+ //Re-add the descriptor to the data dictionary.\r
+ FileInfoDescriptor fid2 = \r
+ ddg.newFileInfoDescriptor(fid.getUUID(),fid.getSchemaDescriptor(),\r
+ sqlName,generationId);\r
+ dd.addDescriptor(fid2, fid.getSchemaDescriptor(),\r
+ DataDictionary.SYSFILES_CATALOG_NUM, false, lcc.getTransactionExecute());\r
+ return generationId;\r
+\r
+ } finally {\r
+\r
+ // reenable class loading from this jar\r
+ notifyLoader(true);\r
+ }\r
+ }\r
+\r
+ /**\r
+ Get the FileInfoDescriptor for the Jar file or null if it does not exist.\r
+ @exception StandardException Ooops\r
+ */\r
+ private FileInfoDescriptor getInfo()\r
+ throws StandardException\r
+ {\r
+ SchemaDescriptor sd = dd.getSchemaDescriptor(schemaName, null, true);\r
+ return dd.getFileInfoDescriptor(sd,sqlName);\r
+ }\r
+\r
+ private void notifyLoader(boolean reload) throws StandardException {\r
+ ClassFactory cf = lcc.getLanguageConnectionFactory().getClassFactory();\r
+ cf.notifyModifyJar(reload);\r
+ }\r
+\r
+ /**\r
+ * Open an input stream to read a URL or a file.\r
+ * URL is attempted first, if the string does not conform\r
+ * to a URL then an attempt to open it as a regular file\r
+ * is tried.\r
+ * <BR>\r
+ * Attempting the file first can throw a security execption\r
+ * when a valid URL is passed in.\r
+ * The security exception is due to not have the correct permissions\r
+ * to access the bogus file path. To avoid this the order was reversed\r
+ * to attempt the URL first and only attempt a file open if creating\r
+ * the URL throws a MalformedURLException.\r
+ */\r
+ private static InputStream openJarURL(final String externalPath)\r
+ throws IOException\r
+ {\r
+ try {\r
+ return (InputStream) AccessController.doPrivileged\r
+ (new java.security.PrivilegedExceptionAction(){\r
+ \r
+ public Object run() throws IOException { \r
+ try {\r
+ return new URL(externalPath).openStream();\r
+ } catch (MalformedURLException mfurle)\r
+ {\r
+ return new FileInputStream(externalPath);\r
+ }\r
+ }\r
+ });\r
+ } catch (PrivilegedActionException e) {\r
+ throw (IOException) e.getException();\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * Copy the jar from the externally obtained \r
+ * input stream into the database\r
+ * @param jarExternalName Name of jar with database structure.\r
+ * @param contents Contents of jar file.\r
+ * @param add true to add, false to replace\r
+ * @param currentGenerationId generation id of existing version, ignored when adding.\r
+ */\r
+ private long setJar(final String jarExternalName,\r
+ final InputStream contents,\r
+ final boolean add,\r
+ final long currentGenerationId)\r
+ throws StandardException {\r
+ try {\r
+ return ((Long) AccessController\r
+ .doPrivileged(new java.security.PrivilegedExceptionAction() {\r
+\r
+ public Object run() throws StandardException {\r
+ long generationId;\r
+ \r
+ if (add)\r
+ generationId = fr.add(jarExternalName, contents);\r
+ else\r
+ generationId = fr.replace(jarExternalName,\r
+ currentGenerationId, contents);\r
+ return new Long(generationId);\r
+ }\r
+ })).longValue();\r
+ } catch (PrivilegedActionException e) {\r
+ throw (StandardException) e.getException();\r
+ }\r
+ }\r
+ \r
+ /**\r
+ Make an external name for a jar file stored in the database.\r
+ */\r
+ public static String mkExternalName(String schemaName, String sqlName, char separatorChar)\r
+ {\r
+ StringBuffer sb = new StringBuffer(30);\r
+\r
+ sb.append(FileResource.JAR_DIRECTORY_NAME);\r
+ sb.append(separatorChar);\r
+ sb.append(schemaName);\r
+ sb.append(separatorChar);\r
+ sb.append(sqlName);\r
+ sb.append(".jar");\r
+ return sb.toString();\r
+ }\r
+}\r