/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2015 ForgeRock AS. All rights reserved.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openidm.repo.jdbc.impl;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.forgerock.guava.common.base.Function;
import org.forgerock.guava.common.collect.FluentIterable;
import org.forgerock.openidm.config.enhanced.InternalErrorException;
import org.forgerock.openidm.repo.util.Clause;
import org.forgerock.openidm.repo.util.SQLRenderer;
/**
* An {@link org.forgerock.openidm.repo.util.SQLRenderer} that models an SQL SELECT statement and
* renders table joins as part of the where clause. Abstract so database implementations may subclass
* for final SQL assembly, most notably with paging and offset query idioms.
* <p>
* For instance, a simple MySQL expression-builder might employ:
* <pre>
* SQLBuilder builder = new SQLBuilder() {
* public String toSQL() {
* return "SELECT " + getColumns().toSQL()
* + getFromClause().toSQL()
* + getJoinClause().toSQL()
* + getWhereClause().toSQL()
* + getOrderByClause().toSQL()
* + " LIMIT " + pageSizeParam
* + " OFFSET " + offsetParam;
* }
* }
* </pre>
*/
abstract class SQLBuilder implements SQLRenderer<String> {
/**
* Renders a select column.
*/
private class Column implements SQLRenderer<String> {
final String column;
Column(String column) {
this.column = column;
}
public String toSQL() {
return column;
}
}
/**
* Renders a table with an option alias.
*/
private class Table implements SQLRenderer<String> {
final String table;
final String alias;
Table(String table, String alias) {
this.table = table;
this.alias = alias;
}
public String toSQL() {
return table + (alias != null ? " " + alias : "");
}
}
/**
* Models the type of table join.
*/
private enum JoinType implements SQLRenderer<String> {
INNER {
@Override
public String toSQL() { return "INNER JOIN"; }
},
LEFT_OUTER {
@Override
public String toSQL() { return "LEFT OUTER JOIN"; }
},
RIGHT_OUTER {
@Override
public String toSQL() { return "RIGHT OUTER JOIN"; }
}
}
/**
* Models/renders a table join.
*/
class Join implements SQLRenderer<String> {
final JoinType type;
final Table table;
final Clause onClause;
final SQLBuilder builder;
/**
* Construct the first pass of a join with the type, table, and alias.
*
* @param builder the calling SQLBuilder
* @param type the type of join
* @param table the table name
* @param alias the table alias
*/
Join(SQLBuilder builder, JoinType type, String table, String alias) {
this.builder = builder;
this.type = type;
this.table = new Table(table, alias);
this.onClause = null;
}
/**
* Construct the second pass of a join with the type, table clause, and on clause.
*
* @param builder the calling SQLBuilder
* @param type the type of join
* @param table the table object
* @param clause the on clause
*/
Join(SQLBuilder builder, JoinType type, Table table, Clause clause) {
this.builder = builder;
this.type = type;
this.table = table;
this.onClause = clause;
}
/**
* Complete this join on the on-clause.
*
* @param clause the on clause
* @return the calling SQLBuilder with this completed join added
*/
SQLBuilder on(Clause clause) {
builder.addJoin(new Join(builder, type, table, clause));
return builder;
}
@Override
public String toSQL() {
return new StringBuilder(type.toSQL())
.append(" ")
.append(table.toSQL())
.append(" ON ")
.append(onClause.toSQL())
.toString();
}
}
/**
* Renders an ORDER BY clause.
*/
private class OrderBy implements SQLRenderer<String> {
final String order;
final boolean ascending;
OrderBy(String order, boolean ascending) {
this.order = order;
this.ascending = ascending;
}
public String toSQL() {
return order + " " + (ascending ? "ASC" : "DESC");
}
}
private final List<SQLRenderer<String>> columns = new ArrayList<SQLRenderer<String>>();
private final List<SQLRenderer<String>> tables = new ArrayList<SQLRenderer<String>>();
private final List<SQLRenderer<String>> joins = new ArrayList<SQLRenderer<String>>();
// the where clause is not final because it is not set at build time
private SQLRenderer<String> whereClause = null;
private final List<SQLRenderer<String>> orderBys = new ArrayList<SQLRenderer<String>>();
/**
* Add a column.
*
* @param column the column to add to the select list
* @return the builder
*/
SQLBuilder addColumn(String column) {
columns.add(new Column(column));
return this;
}
/**
* Select from a particular table.
*
* @param table a table to select from
* @return the builder
*/
SQLBuilder from(String table) {
return from(table, null);
}
/**
* Select from a particular aliased table.
*
* @param table the table to select from
* @param alias the table alias
* @return the builder
*/
SQLBuilder from(String table, String alias) {
tables.add(new Table(table, alias));
return this;
}
/**
* Create a left [outer] table join.
*
* @param table the table to join
* @return the Join
*/
Join leftJoin(String table) {
return leftJoin(table, null);
}
/**
* Create an aliased left [outer] table join.
*
* @param table the table to join
* @param alias the table's alias
* @return the Join
*/
Join leftJoin(String table, String alias) {
return join(JoinType.LEFT_OUTER, table, alias);
}
/**
* Create a right [outer] table join.
*
* @param table the table to join
* @return the Join
*/
Join rightJoin(String table) {
return rightJoin(table, null);
}
/**
* Create an aliased right [outer] table join.
*
* @param table the table to join
* @param alias the table's alias
* @return the Join
*/
Join rightJoin(String table, String alias) {
return join(JoinType.RIGHT_OUTER, table, alias);
}
/**
* Create an inner table join.
*
* @param table the table to join
* @return the Join
*/
Join join(String table) {
return join(table, null);
}
/**
* Create an aliased table join.
*
* @param table the table to join
* @param alias the table's alias
* @return the Join
*/
Join join(String table, String alias) {
return join(JoinType.INNER, table, alias);
}
/**
* Create a join given the type, table, and alias.
*
* @param type the join type
* @param table the table to join
* @param alias the alias for the table to join
* @return
*/
private Join join(JoinType type, String table, String alias) {
return new Join(this, type, table, alias);
}
private SQLBuilder addJoin(Join join) {
joins.add(join);
return this;
}
/**
* Set the where clause.
*
* @param whereClause the WhereClause
* @return the builder
*/
SQLBuilder where(Clause whereClause) {
this.whereClause = whereClause;
return this;
}
/**
* Add an order-by clause.
*
* @param orderBy the order-by clause
* @param ascending whether it is ascending
* @return the builder
*/
SQLBuilder orderBy(String orderBy, boolean ascending) {
this.orderBys.add(new OrderBy(orderBy, ascending));
return this;
}
/** Function to render the SQL from a renderer. */
private static final Function<SQLRenderer<String>, String> TO_SQL =
new Function<SQLRenderer<String>, String>() {
@Override
public String apply(SQLRenderer<String> renderer) {
return renderer.toSQL();
}
};
/**
* Produce a Renderer to render the column list.
*
* @return a renderer for the column list
*/
SQLRenderer<String> getColumns() {
return new SQLRenderer<String>() {
@Override
public String toSQL() {
return columns.isEmpty()
? "*"
: StringUtils.join(FluentIterable.from(columns).transform(TO_SQL), ", ");
}
};
}
/**
* Produce a Renderer to render the from clause.
*
* @return a renderer for the from clause
*/
SQLRenderer<String> getFromClause() {
if (tables.isEmpty()) {
throw new InternalErrorException("SQL query contains no tables in FROM clause");
}
return new SQLRenderer<String>() {
@Override
public String toSQL() {
return " FROM " + StringUtils.join(FluentIterable.from(tables).transform(TO_SQL), ", ");
}
};
}
private static final SQLRenderer<String> NO_STRING =
new SQLRenderer<String>() {
@Override
public String toSQL() {
return "";
}
};
SQLRenderer<String> getJoinClause() {
if (joins.isEmpty()) {
return NO_STRING;
}
return new SQLRenderer<String>() {
@Override
public String toSQL() {
return " " + StringUtils.join(FluentIterable.from(joins).transform(TO_SQL), " ");
}
};
}
/**
* Produce a Renderer to render the where clause.
*
* @return a renderer for the where clause
*/
SQLRenderer<String> getWhereClause() {
return new SQLRenderer<String>() {
@Override
public String toSQL() {
return " WHERE " + whereClause.toSQL();
}
};
}
/**
* Produce a Renderer to render the order-by clause.
*
* @return a renderer for the order-by clause
*/
SQLRenderer<String> getOrderByClause() {
if (orderBys.isEmpty()) {
return NO_STRING;
}
return new SQLRenderer<String>() {
@Override
public String toSQL() {
return " ORDER BY " + StringUtils.join(FluentIterable.from(orderBys).transform(TO_SQL), ", ");
}
};
}
/**
* {@inheritDoc}
*/
@Override
public abstract String toSQL();
/**
* Return a string representation of this builder.
*
* @return the SQL string created by this builder
*/
@Override
public String toString() {
return toSQL();
}
}