co.jirm.core.sql.SqlPlaceholderParser.java Source code

Java tutorial

Introduction

Here is the source code for co.jirm.core.sql.SqlPlaceholderParser.java

Source

/**
 * 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 if (!config.isStripNewLines())
                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 {
                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;
        }

    }

}