/******************************************************************************* * Copyright 2014 Miami-Dade County * * 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 org.sharegov.cirm.rdb; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; public class Sql { private static final String AND = ") \nAND ("; private static final String AND_ARRAY = " \nAND "; private static final String OR = ") \nOR ("; private static final String OR_ARRAY = " \nOR "; private static final Set<String> EMPTY_TABLE = new HashSet<String>(){ private static final long serialVersionUID = 75492893884463041L;{this.add("");}}; public static Sql SELECT() { Sql SELECT = new Select(); return SELECT; } public static Sql SELECT_DISTINCT() { Sql SELECT = new Select(); SELECT.distinct = true; return SELECT; } public static Sql INSERT_INTO(String table) { Sql INSERT = new Insert(); INSERT.tables.push(Collections.singletonMap(table, table)); return INSERT; } public static Sql DELETE_FROM(String table) { Sql DELETE = new Delete(); DELETE.tables.push(Collections.singletonMap(table, table)); return DELETE; } public static Sql UPDATE(String table) { Sql UPDATE = new Update(); UPDATE.tables.push(Collections.singletonMap(table, table)); return UPDATE; } static class Select extends Sql { public Sql COLUMN(String column) { if (aliases != columns) aliases = columns; aliasable = column; columns.push(Collections.singletonMap(column, column)); return this; } public Sql CLEAR_COLUMNS() { columns.clear(); return this; } public Sql AS(String alias) { if (!aliases.isEmpty()) { aliases.pop(); aliases.push(Collections.singletonMap(aliasable, alias)); if (aliases == columns && !hasColumnAliases) hasColumnAliases = true; if (aliases == tables && !hasTableAliases) hasTableAliases = true; } return this; } public Sql FROM(String table) { if (aliases != tables) aliases = tables; aliasable = table; tables.push(Collections.singletonMap(table, table)); return this; } public Sql JOIN(String join) { if (join != null) { this.join.add(join); on = this.join; } return this; } public Sql INNER_JOIN(String join) { if (join != null) { this.innerJoin.add(join); on = this.innerJoin; } return this; } public Sql LEFT_OUTER_JOIN(String join) { if (join != null) { this.leftOuterJoin.add(join); on = this.leftOuterJoin; } return this; } public Sql RIGHT_OUTER_JOIN(String join) { if (join != null) { this.rightOuterJoin.add(join); on = this.rightOuterJoin; } return this; } public Sql OUTER_JOIN(String join) { if (join != null) { this.outerJoin.add(join); on = this.outerJoin; } return this; } public Sql ON(String columnLeft, String columnRight) { if (on != null && !on.isEmpty()) { String s = on.get(on.size() - 1); on.set(on.size() - 1, s.concat(" ON ").concat(columnLeft).concat(" = ").concat(columnRight)); } return this; } public Sql ORDER_BY(String column) { if (column != null) this.orderBy.add(column); return this; } public Sql ORDER_DIRECTION(String direction) { if(direction != null) this.orderDirection = direction; return this; } public Sql PAGINATION(String column, String value) { if (column != null) this.pagination.put(column, value); return this; } public Sql CLEAR_PAGINATION() { this.pagination.clear(); return this; } public Sql WHERE(String where) { super.WHERE(where); this.in = this.where; return this; } private Sql IN(boolean notIn, String... values) { if (in != null && !in.isEmpty()) { StringBuilder s = new StringBuilder(in.get(in.size() - 1)); if(notIn) s.append(" NOT "); s.append(" IN ( "); for(int i = 0; i < values.length; i++) { s.append(values[i]); if(!(i == values.length-1)) s.append(","); } s.append(" ) "); in.set(in.size() - 1, s.toString()); } return this; } private Sql IN(boolean notIn, Sql subquery) { if (subquery instanceof Select && in != null && !in.isEmpty()) { StringBuilder s = new StringBuilder(in.get(in.size() - 1)); if(notIn) s.append(" NOT "); s.append(" IN ( ") .append(subquery.SQL()) .append(" ) "); in.set(in.size() - 1, s.toString()); } return this; } public Sql IN(String... values) { return IN(false, values); } public Sql IN(Sql subquery) { return IN(false, subquery); } public Sql NOT_IN(String... values) { return IN(true, values); } public Sql NOT_IN(Sql subquery) { return IN(true, subquery); } public String SQL() { StringBuilder builder = new StringBuilder(); if (distinct) sqlClause(builder, "SELECT DISTINCT", columns, "", "", ", ", null, hasColumnAliases); else sqlClause(builder, "SELECT", columns, "", "", ", ", null, hasColumnAliases); sqlClause(builder, "FROM", tables, "", "", ", ", null, hasTableAliases); sqlClause(builder, "JOIN", join, "", "", "\nJOIN ", null, false); sqlClause(builder, "INNER JOIN", innerJoin, "", "", "\nINNER JOIN ", null, false); sqlClause(builder, "OUTER JOIN", outerJoin, "", "", "\nOUTER JOIN ", null, false); sqlClause(builder, "LEFT OUTER JOIN", leftOuterJoin, "", "", "\nLEFT OUTER JOIN ", null, false); sqlClause(builder, "RIGHT OUTER JOIN", rightOuterJoin, "", "", "\nRIGHT OUTER JOIN ", null, false); sqlClause(builder, "WHERE", where, "(", ")", " AND ", null, false); sqlClause(builder, "GROUP BY", groupBy, "", "", ", ", null, false); sqlClause(builder, "HAVING", having, "(", ")", " AND ", null, false); sqlClause(builder, "ORDER BY", orderBy, "", "", ", ", null, false); if(!pagination.isEmpty()) { if (dbhook != null) return dbhook.paginate(builder.toString(), Long.parseLong(pagination.get("minValue").toString()), Long.parseLong(pagination.get("maxValue").toString())); else return SELECT() .COLUMN("*") .FROM("(" + SELECT() .COLUMN("a.*") .COLUMN("rownum rnum") .FROM("("+builder.toString()+") a") .WHERE("rownum") .LESS_THAN_OR_EQUAL(pagination.get("maxValue")).SQL() +")") .WHERE("rnum") .GREATER_THAN_OR_EQUAL(pagination.get("minValue")).SQL(); } else return builder.toString(); } } static class Insert extends Sql { private boolean mergeInsert = false; public boolean isMergeInsert() { return mergeInsert; } public void setMergeInsert(boolean mergeInsert) { this.mergeInsert = mergeInsert; } public Sql VALUES(String columns, String values) { if(columns != null) { this.columns.push(Collections.singletonMap(columns,columns)); this.values.add(values); } return this; } public Sql WHERE(String where) { return this; } public String SQL() { StringBuilder builder = new StringBuilder(); sqlClause(builder, (!mergeInsert)?"INSERT INTO":"INSERT", (!mergeInsert)?tables:EMPTY_TABLE, "", "", "", null, false); sqlClause(builder, "", columns, "(", ")", ", ", null, false); sqlClause(builder, "VALUES", values, "(", ")", ", ", null, false); return builder.toString(); } } static class Update extends Sql { private boolean mergeUpdate = false; public boolean isMergeUpdate() { return mergeUpdate; } public void setMergeUpdate(boolean mergeUpdate) { this.mergeUpdate = mergeUpdate; } public Sql SET(String columns, String values) { if(columns != null) { this.sets.add(columns + " = " + values); } return this; } public String SQL() { StringBuilder builder = new StringBuilder(); sqlClause(builder, "UPDATE", (!mergeUpdate)?tables:EMPTY_TABLE, "", "", "", null, false); sqlClause(builder, "SET", sets, "", "", ", ", null, false); sqlClause(builder, "WHERE", where, "(", ")", " AND ", null, false); return builder.toString(); } } static class Delete extends Sql { public Sql WHERE(String where) { super.WHERE(where); this.in = this.where; return this; } private Sql IN(boolean notIn, String... values) { if (in != null && !in.isEmpty()) { StringBuilder s = new StringBuilder(in.get(in.size() - 1)); if(notIn) s.append(" NOT "); s.append(" IN ( "); for(int i = 0; i < values.length; i++) { s.append(values[i]); if(!(i == values.length-1)) s.append(","); } s.append(" ) "); in.set(in.size() - 1, s.toString()); } return this; } private Sql IN(boolean notIn, Sql subquery) { if (subquery instanceof Select && in != null && !in.isEmpty()) { StringBuilder s = new StringBuilder(in.get(in.size() - 1)); if(notIn) s.append(" NOT "); s.append(" IN ( ") .append(subquery.SQL()) .append(" ) "); in.set(in.size() - 1, s.toString()); } return this; } public Sql IN(String... values) { return IN(false, values); } public Sql IN(Sql subquery) { return IN(false, subquery); } public Sql NOT_IN(String... values) { return IN(true, values); } public Sql NOT_IN(Sql subquery) { return IN(true, subquery); } public String SQL() { StringBuilder builder = new StringBuilder(); sqlClause(builder, "DELETE FROM", tables, "", "", "", null, false); sqlClause(builder, "WHERE", where, "(", ")", " AND ", null, false); return builder.toString(); } } static class Merge extends Sql { private String table; private String alias; private Select select; private String selectTable; private String selectAlias; private Update update; private Insert insert; private Map<String,String> on; private String columnLeft; private String columnRight; public Merge() { } public Merge(String table) { this.table = table; } public Merge(String table, String alias) { this(table); this.alias = alias; } public Sql ON(String columnLeft, String columnRight) { if(this.on == null) this.on = new LinkedHashMap<String, String>(); this.on.put(columnLeft, columnRight); this.columnLeft = columnLeft; this.columnRight = columnRight; return this; } public Sql USING(Sql select, String alias) { if(select instanceof Select) { this.select = (Select) select; this.selectAlias = alias; } return this; } public Sql USING(String table, String alias) { this.selectTable = table; this.selectAlias = alias; return null; } public Sql WHEN_MATCHED_THEN(Sql update) { if(update instanceof Update) { this.update = (Update)update; this.update.setMergeUpdate(true); } return this; } public Sql WHEN_NOT_MATCHED_THEN(Sql insert) { if(insert instanceof Insert) { this.insert = (Insert)insert; this.insert.setMergeInsert(true); } return this; } public String SQL() { StringBuilder builder = new StringBuilder(); builder.append("MERGE INTO \n"); builder.append(table); if(alias != null) builder.append(" ").append(alias).append(" "); builder.append(" USING "); if(select != null) builder.append("\n(").append(select.SQL()).append(")\n"); else if(selectTable != null) builder.append(selectTable); if(selectAlias != null) builder.append(" ").append(selectAlias).append(" "); builder.append(" ON ( "); for(Map.Entry<String, String> entry: on.entrySet() ) builder.append(entry.getKey()).append(" = ").append(entry.getValue()).append(" AND "); builder.delete(builder.lastIndexOf(" AND "), builder.length()); builder.append(" ) "); builder.append("\n WHEN MATCHED THEN \n"); if(update != null) builder.append(update.SQL()); builder.append("\n WHEN NOT MATCHED THEN \n"); if(insert != null) builder.append(insert.SQL()); return builder.toString(); } } public Sql COLUMN(String column) { return this; } public Sql CLEAR_COLUMNS() { return this; } public Sql FROM(String table) { return this; } public Sql AS(String alias) { return this; } public Sql JOIN(String table) { return this; } public Sql INNER_JOIN(String join) { return this; } public Sql LEFT_OUTER_JOIN(String join) { return this; } public Sql RIGHT_OUTER_JOIN(String join) { return this; } public Sql OUTER_JOIN(String join) { return this; } public Sql ON(String columnLeft, String columnRight) { return this; } public Sql ORDER_BY(String column) { return this; } public Sql ORDER_DIRECTION(String direction) { return this; } public Sql PAGINATION(String column, String value) { return this; } public Sql CLEAR_PAGINATION() { return this; } public Sql WHERE(String where) { this.where.add(where); conjunctionList = this.where; operationsList = this.where; return this; } public Sql EQUALS(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" = ").concat(right)); } return this; } public Sql NOT_LIKE(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" NOT LIKE ").concat(right)); } return this; } public Sql LIKE(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" LIKE ").concat(right)); } return this; } public Sql LESS_THAN(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" < ").concat(right)); } return this; } public Sql GREATER_THAN(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" > ").concat(right)); } return this; } public Sql LESS_THAN_OR_EQUAL(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" <= ").concat(right)); } return this; } public Sql GREATER_THAN_OR_EQUAL(String right) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" >= ").concat(right)); } return this; } public Sql BETWEEN(String lower, String upper) { if (operationsList != null && !operationsList.isEmpty()) { String s = operationsList.get(operationsList.size() - 1); operationsList.set(operationsList.size() - 1, s.concat(" BETWEEN ").concat(lower).concat(" AND ").concat( upper)); } return this; } public Sql AND() { if (!conjunctionList.isEmpty()) conjunctionList.add(AND); return this; } public Sql AND_ARRAY() { if (!conjunctionList.isEmpty()) conjunctionList.add(AND_ARRAY); return this; } public Sql OR() { if (!conjunctionList.isEmpty()) conjunctionList.add(OR); return this; } public Sql OR_ARRAY() { if (!conjunctionList.isEmpty()) conjunctionList.add(OR_ARRAY); return this; } public Sql IN(String... values) { return this; } public Sql IN(Sql subquery) { return this; } public Sql NOT_IN(String... values) { return this; } public Sql NOT_IN(Sql subquery) { return this; } public Sql VALUES(String column, String values) { return this; } public Sql SET(String column, String values) { return this; } public String SQL() { return ""; } public static Sql MERGE_INTO(String table) { Sql MERGE = new Merge(table); return MERGE; } public static Sql MERGE_INTO(String table, String alias) { Sql MERGE = new Merge(table, alias); return MERGE; } public Sql USING(String table, String alias) { return this; } public Sql USING(Sql select, String alias) { return this; } public Sql WHEN_MATCHED_THEN(Sql update) { return this; } public Sql WHEN_NOT_MATCHED_THEN(Sql insert) { return this; } public List<String> COLUMNS() { List<String> result = new ArrayList<String>(columns.size()); for(Map<String,String> columns : this.columns) { result.add(columns.keySet().iterator().next()); } return result; } public Sql DB(DatabaseHook dbhook) { this.dbhook = dbhook; return this; } boolean distinct; Stack<Map<String,String>> tables = new Stack<Map<String,String>>(); Stack<Map<String,String>> columns = new Stack<Map<String,String>>(); Map<String, String> pagination = new LinkedHashMap<String, String>(); List<String> sets = new ArrayList<String>(); List<String> join = new ArrayList<String>(); List<String> innerJoin = new ArrayList<String>(); List<String> outerJoin = new ArrayList<String>(); List<String> leftOuterJoin = new ArrayList<String>(); List<String> rightOuterJoin = new ArrayList<String>(); List<String> where = new ArrayList<String>(); List<String> having = new ArrayList<String>(); List<String> groupBy = new ArrayList<String>(); List<String> orderBy = new ArrayList<String>(); String orderDirection = new String(); List<String> lastList = new ArrayList<String>(); List<String> values = new ArrayList<String>(); Stack<Map<String,String>> aliases = columns; List<String> conjunctionList = where; List<String> on; List<String> operationsList; List<String> in; String aliasable; boolean hasColumnAliases; boolean hasTableAliases; DatabaseHook dbhook = null; protected void sqlClause(StringBuilder builder, String keyword, Collection<?> parts, String open, String close, String conjunction, Map<String, String> aliases, boolean useAlias) { if (parts!= null && !parts.isEmpty()) { if (builder.length() > 0) builder.append("\n"); builder.append(keyword); builder.append(" "); builder.append(open); String last = "________"; String alias = null; int i = 0; for (Object part : parts) { String p = null; if(part instanceof String) p = (String) part; else if(part instanceof Map<?, ?>) { Map.Entry<?, ?> entry = ((Map<?,?>)part).entrySet().iterator().next(); p = (String)entry.getKey(); alias = (String)entry.getValue(); } if (i > 0 && !p.equals(AND) && !p.equals(OR) && !last.equals(AND) && !last.equals(OR) && !p.equals(OR_ARRAY) && !last.equals(OR_ARRAY) && !p.equals(AND_ARRAY) && !last.equals(AND_ARRAY)) { builder.append(conjunction); } builder.append(p); if(keyword.equals("ORDER BY") && !orderDirection.trim().isEmpty()) builder.append(" ").append(orderDirection); if (alias != null && useAlias) builder.append(" AS ").append(alias); last = (String)p; i++; } builder.append(close); } } }