content: /**
   * 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;
  		}
  		
  	}
  
  }
  