/*
 * 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.
 *
 * Other licenses:
 * -----------------------------------------------------------------------------
 * Commercial licenses for this work are available. These replace the above
 * ASL 2.0 and offer limited warranties, support, maintenance, and commercial
 * database integrations.
 *
 * For more information, please visit: http://www.jooq.org/licenses
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package org.jooq.impl;

import static java.lang.Boolean.TRUE;
import static org.jooq.SQLDialect.SQLITE;
import static org.jooq.conf.ParamType.INDEXED;
import static org.jooq.conf.ParamType.INLINED;
import static org.jooq.conf.ParamType.NAMED;
import static org.jooq.conf.SettingsTools.renderLocale;
import static org.jooq.impl.Identifiers.QUOTES;
import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER;
import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER_ESCAPED;
import static org.jooq.impl.Identifiers.QUOTE_START_DELIMITER;
import static org.jooq.impl.Keywords.K_WITH;
import static org.jooq.impl.ScopeMarkers.AFTER_LAST_TOP_LEVEL_CTE;
import static org.jooq.impl.ScopeMarkers.BEFORE_FIRST_TOP_LEVEL_CTE;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_COUNT_BIND_VALUES;
import static org.jooq.impl.Tools.DataKey.DATA_TOP_LEVEL_CTE;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.jooq.BindContext;
import org.jooq.Configuration;
import org.jooq.Constants;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Param;
import org.jooq.QueryPart;
import org.jooq.QueryPartInternal;
import org.jooq.RenderContext;
import org.jooq.SQLDialect;
import org.jooq.Table;
import org.jooq.conf.RenderFormatting;
import org.jooq.conf.RenderKeywordCase;
import org.jooq.conf.RenderNameCase;
import org.jooq.conf.RenderQuotedNames;
import org.jooq.conf.Settings;
import org.jooq.conf.SettingsTools;
import org.jooq.exception.ControlFlowSignal;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.Tools.DataKey;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.StringUtils;

Author:Lukas Eder
/** * @author Lukas Eder */
class DefaultRenderContext extends AbstractContext<RenderContext> implements RenderContext { private static final JooqLogger log = JooqLogger.getLogger(DefaultRenderContext.class); private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]*"); private static final Pattern NEWLINE = Pattern.compile("[\\n\\r]"); private static final Set<String> SQLITE_KEYWORDS; final StringBuilder sql; private final QueryPartList<Param<?>> bindValues; private int params; private int alias; private int indent; private Deque<Integer> indentLock; private boolean separatorRequired; private boolean separator; private boolean newline; private int skipUpdateCounts; // [#1632] Cached values from Settings RenderKeywordCase cachedRenderKeywordCase; RenderNameCase cachedRenderNameCase; RenderQuotedNames cachedRenderQuotedNames; boolean cachedRenderFormatted; // [#6525] Cached values from Settings.renderFormatting String cachedIndentation; int cachedIndentWidth; String cachedNewline; int cachedPrintMargin; DefaultRenderContext(Configuration configuration) { super(configuration, null); Settings settings = configuration.settings(); this.sql = new StringBuilder(); this.bindValues = new QueryPartList<>(); this.cachedRenderKeywordCase = SettingsTools.getRenderKeywordCase(settings); this.cachedRenderFormatted = Boolean.TRUE.equals(settings.isRenderFormatted()); this.cachedRenderNameCase = SettingsTools.getRenderNameCase(settings); this.cachedRenderQuotedNames = SettingsTools.getRenderQuotedNames(settings); RenderFormatting formatting = settings.getRenderFormatting(); if (formatting == null) formatting = new RenderFormatting(); this.cachedNewline = formatting.getNewline() == null ? "\n" : formatting.getNewline(); this.cachedIndentation = formatting.getIndentation() == null ? " " : formatting.getIndentation(); this.cachedIndentWidth = cachedIndentation.length(); this.cachedPrintMargin = formatting.getPrintMargin() == null ? 80 : formatting.getPrintMargin(); } DefaultRenderContext(RenderContext context) { this(context.configuration()); paramType(context.paramType()); qualifyCatalog(context.qualifyCatalog()); qualifySchema(context.qualifySchema()); quote(context.quote()); castMode(context.castMode()); data().putAll(context.data()); declareCTE = context.declareCTE(); declareWindows = context.declareWindows(); declareFields = context.declareFields(); declareTables = context.declareTables(); declareAliases = context.declareAliases(); } // ------------------------------------------------------------------------ // BindContext API // ------------------------------------------------------------------------ @Override public final BindContext bindValue(Object value, Field<?> field) throws DataAccessException { throw new UnsupportedOperationException(); } final QueryPartList<Param<?>> bindValues() { return bindValues; } // ------------------------------------------------------------------------ // RenderContext API // ------------------------------------------------------------------------ @Override void scopeMarkStart0(QueryPart part) { ScopeStackElement e = scopeStack.getOrCreate(part); e.positions = new int[] { sql.length(), -1 }; e.indent = indent; } @Override void scopeMarkEnd0(QueryPart part) { ScopeStackElement e = scopeStack.getOrCreate(part); e.positions[1] = sql.length(); } @Override public RenderContext scopeRegister(QueryPart part, boolean forceNew) { if (scopeStack.inScope()) { if (part instanceof TableImpl) { Table<?> root = (Table<?>) part; Table<?> child = root; List<Table<?>> tables = new ArrayList<>(); while (root instanceof TableImpl && (child = ((TableImpl<?>) root).child) != null) { tables.add(root); root = child; } ScopeStackElement e = forceNew ? scopeStack.create(root) : scopeStack.getOrCreate(root); if (e.joinNode == null) e.joinNode = new JoinNode(root); JoinNode childNode = e.joinNode; for (int i = tables.size() - 1; i >= 0; i--) { Table<?> t = tables.get(i); ForeignKey<?, ?> k = ((TableImpl<?>) t).childPath; JoinNode next = childNode.children.get(k); if (next == null) { next = new JoinNode(t); childNode.children.put(k, next); } childNode = next; } } } return this; } @Override void scopeEnd0() { // TODO: Think about a more appropriate location for this logic, rather // than the generic DefaultRenderContext, which shouldn't know anything // about the individual query parts that it is rendering. TopLevelCte cte = null; ScopeStackElement beforeFirstCte = null; ScopeStackElement afterLastCte = null; if (subqueryLevel() == 0 && (cte = (TopLevelCte) data(DATA_TOP_LEVEL_CTE)) != null && !cte.isEmpty()) { beforeFirstCte = scopeStack.get(BEFORE_FIRST_TOP_LEVEL_CTE); afterLastCte = scopeStack.get(AFTER_LAST_TOP_LEVEL_CTE); } outer: for (ScopeStackElement e1 : scopeStack) { String replaced = null; if (subqueryLevel() != e1.scopeLevel) { continue outer; } else if (e1.positions == null) { continue outer; } else if (e1 == beforeFirstCte) { boolean single = cte != null && cte.size() == 1; RenderContext render = configuration.dsl().renderContext(); // There is no WITH clause if (afterLastCte != null && e1.positions[0] == afterLastCte.positions[0]) render.visit(K_WITH); if (single) render.formatIndentStart() .formatSeparator(); else render.sql(' '); render.declareCTE(true).visit(cte).declareCTE(false); if (single) render.formatIndentEnd(); replaced = render.render(); } else if (e1.joinNode == null) { continue outer; } else if (!e1.joinNode.children.isEmpty()) { replaced = configuration .dsl() .renderContext() .declareTables(true) .sql('(') .formatIndentStart(e1.indent) .formatIndentStart() .formatNewLine() .visit(e1.joinNode.joinTree()) .formatNewLine() .sql(')') .render(); } if (replaced != null) { sql.replace(e1.positions[0], e1.positions[1], replaced); int shift = replaced.length() - (e1.positions[1] - e1.positions[0]); inner: for (ScopeStackElement e2 : scopeStack) { if (e2.positions == null) continue inner; if (e2.positions[0] > e1.positions[0]) { e2.positions[0] = e2.positions[0] + shift; e2.positions[1] = e2.positions[1] + shift; } } } } } final int peekSkipUpdateCounts() { return skipUpdateCounts; } final void incrementSkipUpdateCounts() { skipUpdateCounts++; } @Override public final String peekAlias() { return "alias_" + (alias + 1); } @Override public final String nextAlias() { return "alias_" + (++alias); } @Override public final String render() { String prepend = null; String result = sql.toString(); return prepend == null ? result : prepend + result; } @Override public final String render(QueryPart part) { return new DefaultRenderContext(this).visit(part).render(); } @Override public final RenderContext keyword(String keyword) { return visit(DSL.keyword(keyword)); } @Override public final RenderContext sql(String s) { return sql(s, s == null || !cachedRenderFormatted); } @Override public final RenderContext sql(String s, boolean literal) { if (!literal) s = NEWLINE.matcher(s).replaceAll("$0" + indentation()); if (stringLiteral()) s = StringUtils.replace(s, "'", stringLiteralEscapedApos); applyNewLine(); sql.append(s); resetSeparatorFlags(); return this; } @Override public final RenderContext sql(char c) { applyNewLine(); sql.append(c); if (c == '\'' && stringLiteral()) sql.append(c); resetSeparatorFlags(); return this; } @Override public final RenderContext sql(int i) { applyNewLine(); sql.append(i); resetSeparatorFlags(); return this; } @Override public final RenderContext sql(long l) { applyNewLine(); sql.append(l); resetSeparatorFlags(); return this; } @Override public final RenderContext sql(float f) { applyNewLine(); sql.append(f); resetSeparatorFlags(); return this; } @Override public final RenderContext sql(double d) { applyNewLine(); sql.append(d); resetSeparatorFlags(); return this; } private final void resetSeparatorFlags() { separatorRequired = false; separator = false; newline = false; } @Override public final RenderContext formatNewLine() { if (cachedRenderFormatted) newline = true; return this; } private final void applyNewLine() { if (newline) { sql.append(cachedNewline); sql.append(indentation()); } } @Override public final RenderContext formatNewLineAfterPrintMargin() { if (cachedRenderFormatted && cachedPrintMargin > 0) if (sql.length() - sql.lastIndexOf(cachedNewline) > cachedPrintMargin) formatNewLine(); return this; } private final String indentation() { return StringUtils.leftPad("", indent, cachedIndentation); } @Override public final RenderContext format(boolean format) { cachedRenderFormatted = format; return this; } @Override public final boolean format() { return cachedRenderFormatted; } @Override public final RenderContext formatSeparator() { if (!separator && !newline) { if (cachedRenderFormatted) formatNewLine(); else sql(" ", true); separator = true; } return this; } @Override public final RenderContext separatorRequired(boolean separatorRequired) { this.separatorRequired = separatorRequired; return this; } @Override public final boolean separatorRequired() { return separatorRequired && !separator && !newline; } @Override public final RenderContext formatIndentStart() { return formatIndentStart(cachedIndentWidth); } @Override public final RenderContext formatIndentEnd() { return formatIndentEnd(cachedIndentWidth); } @Override public final RenderContext formatIndentStart(int i) { if (cachedRenderFormatted) indent += i; // // [#9193] If we've already generated the separator (and indentation) // if (newline) // sql.append(cachedIndentation); return this; } @Override public final RenderContext formatIndentEnd(int i) { if (cachedRenderFormatted) indent -= i; return this; } private final Deque<Integer> indentLock() { if (indentLock == null) indentLock = new ArrayDeque<>(); return indentLock; } @Override public final RenderContext formatIndentLockStart() { if (cachedRenderFormatted) { indentLock().push(indent); String[] lines = NEWLINE.split(sql); indent = lines[lines.length - 1].length(); } return this; } @Override public final RenderContext formatIndentLockEnd() { if (cachedRenderFormatted) indent = indentLock().pop(); return this; } @Override public final RenderContext formatPrintMargin(int margin) { cachedPrintMargin = margin; return this; } @Override public final RenderContext literal(String literal) { // Literal usually originates from NamedQueryPart.getName(). This could // be null for CustomTable et al. if (literal == null) return this; SQLDialect family = family(); // Quoting is needed when explicitly requested... boolean needsQuote = // [#2367] ... but in SQLite, quoting "normal" literals is generally // asking for trouble, as SQLite bends the rules here, see // http://www.sqlite.org/lang_keywords.html for details ... (family != SQLITE && quote()) || // [#2367] ... yet, do quote when an identifier is a SQLite keyword (family == SQLITE && SQLITE_KEYWORDS.contains(literal.toUpperCase(renderLocale(configuration().settings())))) || // [#1982] [#3360] ... yet, do quote when an identifier contains special characters (family == SQLITE && !IDENTIFIER_PATTERN.matcher(literal).matches()); if (RenderNameCase.LOWER == cachedRenderNameCase || RenderNameCase.LOWER_IF_UNQUOTED == cachedRenderNameCase && !quote()) literal = literal.toLowerCase(renderLocale(configuration().settings())); else if (RenderNameCase.UPPER == cachedRenderNameCase || RenderNameCase.UPPER_IF_UNQUOTED == cachedRenderNameCase && !quote()) literal = literal.toUpperCase(renderLocale(configuration().settings())); if (needsQuote) { char[][][] quotes = QUOTES.get(family); char start = quotes[QUOTE_START_DELIMITER][0][0]; char end = quotes[QUOTE_END_DELIMITER][0][0]; sql(start); // [#4922] This micro optimisation does seem to have a significant // effect as the replace call can be avoided in almost all // situations if (literal.indexOf(end) > -1) sql(StringUtils.replace(literal, new String(quotes[QUOTE_END_DELIMITER][0]), new String(quotes[QUOTE_END_DELIMITER_ESCAPED][0])), true); else sql(literal, true); sql(end); } else { sql(literal, true); } return this; } @Override @Deprecated public final RenderContext sql(QueryPart part) { return visit(part); } @Override protected final void visit0(QueryPartInternal internal) { int before = bindValues.size(); internal.accept(this); int after = bindValues.size(); // [#4650] In PostgreSQL, UDTConstants are always inlined as ROW(?, ?) // as the PostgreSQL JDBC driver doesn't support SQLData. This // means that the above internal.accept(this) call has already // collected the bind variable. The same is true if custom data // type bindings use Context.visit(Param), in case of which we // must not collect the current Param if (after == before && paramType != INLINED && internal instanceof Param) { Param<?> param = (Param<?>) internal; if (!param.isInline()) { bindValues.add(param); Integer threshold = settings().getInlineThreshold(); if (threshold != null && threshold > 0) { checkForceInline(threshold); } else { switch (family()) { // [#5701] Tests were conducted with PostgreSQL 9.5 and pgjdbc 9.4.1209 case POSTGRES: checkForceInline(32767); break; case SQLITE: checkForceInline(999); break; default: break; } } } } } private final void checkForceInline(int max) throws ForceInlineSignal { if (bindValues.size() > max) if (TRUE.equals(data(DATA_COUNT_BIND_VALUES))) throw new ForceInlineSignal(); } @Override @Deprecated public final boolean inline() { return paramType == INLINED; } @Override @Deprecated public final boolean namedParams() { return paramType == NAMED; } @Override @Deprecated public final RenderContext inline(boolean i) { this.paramType = i ? INLINED : INDEXED; return this; } @Override @Deprecated public final RenderContext namedParams(boolean r) { this.paramType = r ? NAMED : INDEXED; return this; } // ------------------------------------------------------------------------ // Object API // ------------------------------------------------------------------------ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("rendering [").append(render()).append("]\n"); sb.append("formatting [").append(format()).append("]\n"); sb.append("parameters [").append(paramType).append("]\n"); toString(sb); return sb.toString(); } // ------------------------------------------------------------------------ // Static initialisation // ------------------------------------------------------------------------ static { SQLITE_KEYWORDS = new HashSet<>(); // [#2367] Taken from http://www.sqlite.org/lang_keywords.html SQLITE_KEYWORDS.addAll(Arrays.asList( "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LEFT", "LIKE", "LIMIT", "MATCH", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", "REPLACE", "RESTRICT", "RIGHT", "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT" )); /* * So, you've found the piece of logic that displays our beautifully-crafted ASCII-art logo that * we display in the log files to our jOOQ Open Source Edition and jOOQ Free Trial Edition users * * You probably came here to see if you can somehow turn it off, e.g. because you found this * page here: * * http://stackoverflow.com/q/28272284/521799 * * And yes! You can: * * a) turn off your logger for org.jooq.Constants * b) set the -Dorg.jooq.no-logo=true property * c) simply patch this file in your sources and rebuild jOOQ * d) buy a commercial license * * Hint: While a) - c) work, d) is the right answer :-) * * But before you do any of a) - c), consider this. We give away this awesome software for free, * and we'd love to continue giving it away for free, so all we would like to ask you is to * continue to show your love and our brand to everyone involved in your software simply in the * log files when you load jOOQ. Please don't remove our logo. * * Thank you very much! If you absolutely must remove this logo, and can live with the guilt * and shame, below is the system property that deactivates it. * * Cheers from the jOOQ Team. * * -------------------------------------------------- * DEAR USER, PLEASE READ THE ABOVE BEFORE PROCEEDING */ if (!Boolean.getBoolean("org.jooq.no-logo")) { JooqLogger l = JooqLogger.getLogger(Constants.class); String message; message = "Thank you for using jOOQ " + Constants.FULL_VERSION; l.info("\n " + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@ @@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@ @@@@ @@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + "\n@@@@@@@@@@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@ @@ @@ @@@@ @@@@@@@@@@" + "\n@@@@@@@@@@ @@ @@ @@@@ @@@@@@@@@@" + "\n@@@@@@@@@@ @@ @ @ @@@@@@@@@@" + "\n@@@@@@@@@@ @@ @@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ " + message + "\n "); } }
A query execution interception signal.

This exception is used as a signal for jOOQ's internals to abort query execution, and return generated SQL back to batch execution.

/** * A query execution interception signal. * <p> * This exception is used as a signal for jOOQ's internals to abort query * execution, and return generated SQL back to batch execution. */
class ForceInlineSignal extends ControlFlowSignal {
Generated UID
/** * Generated UID */
private static final long serialVersionUID = -9131368742983295195L; public ForceInlineSignal() { if (log.isDebugEnabled()) log.debug("Re-render query", "Forcing bind variable inlining as " + configuration().dialect() + " does not support " + params + " bind variables (or more) in a single query"); } }
A query rendering signal to force re-rendering a query with different settings.
/** * A query rendering signal to force re-rendering a query with different * settings. */
static class ForceSettingsSignal extends ControlFlowSignal {
Generated UID
/** * Generated UID */
private static final long serialVersionUID = -1530836969063166588L; final Settings settings; ForceSettingsSignal(Settings settings) { this.settings = settings; } } }