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 @SuppressWarnings("deprecation") 296 public static void runScript(Connection connection, InputStream is) throws IOException, SQLException 297 { 298 try 299 { 300 String script = IOUtils.toString(is, "UTF-8"); 301 runScript(connection, script); 302 } 303 finally 304 { 305 IOUtils.closeQuietly(is); 306 } 307 } 308 309 /** 310 * Process a script line. 311 * @param line the line to process. 312 * @param commandBuffer the command buffer. 313 * @param scriptContext the script execution context. 314 * @return true to immediately process the command (a separator was found), false to process it later. 315 */ 316 protected static boolean processScriptLine(String line, StringBuilder commandBuffer, ScriptContext scriptContext) 317 { 318 boolean processCommand = false; 319 320 if (line.startsWith("//") || line.startsWith("--")) 321 { 322 String currentSeparator = scriptContext.getSeparator(); 323 324 // Search if the separator needs to be changed 325 if (line.contains(CHANGE_SEPARATOR_COMMAND)) 326 { 327 // New separator 328 String newSeparator = line.substring(line.indexOf(CHANGE_SEPARATOR_COMMAND) 329 + CHANGE_SEPARATOR_COMMAND.length()).trim(); 330 331 scriptContext.setSeparator(newSeparator); 332 333 if (__LOGGER.isDebugEnabled()) 334 { 335 __LOGGER.debug(String.format("Changing separator to: '%s'", newSeparator)); 336 } 337 } 338 else if (line.contains(IGNORE_EXCEPTIONS_COMMAND)) 339 { 340 String ignoreStr = line.substring(line.indexOf(IGNORE_EXCEPTIONS_COMMAND) 341 + IGNORE_EXCEPTIONS_COMMAND.length()).trim(); 342 343 boolean ignoreExceptions = "on".equals(ignoreStr); 344 345 scriptContext.setIgnoreExceptions(ignoreExceptions); 346 347 if (__LOGGER.isDebugEnabled()) 348 { 349 __LOGGER.debug(String.format("Ignore exceptions: '%s'", ignoreExceptions ? "on" : "off")); 350 } 351 } 352 353 if (line.contains(currentSeparator)) 354 { 355 if (commandBuffer.length() > 0) 356 { 357 // End of command but do not use current line 358 processCommand = true; 359 } 360 } 361 } 362 else if (line.endsWith(scriptContext.getSeparator())) 363 { 364 // End of command and use current line 365 processCommand = true; 366 commandBuffer.append(line.substring(0, line.lastIndexOf(scriptContext.getSeparator()))); 367 } 368 else 369 { 370 // Append current command to the buffer 371 commandBuffer.append(line); 372 commandBuffer.append(" "); 373 } 374 375 return processCommand; 376 } 377 378 private static void _processCommand(Connection connection, StringBuilder command, int lineNumber, ScriptContext scriptContext) throws SQLException 379 { 380 if (__LOGGER.isInfoEnabled()) 381 { 382 __LOGGER.info(String.format("Executing SQL command: '%s'", command)); 383 } 384 385 _execute(connection, command.toString(), lineNumber, scriptContext); 386 387 // Clear command 388 command.setLength(0); 389 } 390 391 private static void _execute(Connection connection, String command, int lineNumber, ScriptContext scriptContext) throws SQLException 392 { 393 Statement statement = null; 394 try 395 { 396 statement = connection.createStatement(); 397 statement.execute(command); 398 } 399 catch (SQLException e) 400 { 401 if (!scriptContext.ignoreExceptions()) 402 { 403 String message = String.format("Unable to execute SQL: '%s' at line %d", command, lineNumber); 404 __LOGGER.error(message, e); 405 406 throw new SQLException(message, e); 407 } 408 } 409 finally 410 { 411 ConnectionHelper.cleanup(statement); 412 } 413 } 414 415 /** 416 * Script execution context. 417 */ 418 protected static class ScriptContext 419 { 420 421 /** The current script execution block separator. */ 422 protected String _separator; 423 424 /** True to ignore sql exceptions. */ 425 protected boolean _ignoreExceptions; 426 427 /** 428 * Default ScriptContext object. 429 */ 430 public ScriptContext() 431 { 432 this(DEFAULT_SEPARATOR, false); 433 } 434 435 /** 436 * Build a ScriptContext object. 437 * @param separator the separator 438 * @param ignoreExceptions true to ignore exceptions. 439 */ 440 public ScriptContext(String separator, boolean ignoreExceptions) 441 { 442 this._separator = separator; 443 this._ignoreExceptions = ignoreExceptions; 444 } 445 446 /** 447 * Get the separator. 448 * @return the separator 449 */ 450 public String getSeparator() 451 { 452 return _separator; 453 } 454 455 /** 456 * Set the separator. 457 * @param separator the separator to set 458 */ 459 public void setSeparator(String separator) 460 { 461 this._separator = separator; 462 } 463 464 /** 465 * Get the ignoreExceptions. 466 * @return the ignoreExceptions 467 */ 468 public boolean ignoreExceptions() 469 { 470 return _ignoreExceptions; 471 } 472 473 /** 474 * Set the ignoreExceptions. 475 * @param ignoreExceptions the ignoreExceptions to set 476 */ 477 public void setIgnoreExceptions(boolean ignoreExceptions) 478 { 479 this._ignoreExceptions = ignoreExceptions; 480 } 481 482 } 483 484}