001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.script; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.LineNumberReader; 021import java.io.StringReader; 022import java.sql.Connection; 023import java.sql.DatabaseMetaData; 024import java.sql.ResultSet; 025import java.sql.SQLException; 026import java.sql.Statement; 027import java.util.Map; 028 029import org.apache.commons.io.IOUtils; 030import org.apache.excalibur.source.Source; 031import org.apache.excalibur.source.SourceResolver; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import org.ametys.core.datasource.ConnectionHelper; 036 037/** 038 * Example of simple use: 039 * SQLScriptHelper.createTableIfNotExists(dataSourceId, "QRTZ_JOB_DETAILS", "plugin:core://scripts/%s/quartz.sql", _sourceResolver); 040 * Will test if table QRTZ_JOB_DETAILS exits in database from datasource dataSourceId. If not, the script plugin:core://scripts/%s/quartz.sql will be resolved and executed (where %s is replaced by the database type 'mysql', 'derby'...) 041 * 042 * Tools to run SQL scripts.<p> 043 * Default separator for isolating statements is the semi colon 044 * character: <code>;</code>.<br> 045 * It can be changed by using a comment like the following snippet 046 * for using the string <code>---</code>:<br> 047 * <code>-- _separator_=---<br> 048 * begin<br> 049 * execute immediate 'DROP TABLE MYTABLE';<br> 050 * Exception when others then null;<br> 051 * end;<br> 052 * ---<br> 053 * -- _separator_=;<br> 054 * CREATE TABLE MYTABLE;<br> 055 * ...</code><br> 056 * Note that the command must be placed at the end of the comment.<br><br> 057 * The runner can be configured to ignore SQLExceptions. This can be useful 058 * to execute DROP statements when it's unknown if the tables exist:<br> 059 * <code>--_ignore_exceptions_=on<br> 060 * DROP TABLE MYTABLE;<br> 061 * --_ignore_exceptions_=off</code> 062 */ 063public final class SQLScriptHelper 064{ 065 /** Default separator used for isolating statements. */ 066 public static final String DEFAULT_SEPARATOR = ";"; 067 /** Command to ignore sql exceptions. */ 068 public static final String IGNORE_EXCEPTIONS_COMMAND = "_ignore_exceptions_="; 069 /** Command to change the separator. */ 070 public static final String CHANGE_SEPARATOR_COMMAND = "_separator_="; 071 /** Logger available to subclasses. */ 072 protected static final Logger __LOGGER = LoggerFactory.getLogger(SQLScriptHelper.class); 073 074 private SQLScriptHelper() 075 { 076 // Nothing to do 077 } 078 079 /** 080 * This method will test if a table exists, and if not will execute a script to create it 081 * @param datasourceId The data source id to open a connection to the database 082 * @param tableNameToCheck The name of the table that will be checked 083 * @param location The source location where to find the script to execute to create the table. This string will be format with String.format with the dbType as argument. 084 * @param sourceResolver The source resolver 085 * @return true if the table was created, false otherwise 086 * @throws SQLException If an error occurred while executing SQL script, or while testing table existence 087 * @throws IOException If an error occurred while getting the script file, or if the url is malformed 088 */ 089 public static boolean createTableIfNotExists(String datasourceId, String tableNameToCheck, String location, SourceResolver sourceResolver) throws SQLException, IOException 090 { 091 return createTableIfNotExists(datasourceId, tableNameToCheck, location, sourceResolver, null); 092 } 093 094 /** 095 * This method will test if a table exists, and if not will execute a script to create it 096 * @param datasourceId The data source id to open a connection to the database 097 * @param tableNameToCheck The name of the table that will be checked 098 * @param location The source location where to find the script to execute to create the table. This string will be format with String.format with the dbType as argument. 099 * @param sourceResolver The source resolver 100 * @param replace The map of string to replace. Key is the regexp to seek, value is the replacing string. 101 * @return true if the table was created, false otherwise 102 * @throws SQLException If an error occurred while executing SQL script, or while testing table existence 103 * @throws IOException If an error occurred while getting the script file, or if the url is malformed 104 */ 105 public static boolean createTableIfNotExists(String datasourceId, String tableNameToCheck, String location, SourceResolver sourceResolver, Map<String, String> replace) throws SQLException, IOException 106 { 107 Connection connection = null; 108 try 109 { 110 connection = ConnectionHelper.getConnection(datasourceId); 111 112 return createTableIfNotExists(connection, tableNameToCheck, location, sourceResolver, replace); 113 } 114 finally 115 { 116 ConnectionHelper.cleanup(connection); 117 } 118 } 119 120 /** 121 * This method will test if a table exists, and if not will execute a script to create it 122 * @param connection The database connection to use 123 * @param tableNameToCheck The name of the table that will be checked 124 * @param location The source location where to find the script to execute to create the table. This string will be format with String.format with the dbType as argument. 125 * @param sourceResolver The source resolver 126 * @return true if the table was created, false otherwise 127 * @throws SQLException If an error occurred while executing SQL script, or while testing table existence 128 * @throws IOException If an error occurred while getting the script file, or if the url is malformed 129 */ 130 public static boolean createTableIfNotExists(Connection connection, String tableNameToCheck, String location, SourceResolver sourceResolver) throws SQLException, IOException 131 { 132 return createTableIfNotExists(connection, tableNameToCheck, location, sourceResolver, null); 133 } 134 135 /** 136 * This method will test if a table exists, and if not will execute a script to create it 137 * @param connection The database connection to use 138 * @param tableNameToCheck The name of the table that will be checked 139 * @param location The source location where to find the script to execute to create the table. This string will be format with String.format with the dbType as argument. 140 * @param sourceResolver The source resolver 141 * @param replace The map of string to replace. Key is the regexp to seek, value is the replacing string. 142 * @return true if the table was created, false otherwise 143 * @throws SQLException If an error occurred while executing SQL script, or while testing table existence 144 * @throws IOException If an error occurred while getting the script file, or if the url is malformed 145 */ 146 public static boolean createTableIfNotExists(Connection connection, String tableNameToCheck, String location, SourceResolver sourceResolver, Map<String, String> replace) throws SQLException, IOException 147 { 148 if (tableExists(connection, tableNameToCheck)) 149 { 150 return false; 151 } 152 153 String finalLocation = String.format(location, ConnectionHelper.getDatabaseType(connection)); 154 155 Source source = null; 156 try 157 { 158 source = sourceResolver.resolveURI(finalLocation); 159 160 try (InputStream is = source.getInputStream()) 161 { 162 String script = IOUtils.toString(is, "UTF-8"); 163 164 if (replace != null) 165 { 166 for (String replaceKey : replace.keySet()) 167 { 168 script = script.replaceAll(replaceKey, replace.get(replaceKey)); 169 } 170 } 171 172 SQLScriptHelper.runScript(connection, script); 173 } 174 } 175 finally 176 { 177 sourceResolver.release(source); 178 } 179 180 return true; 181 } 182 183 /** 184 * Checks whether the given table exists in the database. 185 * @param connection The database connection 186 * @param tableName the name of the table 187 * @return true is the table exists 188 * @throws SQLException In an SQL exception occurs 189 */ 190 public static boolean tableExists(Connection connection, String tableName) throws SQLException 191 { 192 ResultSet rs = null; 193 boolean schemaExists = false; 194 195 String name = tableName; 196 DatabaseMetaData metaData = connection.getMetaData(); 197 198 if (metaData.storesLowerCaseIdentifiers()) 199 { 200 name = tableName.toLowerCase(); 201 } 202 else if (metaData.storesUpperCaseIdentifiers()) 203 { 204 name = tableName.toUpperCase(); 205 } 206 207 try 208 { 209 rs = metaData.getTables(null, null, name, null); 210 schemaExists = rs.next(); 211 } 212 finally 213 { 214 ConnectionHelper.cleanup(rs); 215 } 216 217 return schemaExists; 218 } 219 220 /** 221 * Run a SQL script using the connection passed in. 222 * @param connection the connection to use for the script 223 * @param script the script data. 224 * @throws IOException if an error occurs while reading the script. 225 * @throws SQLException if an error occurs while executing the script. 226 */ 227 public static void runScript(Connection connection, String script) throws IOException, SQLException 228 { 229 ScriptContext scriptContext = new ScriptContext(); 230 StringBuilder command = new StringBuilder(); 231 232 try 233 { 234 LineNumberReader lineReader = new LineNumberReader(new StringReader(script)); 235 String line = null; 236 while ((line = lineReader.readLine()) != null) 237 { 238 if (__LOGGER.isDebugEnabled()) 239 { 240 __LOGGER.debug(String.format("Reading line: '%s'", line)); 241 } 242 243 boolean processCommand = false; 244 String trimmedLine = line.trim(); 245 246 if (trimmedLine.length() > 0) 247 { 248 processCommand = processScriptLine(trimmedLine, command, scriptContext); 249 250 if (processCommand) 251 { 252 _processCommand(connection, command, lineReader.getLineNumber(), scriptContext); 253 } 254 } 255 } 256 257 // If the entire file was processed and the command buffer is not empty, execute the current buffer. 258 if (command.length() > 0) 259 { 260 _processCommand(connection, command, lineReader.getLineNumber(), scriptContext); 261 } 262 263 if (!connection.getAutoCommit()) 264 { 265 connection.commit(); 266 } 267 } 268 finally 269 { 270 if (!connection.getAutoCommit()) 271 { 272 try 273 { 274 // Fermer la connexion à la base 275 connection.rollback(); 276 } 277 catch (SQLException s) 278 { 279 __LOGGER.error("Error while rollbacking connection", s); 280 } 281 } 282 } 283 } 284 285 /** 286 * Run a SQL script using the connection passed in. 287 * @param connection the connection to use for the script 288 * @param is the input stream containing the script data. 289 * @throws IOException if an error occurs while reading the script. 290 * @throws SQLException if an error occurs while executing the script. 291 */ 292 @SuppressWarnings("deprecation") 293 public static void runScript(Connection connection, InputStream is) throws IOException, SQLException 294 { 295 try 296 { 297 String script = IOUtils.toString(is, "UTF-8"); 298 runScript(connection, script); 299 } 300 finally 301 { 302 IOUtils.closeQuietly(is); 303 } 304 } 305 306 /** 307 * Process a script line. 308 * @param line the line to process. 309 * @param commandBuffer the command buffer. 310 * @param scriptContext the script execution context. 311 * @return true to immediately process the command (a separator was found), false to process it later. 312 */ 313 protected static boolean processScriptLine(String line, StringBuilder commandBuffer, ScriptContext scriptContext) 314 { 315 boolean processCommand = false; 316 317 if (line.startsWith("//") || line.startsWith("--")) 318 { 319 String currentSeparator = scriptContext.getSeparator(); 320 321 // Search if the separator needs to be changed 322 if (line.contains(CHANGE_SEPARATOR_COMMAND)) 323 { 324 // New separator 325 String newSeparator = line.substring(line.indexOf(CHANGE_SEPARATOR_COMMAND) 326 + CHANGE_SEPARATOR_COMMAND.length()).trim(); 327 328 scriptContext.setSeparator(newSeparator); 329 330 if (__LOGGER.isDebugEnabled()) 331 { 332 __LOGGER.debug(String.format("Changing separator to: '%s'", newSeparator)); 333 } 334 } 335 else if (line.contains(IGNORE_EXCEPTIONS_COMMAND)) 336 { 337 String ignoreStr = line.substring(line.indexOf(IGNORE_EXCEPTIONS_COMMAND) 338 + IGNORE_EXCEPTIONS_COMMAND.length()).trim(); 339 340 boolean ignoreExceptions = "on".equals(ignoreStr); 341 342 scriptContext.setIgnoreExceptions(ignoreExceptions); 343 344 if (__LOGGER.isDebugEnabled()) 345 { 346 __LOGGER.debug(String.format("Ignore exceptions: '%s'", ignoreExceptions ? "on" : "off")); 347 } 348 } 349 350 if (line.contains(currentSeparator)) 351 { 352 if (commandBuffer.length() > 0) 353 { 354 // End of command but do not use current line 355 processCommand = true; 356 } 357 } 358 } 359 else if (line.endsWith(scriptContext.getSeparator())) 360 { 361 // End of command and use current line 362 processCommand = true; 363 commandBuffer.append(line.substring(0, line.lastIndexOf(scriptContext.getSeparator()))); 364 } 365 else 366 { 367 // Append current command to the buffer 368 commandBuffer.append(line); 369 commandBuffer.append(" "); 370 } 371 372 return processCommand; 373 } 374 375 private static void _processCommand(Connection connection, StringBuilder command, int lineNumber, ScriptContext scriptContext) throws SQLException 376 { 377 if (__LOGGER.isInfoEnabled()) 378 { 379 __LOGGER.info(String.format("Executing SQL command: '%s'", command)); 380 } 381 382 _execute(connection, command.toString(), lineNumber, scriptContext); 383 384 // Clear command 385 command.setLength(0); 386 } 387 388 private static void _execute(Connection connection, String command, int lineNumber, ScriptContext scriptContext) throws SQLException 389 { 390 Statement statement = null; 391 try 392 { 393 statement = connection.createStatement(); 394 statement.execute(command); 395 } 396 catch (SQLException e) 397 { 398 if (!scriptContext.ignoreExceptions()) 399 { 400 String message = String.format("Unable to execute SQL: '%s' at line %d", command, lineNumber); 401 __LOGGER.error(message, e); 402 403 throw new SQLException(message, e); 404 } 405 } 406 finally 407 { 408 ConnectionHelper.cleanup(statement); 409 } 410 } 411 412 /** 413 * Script execution context. 414 */ 415 protected static class ScriptContext 416 { 417 418 /** The current script execution block separator. */ 419 protected String _separator; 420 421 /** True to ignore sql exceptions. */ 422 protected boolean _ignoreExceptions; 423 424 /** 425 * Default ScriptContext object. 426 */ 427 public ScriptContext() 428 { 429 this(DEFAULT_SEPARATOR, false); 430 } 431 432 /** 433 * Build a ScriptContext object. 434 * @param separator the separator 435 * @param ignoreExceptions true to ignore exceptions. 436 */ 437 public ScriptContext(String separator, boolean ignoreExceptions) 438 { 439 this._separator = separator; 440 this._ignoreExceptions = ignoreExceptions; 441 } 442 443 /** 444 * Get the separator. 445 * @return the separator 446 */ 447 public String getSeparator() 448 { 449 return _separator; 450 } 451 452 /** 453 * Set the separator. 454 * @param separator the separator to set 455 */ 456 public void setSeparator(String separator) 457 { 458 this._separator = separator; 459 } 460 461 /** 462 * Get the ignoreExceptions. 463 * @return the ignoreExceptions 464 */ 465 public boolean ignoreExceptions() 466 { 467 return _ignoreExceptions; 468 } 469 470 /** 471 * Set the ignoreExceptions. 472 * @param ignoreExceptions the ignoreExceptions to set 473 */ 474 public void setIgnoreExceptions(boolean ignoreExceptions) 475 { 476 this._ignoreExceptions = ignoreExceptions; 477 } 478 479 } 480 481}