/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.function.EnvFunction;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
/**
* A {@link ConnectionLifecycleListener} that executes custom SQL commands on connection grab and
* release. The SQL commands can contain environment variable references, where the enviroment
* variable reference contains a name and an eventual default value.
*
* Parsing rules are:
* <ul>
* <li>whatever is between <code>${</code> and <code>}</code> is considered a enviroment variable
* reference in the form <code>${name,defaultvalue}, the default value being optional</li>
* <li><code>$</code> and <code>}</code> can be used stand alone only escaped with <code>\</code>
* (e.g. <code>\$</code> and <code>\}</code>)</li>
* <li><code>\</code> can be used stand alone only escaped with another <code>\</code></li> (e.g.
* <code>\\</code>)
* <li>a enviroment variable name cannot contain a comma, which is used as the separator between the
* enviroemnt variable name and its default value (first comma acts as a separator)</li>
* <li>the default value is always interpreted as a string and expanded as such in the sql commands</li>
* </ul>
*
* Examples of valid expressions:
* <ul>
* <li>"one two three \} \$ \\" (simple literal with special chars escaped)</li>
* <li>"My name is ${name}" (a simple enviroment variable reference without a default value)</li>
* <li>"My name is ${name,Joe}" (a simple enviroment variable reference with a default value)</li>
* </ul>
*
* Examples of non valid expressions:
* <ul>
* <li>"bla ${myAttName" (unclosed expression section)</li>
* <li>"bla } bla" (<code>}</code> is reserved, should have been escaped)</li>
*
*
* @author Andrea Aime - GeoSolutions
*/
public class SessionCommandsListener implements ConnectionLifecycleListener {
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
private Expression sqlOnBorrow;
private Expression sqlOnRelease;
public SessionCommandsListener(String sqlOnBorrow, String sqlOnRelease) {
this.sqlOnBorrow = expandEviromentVariables(sqlOnBorrow);
this.sqlOnRelease = expandEviromentVariables(sqlOnRelease);
}
public void onBorrow(JDBCDataStore store, Connection cx) throws SQLException {
if (sqlOnBorrow != null && !"".equals(sqlOnBorrow)) {
String command = sqlOnBorrow.evaluate(null, String.class);
Statement st = null;
try {
st = cx.createStatement();
st.execute(command);
} finally {
store.closeSafe(st);
}
}
}
public void onRelease(JDBCDataStore store, Connection cx) throws SQLException {
if (sqlOnRelease != null) {
String command = sqlOnRelease.evaluate(null, String.class);
Statement st = null;
try {
st = cx.createStatement();
st.execute(command);
} finally {
store.closeSafe(st);
}
}
}
public void onCommit(JDBCDataStore store, Connection cx) {
// nothing to do
}
public void onRollback(JDBCDataStore store, Connection cx) {
// nothing to do
}
/**
* Parses the original sql command and returns a Expression that has all environment variable
* references expanded to a {@link EnvFunction} call.</p>
* This code is partially copied from gt-renderer ExpressionExtractor code, but simplified
* to only have enviroment variable references instead of CQL to avoid creating a dependendcy
* cascading issue (ExpressionExtractor would have to be moved to gt-cql and gt-jdbc made
* to depend on it.
*
* @param sql
* @return
*/
Expression expandEviromentVariables(String sql) {
if(sql == null || "".equals(sql)) {
return null;
}
boolean inEnvVariable = false;
List<Expression> expressions = new ArrayList<Expression>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < sql.length(); i++) {
final char curr = sql.charAt(i);
final boolean last = (i == sql.length() - 1);
final char next = last ? 0 : sql.charAt(i + 1);
if (curr == '\\') {
if (last)
throw new IllegalArgumentException("Unescaped \\ at position " + (i + 1));
if (next == '\\')
sb.append('\\');
else if (next == '$')
sb.append('$');
else if (next == '}')
sb.append('}');
else
throw new IllegalArgumentException("Unescaped \\ at position " + (i + 1));
// skip the next character
i++;
} else if (curr == '$') {
if (last || next != '{')
throw new IllegalArgumentException("Unescaped $ at position " + (i + 1));
if (inEnvVariable)
throw new IllegalArgumentException(
"Already found a ${ sequence before the one at " + (i + 1));
// if we extracted a literal in between two expressions, add it to the result
if (sb.length() > 0) {
expressions.add(ff.literal(sb.toString()));
sb.setLength(0);
}
// mark the beginning and skip the next character
inEnvVariable = true;
i++;
} else if (curr == '}') {
if (!inEnvVariable)
throw new IllegalArgumentException(
"Already found a ${ sequence before the one at " + (i + 1));
if (sb.length() == 0)
throw new IllegalArgumentException(
"Invalid empty enviroment variable reference ${} at " + (i - 1));
String name = sb.toString();
String defaultValue = null;
int idx = name.indexOf(',');
if(idx >= 0) {
if(idx == 0) {
throw new IllegalArgumentException("There is no variable name before " +
"the comma, the valid format is '${name,defaultValue}'");
} else if(idx < name.length() - 1) {
defaultValue = name.substring(idx + 1);
name = name.substring(0, idx);
}
}
Expression env;
if(defaultValue != null) {
env = ff.function("env", ff.literal(name), ff.literal(defaultValue));
} else {
env = ff.function("env", ff.literal(name));
}
expressions.add(env);
sb.setLength(0);
inEnvVariable = false;
} else {
sb.append(curr);
}
}
// when done, if we are still in a environment variable reference it means it hasn't been
// closed
if (inEnvVariable) {
throw new IllegalArgumentException("Unclosed enviroment variable reference '" + sb
+ "'");
} else if (sb.length() > 0) {
expressions.add(ff.literal(sb.toString()));
}
// now concatenate back all the references
if (expressions == null || expressions.size() == 0)
throw new IllegalArgumentException("The SQL command appears to be empty: " + sql);
Expression result = expressions.get(0);
for (int i = 1; i < expressions.size(); i++) {
result = ff.function("strConcat", result, expressions.get(i));
}
return result;
}
}