/* ESXX - The friendly ECMAscript/XML Application Server Copyright (C) 2007-2015 Martin Blom <martin@blom.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.esxx.dbref; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; public class QueryBuilder { public static void main(String[] args) throws Exception { for (String a: args) { System.out.println("Processing dbref " + a); QueryBuilder qb = new QueryBuilder(new URI("#" + a)); List<String> result = new ArrayList<String>(); Map<String, String> params = new HashMap<String, String>(); try { System.out.println(qb.getSelectQuery(result, params)); } catch (Exception ex) { System.out.println(ex); } try { System.out.println(qb.getInsertQuery(java.util.Arrays.asList(new String[] { "c1", "c2", "c3" }), params)); } catch (Exception ex) { System.out.println(ex); } try { System.out.println(qb.getUpdateQuery(java.util.Arrays.asList(new String[] { "c1", "c2", "c3" }), result, params)); } catch (Exception ex) { System.out.println(ex); } try { System.out.println(qb.getDeleteQuery(result, params)); } catch (Exception ex) { System.out.println(ex); } } } public QueryBuilder(URI uri) throws URISyntaxException { this.uri = uri; dbref = new DBReference(uri.getRawFragment()); String table = dbref.getTable(); if (table == null) { throw new URISyntaxException(uri.toString(), "Table name missing from URI fragment part"); } else if (!strictTableName.matcher(table).matches()) { throw new URISyntaxException(uri.toString(), "'" + table + "' is not a valid table name"); } for (String c : dbref.getColumns()) { ensureValidColumnName(c); } } public DBReference getParsedReference() { return dbref; } public boolean isRequiredParam(String param) { return dbref.getRequiredParams().containsKey(param); } public String findRequiredParam(Map<String, String> params) { for (String key : params.keySet()) { if (isRequiredParam(key)) { return key; } } return null; } public String getSelectQuery(List<String> args, Map<String, String> unhandled_params) throws URISyntaxException { DBReference.Scope scope = dbref.getScope(DBReference.Scope.ALL); args.clear(); unhandled_params.clear(); unhandled_params.putAll(dbref.getOptionalParams()); unhandled_params.putAll(dbref.getRequiredParams()); String order = unhandled_params.remove("order"); String reverse = unhandled_params.remove("reverse"); String offset = unhandled_params.remove("offset"); String count = unhandled_params.remove("count"); if ((scope == DBReference.Scope.SCALAR || scope == DBReference.Scope.COLUMN) && dbref.getColumns().size() != 1) { throw new URISyntaxException(uri.toString(), "Scalar and column scopes only works with one single column"); } QueryBuffer qb = new QueryBuffer(uri.getSchemeSpecificPart()); qb.append("SELECT "); if (scope == DBReference.Scope.DISTINCT) { qb.append("DISTINCT "); } if (dbref.getColumns().isEmpty()) { qb.append("*"); } else { sequence(dbref.getColumns(), true, false, qb); } qb.append(" FROM ").appendTable(dbref.getTable()); if (dbref.getFilter() != null) { qb.append(" WHERE "); where(dbref.getFilter(), qb, args); } orderBy(order, reverse, qb); offsetCount(offset, count, qb); return qb.toString(); } public String getInsertQuery(Iterable<String> columns, Map<String, String> unhandled_params) throws URISyntaxException { DBReference.Scope scope = dbref.getScope(DBReference.Scope.ROW); unhandled_params.clear(); unhandled_params.putAll(dbref.getOptionalParams()); unhandled_params.putAll(dbref.getRequiredParams()); if (dbref.getColumns().isEmpty()) { for (String c : columns) { ensureValidColumnName(c); } } else { columns = dbref.getColumns(); } if (scope != DBReference.Scope.ROW && scope != DBReference.Scope.ALL) { throw new URISyntaxException(uri.toString(), scope.toString().toLowerCase() + " is not a valid scope when inserting"); } if (dbref.getFilter() != null) { throw new URISyntaxException(uri.toString(), "Filters may not be used when inserting"); } QueryBuffer qb = new QueryBuffer(uri.getSchemeSpecificPart()); qb.append("INSERT INTO ").appendTable(dbref.getTable()).append(" ("); sequence(columns, true, false, qb); qb.append(") VALUES ("); sequence(columns, false, true, qb); qb.append(")"); return qb.toString(); } public String getUpdateQuery(Iterable<String> columns, List<String> args, Map<String, String> unhandled_params) throws URISyntaxException { DBReference.Scope scope = dbref.getScope(DBReference.Scope.ROW); args.clear(); unhandled_params.clear(); unhandled_params.putAll(dbref.getOptionalParams()); unhandled_params.putAll(dbref.getRequiredParams()); if (dbref.getColumns().isEmpty()) { for (String c : columns) { ensureValidColumnName(c); } } else { columns = dbref.getColumns(); } if (scope != DBReference.Scope.SCALAR && scope != DBReference.Scope.ROW && scope != DBReference.Scope.ALL) { throw new URISyntaxException(uri.toString(), scope.toString().toLowerCase() + " is not a valid scope when updating"); } if (!columns.iterator().hasNext()) { throw new URISyntaxException(uri.toString(), "No columns to update"); } QueryBuffer qb = new QueryBuffer(uri.getSchemeSpecificPart()); qb.append("UPDATE ").appendTable(dbref.getTable()).append(" SET "); sequence(columns, true, true, qb); if (dbref.getFilter() != null) { qb.append(" WHERE "); where(dbref.getFilter(), qb, args); } return qb.toString(); } public String getDeleteQuery(List<String> args, Map<String, String> unhandled_params) throws URISyntaxException { DBReference.Scope scope = dbref.getScope(DBReference.Scope.ALL); args.clear(); unhandled_params.clear(); unhandled_params.putAll(dbref.getOptionalParams()); unhandled_params.putAll(dbref.getRequiredParams()); if (scope != DBReference.Scope.ALL) { throw new URISyntaxException(uri.toString(), scope.toString().toLowerCase() + " is not a valid scope when deleting"); } if (!dbref.getColumns().isEmpty()) { throw new URISyntaxException(uri.toString(), "Columns may not be specified when deleting"); } QueryBuffer qb = new QueryBuffer(uri.getSchemeSpecificPart()); qb.append("DELETE FROM ").appendTable(dbref.getTable()); if (dbref.getFilter() != null) { qb.append(" WHERE "); where(dbref.getFilter(), qb, args); } return qb.toString(); } public interface ColumnGetter { public Object get(String key); } public String getMergeQuery(Iterable<String> columns, ColumnGetter cg, List<String> args, Map<String, String> unhandled_params) throws URISyntaxException { DBReference.Scope scope = dbref.getScope(DBReference.Scope.ROW); unhandled_params.clear(); unhandled_params.putAll(dbref.getOptionalParams()); unhandled_params.putAll(dbref.getRequiredParams()); String key = unhandled_params.remove("key"); if (key == null) { throw new URISyntaxException(uri.toString(), "Missing 'key' parameter"); } ensureValidColumnName(key); if (dbref.getColumns().isEmpty()) { for (String c : columns) { ensureValidColumnName(c); } } else { columns = dbref.getColumns(); } if (scope != DBReference.Scope.ROW && scope != DBReference.Scope.ALL) { throw new URISyntaxException(uri.toString(), scope.toString().toLowerCase() + " is not a valid scope when merging"); } if (dbref.getFilter() != null) { throw new URISyntaxException(uri.toString(), "Filters may not be used when merging"); } String ssp = uri.getSchemeSpecificPart(); QueryBuffer qb = new QueryBuffer(ssp); if (ssp.startsWith("h2:")) { qb.append("MERGE INTO ").appendTable(dbref.getTable()).append(" ("); sequence(columns, true, false, qb); qb.append(") KEY (").appendColumn(key).append(") VALUES ("); sequence(columns, false, true, qb); qb.append(")"); } else if (ssp.startsWith("mysql:")) { qb.append("INSERT INTO ").appendTable(dbref.getTable()).append(" ("); sequence(columns, true, false, qb); qb.append(") VALUES ("); sequence(columns, false, true, qb); qb.append(") ON DUPLICATE KEY UPDATE "); sequence(columns, true, true, qb); } else { args.add(cg.get(key).toString()); qb.append("MERGE INTO ").appendTable(dbref.getTable()) .append(" USING ").appendTable(dbref.getTable()) .append(" ON ").appendColumn(key).append(" = {0}") .append(" WHEN MATCHED THEN UPDATE SET "); sequence(columns, true, true, qb); qb.append(" WHEN NOT MATCHED THEN INSERT ("); sequence(columns, true, false, qb); qb.append(") VALUES ("); sequence(columns, false, true, qb); qb.append(")"); } return qb.toString(); } private void sequence(Iterable<String> iter, boolean col, boolean ref, QueryBuffer qb) { boolean first = true; for (String s : iter) { if (first) { first = false; } else { qb.append(", "); } if (col) { qb.appendColumn(s); } if (col && ref) { qb.append(" = "); } if (ref) { qb.append("{").append(s).append("}"); } } } private void where(DBReference.Filter filter, QueryBuffer qb, List<String> args) throws URISyntaxException { DBReference.Filter.Op op = filter.getOp(); qb.append("("); switch (op) { case AND: case OR: { boolean first = true; for (DBReference.Filter f : filter.getChildren()) { if (first) { first = false; } else { qb.append(" ").append(op.toString()).append(" "); } where(f, qb, args); } break; } case NOT: if (filter.getChildren().size() != 1) { throw new IllegalStateException("Filter.Op." + op + " must have exactly one child"); } qb.append("NOT "); where(filter.getChildren().get(0), qb, args); break; case LT: case LE: case EQ: case NE: case GT: case GE: { if (filter.getChildren().size() != 2 || filter.getChildren().get(0).getOp() != DBReference.Filter.Op.VAL || filter.getChildren().get(1).getOp() != DBReference.Filter.Op.VAL) { throw new IllegalStateException("Filter.Op." + op + " must have exactly two VAL children"); } String column = filter.getChildren().get(0).getValue(); ensureValidColumnName(column); qb.appendColumn(column); switch (op) { case LT: qb.append(" < "); break; case LE: qb.append(" <= "); break; case EQ: qb.append(" = "); break; case NE: qb.append(" != "); break; case GT: qb.append(" > "); break; case GE: qb.append(" >= "); break; default: throw new IllegalStateException("This can't happen"); } qb.append("{").append(args.size()).append("}"); if (filter.getChildren().get(1).getOp() != DBReference.Filter.Op.VAL) { throw new IllegalStateException("Filter.Op." + op + "'s second child must be VAL"); } args.add(filter.getChildren().get(1).getValue()); break; } case VAL: throw new IllegalStateException("Filter.Op." + op + " should have been handled already"); } qb.append(")"); } private void orderBy(String order, String reverse, QueryBuffer qb) throws URISyntaxException { if (order != null) { ensureValidColumnName(order); qb.append(" ORDER BY ").appendColumn(order); } if ("".equals(reverse) || Boolean.parseBoolean(reverse)) { qb.append(" DESC"); } } private void offsetCount(String offset, String count, QueryBuffer qb) { boolean use_offset_limit = useLimitOffset.matcher(uri.getSchemeSpecificPart()).matches(); if (use_offset_limit) { if (count != null) { qb.append(" LIMIT ").append(Integer.parseInt(count)); } if (offset != null) { qb.append(" OFFSET ").append(Integer.parseInt(offset)); } } else { if (offset != null) { qb.append(" OFFSET ").append(Integer.parseInt(offset)).append(" ROWS"); } if (count != null) { qb.append(" FETCH FIRST ").append(Integer.parseInt(count)).append(" ROWS ONLY"); } } } private void ensureValidColumnName(String name) throws URISyntaxException { if (!strictColumnName.matcher(name).matches()) { throw new URISyntaxException(uri.toString(), "'" + name + "' is not a valid column name"); } } private static class QueryBuffer { public QueryBuffer(String ssp) { sb = new StringBuffer(); sq = '`'; eq = '`'; } public QueryBuffer append(char c) { sb.append(c); return this; } public QueryBuffer append(int i) { sb.append(i); return this; } public QueryBuffer append(String s) { sb.append(s); return this; } public QueryBuffer appendColumn(String s) { sb.append(sq).append(s).append(eq); return this; } public QueryBuffer appendTable(String s) { sb.append(sq).append(s).append(eq); return this; } @Override public String toString() { return sb.toString(); } private StringBuffer sb; private char sq, eq; } private URI uri; private DBReference dbref; private static Pattern useLimitOffset = Pattern.compile("(h2|mysql|postgresql):.*"); private static Pattern strictColumnName = Pattern.compile("[_A-Za-z][-_A-Za-z0-9]*"); private static Pattern strictTableName = Pattern.compile("[_A-Za-z][-_A-Za-z0-9]*" + "(\\.[_A-Za-z][-_A-Za-z0-9]*)*"); }