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 if (source != null) 178 { 179 sourceResolver.release(source); 180 } 181 } 182 183 return true; 184 } 185 186 /** 187 * Checks whether the given table exists in the database. 188 * @param connection The database connection 189 * @param tableName the name of the table 190 * @return true is the table exists 191 * @throws SQLException In an SQL exception occurs 192 */ 193 public static boolean tableExists(Connection connection, String tableName) throws SQLException 194 { 195 ResultSet rs = null; 196 boolean schemaExists = false; 197 198 String name = tableName; 199 DatabaseMetaData metaData = connection.getMetaData(); 200 201 if (metaData.storesLowerCaseIdentifiers()) 202 { 203 name = tableName.toLowerCase(); 204 } 205 else if (metaData.storesUpperCaseIdentifiers()) 206 { 207 name = tableName.toUpperCase(); 208 } 209 210 try 211 { 212 rs = metaData.getTables(null, null, name, null); 213 schemaExists = rs.next(); 214 } 215 finally 216 { 217 ConnectionHelper.cleanup(rs); 218 } 219 220 return schemaExists; 221 } 222 223 /** 224 * Run a SQL script using the connection passed in. 225 * @param connection the connection to use for the script 226 * @param script the script data. 227 * @throws IOException if an error occurs while reading the script. 228 * @throws SQLException if an error occurs while executing the script. 229 */ 230 public static void runScript(Connection connection, String script) throws IOException, SQLException 231 { 232 ScriptContext scriptContext = new ScriptContext(); 233 StringBuilder command = new StringBuilder(); 234 235 try 236 { 237 LineNumberReader lineReader = new LineNumberReader(new StringReader(script)); 238 String line = null; 239 while ((line = lineReader.readLine()) != null) 240 { 241 if (__LOGGER.isDebugEnabled()) 242 { 243 __LOGGER.debug(String.format("Reading line: '%s'", line)); 244 } 245 246 boolean processCommand = false; 247 String trimmedLine = line.trim(); 248 249 if (trimmedLine.length() > 0) 250 { 251 processCommand = processScriptLine(trimmedLine, command, scriptContext); 252 253 if (processCommand) 254 { 255 _processCommand(connection, command, lineReader.getLineNumber(), scriptContext); 256 } 257 } 258 } 259 260 // If the entire file was processed and the command buffer is not empty, execute the current buffer. 261 if (command.length() > 0) 262 { 263 _processCommand(connection, command, lineReader.getLineNumber(), scriptContext); 264 } 265 266 if (!connection.getAutoCommit()) 267 { 268 connection.commit(); 269 } 270 } 271 finally 272 { 273 if (!connection.getAutoCommit()) 274 { 275 try 276 { 277 // Fermer la connexion à la base 278 connection.rollback(); 279 } 280 catch (SQLException s) 281 { 282 __LOGGER.error("Error while rollbacking connection", s); 283 } 284 } 285 } 286 } 287 288 /** 289 * Run a SQL script using the connection passed in. 290 * @param connection the connection to use for the script 291 * @param is the input stream containing the script data. 292 * @throws IOException if an error occurs while reading the script. 293 * @throws SQLException if an error occurs while executing the script. 294 */ 295 public static void runScript(Connection connection, InputStream is) throws IOException, SQLException 296 { 297 try 298 { 299 String script = IOUtils.toString(is, "UTF-8"); 300 runScript(connection, script); 301 } 302 finally 303 { 304 IOUtils.closeQuietly(is); 305 } 306 } 307 308 /** 309 * Process a script line. 310 * @param line the line to process. 311 * @param commandBuffer the command buffer. 312 * @param scriptContext the script execution context. 313 * @return true to immediately process the command (a separator was found), false to process it later. 314 */ 315 protected static boolean processScriptLine(String line, StringBuilder commandBuffer, ScriptContext scriptContext) 316 { 317 boolean processCommand = false; 318 319 if (line.startsWith("//") || line.startsWith("--")) 320 { 321 String currentSeparator = scriptContext.getSeparator(); 322 323 // Search if the separator needs to be changed 324 if (line.contains(CHANGE_SEPARATOR_COMMAND)) 325 { 326 // New separator 327 String newSeparator = line.substring(line.indexOf(CHANGE_SEPARATOR_COMMAND) 328 + CHANGE_SEPARATOR_COMMAND.length()).trim(); 329 330 scriptContext.setSeparator(newSeparator); 331 332 if (__LOGGER.isDebugEnabled()) 333 { 334 __LOGGER.debug(String.format("Changing separator to: '%s'", newSeparator)); 335 } 336 } 337 else if (line.contains(IGNORE_EXCEPTIONS_COMMAND)) 338 { 339 String ignoreStr = line.substring(line.indexOf(IGNORE_EXCEPTIONS_COMMAND) 340 + IGNORE_EXCEPTIONS_COMMAND.length()).trim(); 341 342 boolean ignoreExceptions = "on".equals(ignoreStr); 343 344 scriptContext.setIgnoreExceptions(ignoreExceptions); 345 346 if (__LOGGER.isDebugEnabled()) 347 { 348 __LOGGER.debug(String.format("Ignore exceptions: '%s'", ignoreExceptions ? "on" : "off")); 349 } 350 } 351 352 if (line.contains(currentSeparator)) 353 { 354 if (commandBuffer.length() > 0) 355 { 356 // End of command but do not use current line 357 processCommand = true; 358 } 359 } 360 } 361 else if (line.endsWith(scriptContext.getSeparator())) 362 { 363 // End of command and use current line 364 processCommand = true; 365 commandBuffer.append(line.substring(0, line.lastIndexOf(scriptContext.getSeparator()))); 366 } 367 else 368 { 369 // Append current command to the buffer 370 commandBuffer.append(line); 371 commandBuffer.append(" "); 372 } 373 374 return processCommand; 375 } 376 377 private static void _processCommand(Connection connection, StringBuilder command, int lineNumber, ScriptContext scriptContext) throws SQLException 378 { 379 if (__LOGGER.isInfoEnabled()) 380 { 381 __LOGGER.info(String.format("Executing SQL command: '%s'", command)); 382 } 383 384 _execute(connection, command.toString(), lineNumber, scriptContext); 385 386 // Clear command 387 command.setLength(0); 388 } 389 390 private static void _execute(Connection connection, String command, int lineNumber, ScriptContext scriptContext) throws SQLException 391 { 392 Statement statement = null; 393 try 394 { 395 statement = connection.createStatement(); 396 statement.execute(command); 397 } 398 catch (SQLException e) 399 { 400 if (!scriptContext.ignoreExceptions()) 401 { 402 String message = String.format("Unable to execute SQL: '%s' at line %d", command, lineNumber); 403 __LOGGER.error(message, e); 404 405 throw new SQLException(message, e); 406 } 407 } 408 finally 409 { 410 ConnectionHelper.cleanup(statement); 411 } 412 } 413 414 /** 415 * Script execution context. 416 */ 417 protected static class ScriptContext 418 { 419 420 /** The current script execution block separator. */ 421 protected String _separator; 422 423 /** True to ignore sql exceptions. */ 424 protected boolean _ignoreExceptions; 425 426 /** 427 * Default ScriptContext object. 428 */ 429 public ScriptContext() 430 { 431 this(DEFAULT_SEPARATOR, false); 432 } 433 434 /** 435 * Build a ScriptContext object. 436 * @param separator the separator 437 * @param ignoreExceptions true to ignore exceptions. 438 */ 439 public ScriptContext(String separator, boolean ignoreExceptions) 440 { 441 this._separator = separator; 442 this._ignoreExceptions = ignoreExceptions; 443 } 444 445 /** 446 * Get the separator. 447 * @return the separator 448 */ 449 public String getSeparator() 450 { 451 return _separator; 452 } 453 454 /** 455 * Set the separator. 456 * @param separator the separator to set 457 */ 458 public void setSeparator(String separator) 459 { 460 this._separator = separator; 461 } 462 463 /** 464 * Get the ignoreExceptions. 465 * @return the ignoreExceptions 466 */ 467 public boolean ignoreExceptions() 468 { 469 return _ignoreExceptions; 470 } 471 472 /** 473 * Set the ignoreExceptions. 474 * @param ignoreExceptions the ignoreExceptions to set 475 */ 476 public void setIgnoreExceptions(boolean ignoreExceptions) 477 { 478 this._ignoreExceptions = ignoreExceptions; 479 } 480 481 } 482 483}