/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * 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.jkiss.dbeaver.model.sql.format.tokenized; import org.jkiss.dbeaver.model.DBPIdentifierCase; import org.jkiss.dbeaver.model.sql.SQLUtils; import org.jkiss.dbeaver.model.sql.format.SQLFormatter; import org.jkiss.dbeaver.model.sql.format.SQLFormatterConfiguration; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.Pair; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * SQL formatter */ public class SQLTokenizedFormatter implements SQLFormatter { public static final String FORMATTER_ID = "DEFAULT"; private static final String[] JOIN_BEGIN = { "LEFT", "RIGHT", "INNER", "OUTER", "JOIN" }; private SQLFormatterConfiguration formatterCfg; private List<Boolean> functionBracket = new ArrayList<>(); private List<String> statementDelimiters = new ArrayList<>(2); private String delimiterRedefiner; @Override public String format(final String argSql, SQLFormatterConfiguration configuration) { formatterCfg = configuration; for (String delim : formatterCfg.getSyntaxManager().getStatementDelimiters()) { statementDelimiters.add(delim.toUpperCase(Locale.ENGLISH)); } delimiterRedefiner = formatterCfg.getSyntaxManager().getDialect().getScriptDelimiterRedefiner(); if (delimiterRedefiner != null) { delimiterRedefiner = delimiterRedefiner.toUpperCase(Locale.ENGLISH); } SQLTokensParser fParser = new SQLTokensParser(formatterCfg); functionBracket.clear(); boolean isSqlEndsWithNewLine = false; if (argSql.endsWith("\n")) { //$NON-NLS-1$ isSqlEndsWithNewLine = true; } List<FormatterToken> list = fParser.parse(argSql); list = format(list); StringBuilder after = new StringBuilder(argSql.length() + 20); for (FormatterToken token : list) { after.append(token.getString()); } if (isSqlEndsWithNewLine) { after.append(GeneralUtils.getDefaultLineSeparator()); } return after.toString(); } private List<FormatterToken> format(final List<FormatterToken> argList) { if (argList.isEmpty()) { return argList; } FormatterToken token = argList.get(0); if (token.getType() == TokenType.SPACE) { argList.remove(0); if (argList.isEmpty()) { return argList; } } token = argList.get(argList.size() - 1); if (token.getType() == TokenType.SPACE) { argList.remove(argList.size() - 1); if (argList.isEmpty()) { return argList; } } final DBPIdentifierCase keywordCase = formatterCfg.getKeywordCase(); for (int index = 0; index < argList.size(); index++) { token = argList.get(index); if (token.getType() == TokenType.KEYWORD) { token.setString(keywordCase.transform(token.getString())); } } // Remove extra tokens (spaces, etc) for (int index = argList.size() - 1; index >= 1; index--) { token = argList.get(index); FormatterToken prevToken = argList.get(index - 1); if (token.getType() == TokenType.SPACE && (prevToken.getType() == TokenType.SYMBOL || prevToken.getType() == TokenType.COMMENT)) { argList.remove(index); } else if ((token.getType() == TokenType.SYMBOL || token.getType() == TokenType.COMMENT) && prevToken.getType() == TokenType.SPACE) { argList.remove(index - 1); } else if (token.getType() == TokenType.SPACE) { token.setString(" "); //$NON-NLS-1$ } } for (int index = 0; index < argList.size() - 2; index++) { FormatterToken t0 = argList.get(index); FormatterToken t1 = argList.get(index + 1); FormatterToken t2 = argList.get(index + 2); if (t0.getType() == TokenType.KEYWORD && t1.getType() == TokenType.SPACE && t2.getType() == TokenType.KEYWORD) { if (((t0.getString().equalsIgnoreCase("ORDER") || t0 //$NON-NLS-1$ .getString().equalsIgnoreCase("GROUP")) && t2 //$NON-NLS-1$ .getString().equalsIgnoreCase("BY"))) { //$NON-NLS-1$ t0.setString(t0.getString() + " " + t2.getString()); //$NON-NLS-1$ argList.remove(index + 1); argList.remove(index + 1); } } // Oracle style joins if (t0.getString().equals("(") && t1.getString().equals("+") && t2.getString().equals(")")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ t0.setString("(+)"); //$NON-NLS-1$ argList.remove(index + 1); argList.remove(index + 1); } } int indent = 0; final List<Integer> bracketIndent = new ArrayList<>(); FormatterToken prev = new FormatterToken(TokenType.SPACE, " "); //$NON-NLS-1$ boolean encounterBetween = false; for (int index = 0; index < argList.size(); index++) { token = argList.get(index); String tokenString = token.getString().toUpperCase(Locale.ENGLISH); if (token.getType() == TokenType.SYMBOL) { if (tokenString.equals("(")) { //$NON-NLS-1$ functionBracket.add(formatterCfg.isFunction(prev.getString()) ? Boolean.TRUE : Boolean.FALSE); bracketIndent.add(indent); indent++; index += insertReturnAndIndent(argList, index + 1, indent); } else if (tokenString.equals(")") && !bracketIndent.isEmpty() && !functionBracket.isEmpty()) { //$NON-NLS-1$ indent = bracketIndent.remove(bracketIndent.size() - 1); index += insertReturnAndIndent(argList, index, indent); functionBracket.remove(functionBracket.size() - 1); } else if (tokenString.equals(",")) { //$NON-NLS-1$ index += insertReturnAndIndent(argList, index + 1, indent); } else if (statementDelimiters.contains(tokenString)) { //$NON-NLS-1$ indent = 0; index += insertReturnAndIndent(argList, index, indent); } } else if (token.getType() == TokenType.KEYWORD) { switch (tokenString) { case "DELETE": case "SELECT": case "UPDATE": //$NON-NLS-1$ case "INSERT": case "INTO": case "CREATE": case "DROP": case "TRUNCATE": case "TABLE": case "CASE": //$NON-NLS-1$ indent++; index += insertReturnAndIndent(argList, index + 1, indent); break; case "FROM": case "WHERE": case "SET": case "ORDER BY": case "GROUP BY": case "HAVING": //$NON-NLS-1$ index += insertReturnAndIndent(argList, index, indent - 1); index += insertReturnAndIndent(argList, index + 1, indent); break; case "LEFT": case "RIGHT": case "INNER": case "OUTER": case "JOIN": if (isJoinStart(argList, index)) { index += insertReturnAndIndent(argList, index, indent - 1); } if (tokenString.equals("JOIN")) { //index += insertReturnAndIndent(argList, index + 1, indent); } break; case "VALUES": //$NON-NLS-1$ indent--; index += insertReturnAndIndent(argList, index, indent); break; case "END": //$NON-NLS-1$ indent--; index += insertReturnAndIndent(argList, index, indent); break; case "OR": case "WHEN": case "ELSE": //$NON-NLS-1$ index += insertReturnAndIndent(argList, index, indent); break; case "ON": //indent++; index += insertReturnAndIndent(argList, index + 1, indent); break; case "USING": //$NON-NLS-1$ //$NON-NLS-2$ index += insertReturnAndIndent(argList, index, indent + 1); break; case "UNION": case "INTERSECT": case "EXCEPT": //$NON-NLS-1$ indent -= 2; index += insertReturnAndIndent(argList, index, indent); //index += insertReturnAndIndent(argList, index + 1, indent); indent++; break; case "BETWEEN": //$NON-NLS-1$ encounterBetween = true; break; case "AND": //$NON-NLS-1$ if (!encounterBetween) { index += insertReturnAndIndent(argList, index, indent); } encounterBetween = false; break; } } else if (token.getType() == TokenType.COMMENT) { boolean isComment = false; String[] slComments = formatterCfg.getSyntaxManager().getDialect().getSingleLineComments(); if (slComments != null) { for (String slc : slComments) { if (token.getString().startsWith(slc)) { index += insertReturnAndIndent(argList, index + 1, indent); isComment = true; break; } } } if (!isComment) { Pair<String, String> mlComments = formatterCfg.getSyntaxManager().getDialect().getMultiLineComments(); if (mlComments != null) { if (token.getString().startsWith(mlComments.getFirst())) { index += insertReturnAndIndent(argList, index + 1, indent); } } } } else if (token.getType() == TokenType.COMMAND) { indent = 0; if (index > 0) { index += insertReturnAndIndent(argList, index, 0); } index += insertReturnAndIndent(argList, index + 1, 0); if (!CommonUtils.isEmpty(delimiterRedefiner) && token.getString().startsWith(delimiterRedefiner)) { final String command = token.getString().trim().toUpperCase(Locale.ENGLISH); final int divPos = command.lastIndexOf(' '); if (divPos > 0) { String delimiter = command.substring(divPos).trim(); if (!CommonUtils.isEmpty(delimiter)) { statementDelimiters.clear(); statementDelimiters.add(delimiter); } } } } else { if (statementDelimiters.contains(tokenString)) { indent = 0; index += insertReturnAndIndent(argList, index + 1, indent); } } prev = token; } for (int index = argList.size() - 1; index >= 4; index--) { if (index >= argList.size()) { continue; } FormatterToken t0 = argList.get(index); FormatterToken t1 = argList.get(index - 1); FormatterToken t2 = argList.get(index - 2); FormatterToken t3 = argList.get(index - 3); FormatterToken t4 = argList.get(index - 4); if (t4.getString().equals("(") //$NON-NLS-1$ && t3.getString().trim().isEmpty() && t1.getString().trim().isEmpty() && t0.getString().equalsIgnoreCase(")")) //$NON-NLS-1$ { t4.setString(t4.getString() + t2.getString() + t0.getString()); argList.remove(index); argList.remove(index - 1); argList.remove(index - 2); argList.remove(index - 3); } } for (int index = 1; index < argList.size(); index++) { prev = argList.get(index - 1); token = argList.get(index); if (prev.getType() != TokenType.SPACE && token.getType() != TokenType.SPACE && !token.getString().startsWith("(")) { if (token.getString().equals(",") || statementDelimiters.contains(token.getString())) { //$NON-NLS-1$ continue; } if (formatterCfg.isFunction(prev.getString()) && token.getString().equals("(")) { //$NON-NLS-1$ continue; } if (token.getType() == TokenType.VALUE && prev.getType() == TokenType.NAME) { // Do not add space between name and value [JDBC:MSSQL] continue; } if (token.getType() == TokenType.SYMBOL && isEmbeddedToken(token) || prev.getType() == TokenType.SYMBOL && isEmbeddedToken(prev)) { // Do not insert spaces around colons continue; } if (token.getType() == TokenType.SYMBOL && prev.getType() == TokenType.SYMBOL) { // Do not add space between symbols continue; } argList.add(index, new FormatterToken(TokenType.SPACE, " ")); //$NON-NLS-1$ } } return argList; } private static boolean isEmbeddedToken(FormatterToken token) { return ":".equals(token.getString()) || ".".equals(token.getString()); } private boolean isJoinStart(List<FormatterToken> argList, int index) { // Keyword sequence must start from LEFT, RIGHT, INNER, OUTER or JOIN and must end with JOIN // And we must be in the beginning of sequence // check current token if (!ArrayUtils.contains(JOIN_BEGIN, argList.get(index).getString())) { return false; } // check previous token for (int i = index - 1; i >= 0; i--) { FormatterToken token = argList.get(i); if (token.getType() == TokenType.SPACE) { continue; } if (ArrayUtils.contains(JOIN_BEGIN, token.getString())) { // It is not the begin of sequence return false; } else { break; } } // check last token for (int i = index; i < argList.size(); i++) { FormatterToken token = argList.get(i); if (token.getType() == TokenType.SPACE) { continue; } if (token.getString().equals("JOIN")) { return true; } if (!ArrayUtils.contains(JOIN_BEGIN, token.getString())) { // It is not the begin of sequence return false; } } return false; } private int insertReturnAndIndent(final List<FormatterToken> argList, final int argIndex, final int argIndent) { if (functionBracket.contains(Boolean.TRUE)) return 0; try { String s = GeneralUtils.getDefaultLineSeparator(); if (argIndex > 0) { final FormatterToken prevToken = argList.get(argIndex - 1); if (prevToken.getType() == TokenType.COMMENT && SQLUtils.isCommentLine(formatterCfg.getSyntaxManager().getDialect(), prevToken.getString())) { s = ""; //$NON-NLS-1$ } } for (int index = 0; index < argIndent; index++) { s += formatterCfg.getIndentString(); } FormatterToken token = argList.get(argIndex); if (token.getType() == TokenType.SPACE) { token.setString(s); return 0; } boolean isDelimiter = statementDelimiters.contains(token.getString().toUpperCase()); if (!isDelimiter) { token = argList.get(argIndex - 1); if (token.getType() == TokenType.SPACE) { token.setString(s); return 0; } } if (isDelimiter) { if (argList.size() > argIndex + 1) { argList.add(argIndex + 1, new FormatterToken(TokenType.SPACE, s + s)); } } else { argList.add(argIndex, new FormatterToken(TokenType.SPACE, s)); } return 1; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); return 0; } } }