/* * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.sql.jdbc.dialect; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.nuxeo.ecm.core.storage.sql.Activator; import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger; /** * A SQL statement and some optional tags that condition execution. */ public class SQLStatement { // for derby... public static final String DIALECT_WITH_NO_SEMICOLON = "noSemicolon"; /** Category pseudo-tag */ public static final String CATEGORY = "#CATEGORY:"; /** * Tags that may condition execution of the statement. */ public static class Tag { /** * Tag for a SELECT statement whose number of rows must be counted. Var "emptyResult" is set accordingly. */ public static final String TAG_TEST = "#TEST:"; /** * Tag to only execute statement if a var is true. Var may be preceded by ! inverse the test. */ public static final String TAG_IF = "#IF:"; /** * Tag to define a stored procedure / function / type / trigger. Followed by its name. Use by * {@link Dialect#checkStoredProcedure}. */ public static final String TAG_PROC = "#PROC:"; /** * Tag to set a var to true if the result if the statement is empty. */ public static final String TAG_SET_IF_EMPTY = "#SET_IF_EMPTY:"; /** * Tag to set a var to true if the result if the statement is not empty. */ public static final String TAG_SET_IF_NOT_EMPTY = "#SET_IF_NOT_EMPTY:"; public static final String VAR_EMPTY_RESULT = "emptyResult"; /** The tag key. */ public final String key; /** * The value behind a tag, used for {@link #TAG_IF}, {@link #TAG_PROC}, {@link #TAG_SET_IF_EMPTY}, * {@link #TAG_SET_IF_NOT_EMPTY} */ public final String value; public Tag(String key, String value) { this.key = key; this.value = value; } } /** * Collects a list of strings. * * @since 6.0-HF24, 7.10-HF01, 8.1 */ public static class ListCollector { private final List<String> list = new ArrayList<>(); /** Collects one string. */ public void add(String string) { list.add(string); } /** Collects several strings. */ public void addAll(List<String> strings) { list.addAll(strings); } /** Gets the collected strings. */ public List<String> getStrings() { return list; } } /** SQL statement */ public final String sql; /** Tags on the statement */ public final List<Tag> tags; public SQLStatement(String sql, List<Tag> tags) { this.sql = sql; this.tags = tags == null ? Collections.<Tag> emptyList() : tags; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("SQLStatement("); for (Tag tag : tags) { buf.append(tag.key); String value = tag.value; if (value != null) { buf.append(' '); buf.append(value); } buf.append(", "); } buf.append(sql); buf.append(')'); return buf.toString(); } /** * Reads SQL statements from a text file. * <p> * Statements have a category, and optional tags (that may condition execution). * * <pre> * #CATEGORY: mycat * #TEST: * SELECT foo * from bar; * </pre> * * <pre> * #CATEGORY: mycat * #IF: emptyResult * #IF: somethingEnabled * INSERT INTO ...; * </pre> * * An empty line terminates a statement. */ public static Map<String, List<SQLStatement>> read(String filename, Map<String, List<SQLStatement>> statements) throws IOException { return read(filename, statements, false); } public static Map<String, List<SQLStatement>> read(String filename, Map<String, List<SQLStatement>> statements, boolean allDDL) throws IOException { InputStream is = Activator.getResourceAsStream(filename); if (is == null) { throw new IOException("Cannot open: " + filename); } BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; String category = null; List<Tag> tags = new LinkedList<>(); try { while ((line = reader.readLine()) != null) { int colonPos = line.indexOf(':'); String key = colonPos < 0 ? "" : line.substring(0, colonPos + 1); String value = colonPos < 0 ? "" : line.substring(colonPos + 1).trim(); switch (key) { case SQLStatement.CATEGORY: category = value; continue; case Tag.TAG_TEST: case Tag.TAG_IF: case Tag.TAG_PROC: case Tag.TAG_SET_IF_EMPTY: case Tag.TAG_SET_IF_NOT_EMPTY: if (value.length() == 0) { value = null; } tags.add(new Tag(key, value)); continue; } if (line.startsWith("#")) { continue; } StringBuilder buf = new StringBuilder(); boolean read = false; while (true) { if (read) { line = reader.readLine(); } else { read = true; } if (line == null || line.trim().equals("")) { if (buf.length() == 0) { break; } String sql = buf.toString().trim(); SQLStatement statement = new SQLStatement(sql, tags); List<SQLStatement> catStatements = statements.get(category); if (catStatements == null) { statements.put(category, catStatements = new LinkedList<SQLStatement>()); } catStatements.add(statement); break; } else if (line.startsWith("#")) { continue; } else { buf.append(line); buf.append('\n'); } } tags = new LinkedList<>(); if (line == null) { break; } } } finally { reader.close(); } return statements; } protected static String replaceVars(String sql, Map<String, Serializable> properties) { if (properties != null) { for (Entry<String, Serializable> en : properties.entrySet()) { String key = "${" + en.getKey() + "}"; String value = String.valueOf(en.getValue()); sql = sql.replaceAll(Pattern.quote(key), Matcher.quoteReplacement(value)); } } return sql; } /** * Executes a list of SQL statements, following the tags. */ public static void execute(List<SQLStatement> statements, String ddlMode, Map<String, Serializable> properties, Dialect dialect, Connection connection, JDBCLogger logger, ListCollector ddlCollector) throws SQLException { try (Statement st = connection.createStatement()) { STATEMENT: // for (SQLStatement statement : statements) { boolean test = false; String proc = null; Set<String> setIfEmpty = new HashSet<>(); Set<String> setIfNotEmpty = new HashSet<>(); for (Tag tag : statement.tags) { switch (tag.key) { case Tag.TAG_TEST: test = true; break; case Tag.TAG_PROC: proc = tag.value; break; case Tag.TAG_IF: String expr = tag.value; boolean res = false; for (String key : expr.split(" OR: ")) { boolean neg = key.startsWith("!"); if (neg) { key = key.substring(1).trim(); } Serializable value = properties.get(key); if (value == null) { logger.log("Defaulting to false: " + key); value = Boolean.FALSE; } if (!(value instanceof Boolean)) { logger.error("Not a boolean condition: " + key); continue STATEMENT; } if (((Boolean) value).booleanValue() != neg) { res = true; break; } } if (!res) { continue STATEMENT; } break; case Tag.TAG_SET_IF_EMPTY: setIfEmpty.add(tag.value); break; case Tag.TAG_SET_IF_NOT_EMPTY: setIfNotEmpty.add(tag.value); break; } } String sql = statement.sql; sql = replaceVars(sql, properties); if (sql.startsWith("LOG.DEBUG")) { String msg = sql.substring("LOG.DEBUG".length()).trim(); logger.log(msg); continue; } else if (sql.startsWith("LOG.INFO")) { String msg = sql.substring("LOG.INFO".length()).trim(); logger.info(msg); continue; } else if (sql.startsWith("LOG.ERROR")) { String msg = sql.substring("LOG.ERROR".length()).trim(); logger.error(msg); continue; } else if (sql.startsWith("LOG.FATAL")) { String msg = sql.substring("LOG.FATAL".length()).trim(); logger.error(msg); throw new SQLException("Fatal error: " + msg); } if (sql.endsWith(";") && properties.containsKey(DIALECT_WITH_NO_SEMICOLON)) { // derby at least doesn't allow a terminating semicolon sql = sql.substring(0, sql.length() - 1); } try { if (test) { logger.log(sql.replace("\n", "\n ")); // indented try (ResultSet rs = st.executeQuery(sql)) { boolean empty = !rs.next(); properties.put(Tag.VAR_EMPTY_RESULT, Boolean.valueOf(empty)); logger.log(" -> emptyResult = " + empty); if (empty) { for (String prop : setIfEmpty) { properties.put(prop, Boolean.TRUE); logger.log(" -> " + prop + " = true"); } } else { for (String prop : setIfNotEmpty) { properties.put(prop, Boolean.TRUE); logger.log(" -> " + prop + " = true"); } } } } else if (proc != null) { ddlCollector.addAll( dialect.checkStoredProcedure(proc, sql, ddlMode, connection, logger, properties)); } else if (ddlCollector != null) { ddlCollector.add(sql); } else { // upgrade stuff, execute immediately logger.log(sql.replace("\n", "\n ")); // indented st.execute(sql); } } catch (SQLException e) { throw new SQLException("Error executing: " + sql + " : " + e.getMessage(), e); } } } } }