/**
* Copyright (C) 2012 the original author or authors.
*
* 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.
*/
package co.jirm.core.sql;
import static com.google.common.base.Strings.nullToEmpty;
import java.io.IOException;
import java.io.StringReader;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static co.jirm.core.util.JirmPrecondition.check;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.io.LineReader;
public class SqlPlaceholderParser {
private static final Pattern tokenPattern = Pattern.compile("^(.*?)-- ?\\{(.*?)\\}[ \t]*$");
private static final LoadingCache<CacheKey, ParsedSql> cache = CacheBuilder
.newBuilder()
.maximumSize(100)
.build(new CacheLoader<CacheKey, ParsedSql>() {
@Override
public ParsedSql load(CacheKey key) throws Exception {
return _parseSql(key.sql, key.config);
}
});
public static ParsedSql parseSql(String sql) {
return _parseSqlCache(sql, SqlPlaceholderParserConfig.DEFAULT);
}
private static ParsedSql _parseSqlCache(String sql, SqlPlaceholderParserConfig config) {
try {
return cache.get(new CacheKey(config, sql));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
private static ParsedSql _parseSql(String sql, SqlPlaceholderParserConfig config) throws IOException {
StringBuilder sb = new StringBuilder(sql.length());
LineReader lr = new LineReader(new StringReader(sql));
String line;
ImmutableList.Builder<PlaceHolder> placeHolders = ImmutableList.builder();
int nameIndex = 0;
int positionalIndex = 0;
int position = 0;
boolean first = true;
while ( (line = lr.readLine()) != null) {
if (first) first = false;
else sb.append("\n");
Matcher m = tokenPattern.matcher(line);
if (m.matches()) {
String leftHand = m.group(1);
int start = m.start(1);
check.state(start == 0, "start should be 0");
int[] ind = parseForReplacement(leftHand);
check.state(ind != null, "Problem parsing {}", line);
String before = leftHand.substring(0, ind[0]);
String after = leftHand.substring(ind[1], leftHand.length());
sb.append(before);
sb.append("?");
sb.append(after);
String name = null;
final PlaceHolder ph;
if (m.groupCount() == 2 && ! (name = nullToEmpty(m.group(2))).isEmpty() ) {
ph = new NamePlaceHolder(position, name, nameIndex);
++nameIndex;
}
else {
ph = new PositionPlaceHolder(position, positionalIndex);
++positionalIndex;
}
placeHolders.add(ph);
++position;
}
else {
if (! config.isStripNewLines() )
sb.append(line);
}
}
if (sql.endsWith("\r\n") || sql.endsWith("\n") || sql.endsWith("\r")) {
if (! config.isStripNewLines() )
sb.append("\n");
}
return new ParsedSql(config, sql, sb.toString(), placeHolders.build());
}
protected static int[] parseForReplacement (String leftHand) {
int end = leftHand.length();
while (end > 0 && Character.isWhitespace(leftHand.charAt(end - 1))) {
--end;
}
if (end == 0) return null;
int start = end - 1;
if (leftHand.charAt(start) == '\'') {
//looking for another '
if (start == 0) return null;
int apos = 1;
--start;
while ( start >= 0 ) {
if (leftHand.charAt(start) == '\'') {
apos++;
if (start == 0) {
break;
}
if (leftHand.charAt(start - 1) != '\'' && apos % 2 == 0) {
break;
}
}
--start;
}
if (apos < 2 || apos % 2 != 0) {
return null;
}
}
else {
//looking for space or end.
while ( start > 0 && ! Character.isWhitespace(leftHand.charAt(start - 1))) {
--start;
}
}
return new int [] {start, end};
}
public static class ParsedSql {
private final String resultSql;
private final String originalSql;
private final List<PlaceHolder> placeHolders;
private final SqlPlaceholderParserConfig config;
private ParsedSql(SqlPlaceholderParserConfig config,
String originalSql, String resultSql, ImmutableList<PlaceHolder> placeHolders) {
super();
this.config = config;
this.originalSql = originalSql;
this.resultSql = resultSql;
this.placeHolders = placeHolders;
}
public String getResultSql() {
return resultSql;
}
public String getOriginalSql() {
return originalSql;
}
public List<PlaceHolder> getPlaceHolders() {
return placeHolders;
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("resultSql", resultSql)
.add("originalSql", originalSql)
.add("placeHolders", placeHolders).toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((config == null) ? 0 : config.hashCode());
result = prime * result + ((originalSql == null) ? 0 : originalSql.hashCode());
result = prime * result + ((placeHolders == null) ? 0 : placeHolders.hashCode());
result = prime * result + ((resultSql == null) ? 0 : resultSql.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ParsedSql other = (ParsedSql) obj;
if (config == null) {
if (other.config != null)
return false;
}
else if (!config.equals(other.config))
return false;
if (originalSql == null) {
if (other.originalSql != null)
return false;
}
else if (!originalSql.equals(other.originalSql))
return false;
if (placeHolders == null) {
if (other.placeHolders != null)
return false;
}
else if (!placeHolders.equals(other.placeHolders))
return false;
if (resultSql == null) {
if (other.resultSql != null)
return false;
}
else if (!resultSql.equals(other.resultSql))
return false;
return true;
}
public ImmutableList<Object> mergeParameters(Parameters parameters) {
check.argument(parameters.getNameParameters().isEmpty()
|| parameters.getParameters().isEmpty(),
"Mixing of name and positional parameters not supported yet: {}", parameters);
Optional<PlaceHolderType> allType = allOfType();
if (parameters.getNameParameters().isEmpty()
&& parameters.getParameters().isEmpty()
&& ! this.getPlaceHolders().isEmpty()) {
throw check.argumentInvalid("ParsedSql expected arguments but got none: {}, {}", this, parameters);
}
else if (parameters.getNameParameters().isEmpty()
&& parameters.getParameters().isEmpty()
&& this.getPlaceHolders().isEmpty()) {
return ImmutableList.of();
}
else if (parameters.getNameParameters().isEmpty()
&& ! parameters.getParameters().isEmpty()
&& this.getPlaceHolders().isEmpty()) {
/*
* The sql probably already contains JDBC placeholders.
*/
check.state(this.getOriginalSql().contains("?"),
"Expecting already included JDBC placeholders: {} in sql:", parameters, this);
return parameters.getParameters();
}
else if (this.getPlaceHolders().isEmpty() && ! parameters.getNameParameters().isEmpty()) {
throw check.stateInvalid("Name parameters bound but not defined in sql template " +
" sql: {}, parameters: {}", this, parameters);
}
else if (! allType.isPresent() ){
throw check.stateInvalid("Mixing of name and positional parameters in parsed" +
" sql: {}, parameters: {}", this, parameters);
}
else if ( allType.get() == PlaceHolderType.POSITION && ! parameters.getNameParameters().isEmpty() ) {
throw check.stateInvalid("Expected positional parameters in: {} but was passed name parameters: {}",
this, parameters);
}
else if ( allType.get() == PlaceHolderType.NAME && ! parameters.getParameters().isEmpty() ) {
check.argument(parameters.getParameters().size() >= getPlaceHolders().size(),
"Insufficient positional parameters: {} for ParsedSql name parameters: {}", parameters, this);
return parameters.getParameters();
}
else if ( allType.get() == PlaceHolderType.NAME && ! parameters.getNameParameters().isEmpty() ) {
ImmutableList.Builder<Object> b = ImmutableList.builder();
for (PlaceHolder ph : getPlaceHolders()) {
NamePlaceHolder np = ph.asName();
String name = np.getName();
check.state(parameters.getNameParameters().containsKey(name),
"ParsedSql wants parameter '{}' but name parameters do not have it: {}", name, parameters);
Object o = parameters.getNameParameters().get(name);
b.add(o);
}
return b.build();
}
else if ( allType.get() == PlaceHolderType.POSITION && ! parameters.getParameters().isEmpty() ) {
check.argument(parameters.getParameters().size() >= getPlaceHolders().size(),
"Insufficient positional parameters: {} for ParsedSql parameters: {}", parameters, this);
return parameters.getParameters();
}
else {
throw check.stateInvalid("Programming error parsed: {}, parameters: {}", this, parameters);
}
}
public Optional<PlaceHolderType> allOfType() {
if (placeHolders.isEmpty()) return Optional.absent();
Iterator<PlaceHolder> it = placeHolders.iterator();
PlaceHolderType pt = it.next().getType();
while (it.hasNext()) {
if (pt != it.next().getType())
return Optional.absent();
}
return Optional.of(pt);
}
}
public abstract static class PlaceHolder {
private final int position;
private final PlaceHolderType type;
private PlaceHolder(int position, PlaceHolderType type) {
super();
this.position = position;
this.type = type;
}
public int getPosition() {
return position;
}
public PlaceHolderType getType() {
return type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + position;
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PlaceHolder other = (PlaceHolder) obj;
if (position != other.position)
return false;
if (type != other.type)
return false;
return true;
}
public abstract NamePlaceHolder asName();
public abstract PositionPlaceHolder asPosition();
}
public static class NamePlaceHolder extends PlaceHolder {
private final String name;
private final int namePosition;
private NamePlaceHolder(int position, String name, int namePosition) {
super(position, PlaceHolderType.NAME);
this.name = name;
this.namePosition = namePosition;
}
public String getName() {
return name;
}
public int getNamePosition() {
return namePosition;
}
@Override
public String toString() {
return "NamePlaceHolder{name=" + name + "}";
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + namePosition;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
NamePlaceHolder other = (NamePlaceHolder) obj;
if (name == null) {
if (other.name != null)
return false;
}
else if (!name.equals(other.name))
return false;
if (namePosition != other.namePosition)
return false;
return true;
}
@Override
public NamePlaceHolder asName() {
return this;
}
@Override
public PositionPlaceHolder asPosition() {
throw new UnsupportedOperationException("asPosition is not yet supported");
}
}
public static class PositionPlaceHolder extends PlaceHolder {
private final int parameterPosition;
private PositionPlaceHolder(int position, int parameterPosition) {
super(position, PlaceHolderType.POSITION);
this.parameterPosition = parameterPosition;
}
public int getParameterPosition() {
return parameterPosition;
}
@Override
public String toString() {
return "PositionPlaceHolder{parameterPosition=" + parameterPosition + "}";
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + parameterPosition;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
PositionPlaceHolder other = (PositionPlaceHolder) obj;
if (parameterPosition != other.parameterPosition)
return false;
return true;
}
@Override
public NamePlaceHolder asName() {
throw new UnsupportedOperationException("asName is not yet supported");
}
@Override
public PositionPlaceHolder asPosition() {
return this;
}
}
public enum PlaceHolderType {
NAME, POSITION;
}
private static class CacheKey {
private final String sql;
private final SqlPlaceholderParserConfig config;
private CacheKey(SqlPlaceholderParserConfig config, String sql) {
super();
this.config = config;
this.sql = sql;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((config == null) ? 0 : config.hashCode());
result = prime * result + ((sql == null) ? 0 : sql.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CacheKey other = (CacheKey) obj;
if (config == null) {
if (other.config != null)
return false;
}
else if (!config.equals(other.config))
return false;
if (sql == null) {
if (other.sql != null)
return false;
}
else if (!sql.equals(other.sql))
return false;
return true;
}
}
}