Skip to content

Commit

Permalink
Added the ability to specify a custom SQL formatter via the -sqlForma…
Browse files Browse the repository at this point in the history
…tter option.
  • Loading branch information
johncurrier committed Apr 30, 2010
1 parent 2c94d70 commit e3fe369
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 0 deletions.
242 changes: 242 additions & 0 deletions src/net/sourceforge/schemaspy/view/DefaultSqlFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package net.sourceforge.schemaspy.view;

import java.sql.DatabaseMetaData;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import net.sourceforge.schemaspy.model.Database;
import net.sourceforge.schemaspy.model.Table;
import net.sourceforge.schemaspy.util.CaseInsensitiveMap;
import net.sourceforge.schemaspy.util.HtmlEncoder;

/**
* Default implementation of {@link SqlFormatter}
*
* @author John Currier
*/
public class DefaultSqlFormatter implements SqlFormatter {
private Set<String> keywords;
private Map<String, Table> tablesByPossibleNames;
private static String TOKENS = " \t\n\r\f()<>|,";

/**
* Return a HTML-formatted representation of the specified SQL.
*
* @param sql SQL to be formatted
* @param db Database
* @return HTML-formatted representation of the specified SQL
*/
public String format(String sql, Database db, Set<Table> references) {
StringBuilder formatted = new StringBuilder(sql.length() * 2);

boolean alreadyFormatted = sql.contains("\n") || sql.contains("\r");
if (alreadyFormatted)
{
// apparently already formatted, so dump it as is
formatted.append("<div class='viewDefinition preFormatted'>");

int len = sql.length();
for (int i = 0; i < len; i++) {
char ch = sql.charAt(i);

// encode everything except whitespace
if (Character.isWhitespace(ch)) {
formatted.append(ch);
} else {
formatted.append(HtmlEncoder.encodeToken(ch));
}
}
}
else
{
formatted.append(" <div class='viewDefinition'>");
@SuppressWarnings("hiding")
Set<String> keywords = getKeywords(db.getMetaData());
StringTokenizer tokenizer = new StringTokenizer(sql, TOKENS, true);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (keywords.contains(token.toUpperCase())) {
formatted.append("<b>");
formatted.append(token);
formatted.append("</b>");
} else {
formatted.append(HtmlEncoder.encodeToken(token));
}
}
}

formatted.append("</div>");

references.addAll(getReferencedTables(sql, db));

return formatted.toString();
}

/**
* Returns a {@link Set} of tables/views that are possibly referenced
* by the specified SQL.
*
* @param sql
* @param db
* @return
*/
protected Set<Table> getReferencedTables(String sql, Database db) {
Set<Table> referenced = new HashSet<Table>();

Map<String, Table> tables = getTableMap(db);
@SuppressWarnings("hiding")
Set<String> keywords = getKeywords(db.getMetaData());

StringTokenizer tokenizer = new StringTokenizer(sql, TOKENS, true);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (!keywords.contains(token.toUpperCase())) {
Table t = tables.get(token);

if (t == null) {
int lastDot = token.lastIndexOf('.');
if (lastDot != -1) {
t = tables.get(token.substring(0, lastDot));
}
}

if (t != null) {
referenced.add(t);
}
}
}

return referenced;
}

/**
* Returns a {@link Map} of all tables/views in the database
* keyed by several possible ways to refer to the table.
*
* @param db
* @return
*/
protected Map<String, Table> getTableMap(Database db)
{
if (tablesByPossibleNames == null)
{
tablesByPossibleNames = new CaseInsensitiveMap<Table>();

tablesByPossibleNames.putAll(getTableMap(db.getTables(), db.getName()));
tablesByPossibleNames.putAll(getTableMap(db.getViews(), db.getName()));
}

return tablesByPossibleNames;
}

/**
* Returns a {@link Map} of the specified tables/views
* keyed by several possible ways to refer to the table.
*
* @param tables
* @param dbName
* @return
*/
protected Map<String, Table> getTableMap(Collection<? extends Table> tables, String dbName) {
Map<String, Table> map = new CaseInsensitiveMap<Table>();
for (Table t : tables) {
String name = t.getName();
String schema = t.getSchema();
if (schema == null)
schema = dbName;

map.put(name, t);
map.put("`" + name + "`", t);
map.put("'" + name + "'", t);
map.put("\"" + name + "\"", t);
map.put(schema + "." + name, t);
map.put("`" + schema + "`.`" + name + "`", t);
map.put("'" + schema + "'.'" + name + "'", t);
map.put("\"" + schema + "\".\"" + name + "\"", t);
map.put("`" + schema + '.' + name + "`", t);
map.put("'" + schema + '.' + name + "'", t);
map.put("\"" + schema + '.' + name + "\"", t);
}

return map;
}

/**
* @param meta
* @return
*/
public Set<String> getKeywords(DatabaseMetaData meta) {
if (keywords == null) {
keywords = new HashSet<String>(Arrays.asList(new String[] {
"ABSOLUTE", "ACTION", "ADD", "ALL", "ALLOCATE", "ALTER", "AND",
"ANY", "ARE", "AS", "ASC", "ASSERTION", "AT", "AUTHORIZATION", "AVG",
"BEGIN", "BETWEEN", "BIT", "BIT_LENGTH", "BOTH", "BY",
"CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER",
"CHAR_LENGTH", "CHARACTER_LENGTH", "CHECK", "CLOSE", "COALESCE",
"COLLATE", "COLLATION", "COLUMN", "COMMIT", "CONNECT", "CONNECTION",
"CONSTRAINT", "CONSTRAINTS", "CONTINUE", "CONVERT", "CORRESPONDING",
"COUNT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME",
"CURRENT_TIMESTAMP", "CURRENT_USER", "CURSOR",
"DATE", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT",
"DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DESCRIBE", "DESCRIPTOR",
"DIAGNOSTICS", "DISCONNECT", "DISTINCT", "DOMAIN", "DOUBLE", "DROP",
"ELSE", "END", "END - EXEC", "ESCAPE", "EXCEPT", "EXCEPTION", "EXEC",
"EXECUTE", "EXISTS", "EXTERNAL", "EXTRACT",
"FALSE", "FETCH", "FIRST", "FLOAT", "FOR", "FOREIGN", "FOUND", "FROM", "FULL",
"GET", "GLOBAL", "GO", "GOTO", "GRANT", "GROUP",
"HAVING", "HOUR",
"IDENTITY", "IMMEDIATE", "IN", "INDICATOR", "INITIALLY", "INNER", "INPUT",
"INSENSITIVE", "INSERT", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO",
"IS", "ISOLATION",
"JOIN",
"KEY",
"LANGUAGE", "LAST", "LEADING", "LEFT", "LEVEL", "LIKE", "LOCAL", "LOWER",
"MATCH", "MAX", "MIN", "MINUTE", "MODULE", "MONTH",
"NAMES", "NATIONAL", "NATURAL", "NCHAR", "NEXT", "NO", "NOT", "NULL",
"NULLIF", "NUMERIC",
"OCTET_LENGTH", "OF", "ON", "ONLY", "OPEN", "OPTION", "OR", "ORDER",
"OUTER", "OUTPUT", "OVERLAPS",
"PAD", "PARTIAL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY",
"PRIOR", "PRIVILEGES", "PROCEDURE", "PUBLIC",
"READ", "REAL", "REFERENCES", "RELATIVE", "RESTRICT", "REVOKE", "RIGHT",
"ROLLBACK", "ROWS",
"SCHEMA", "SCROLL", "SECOND", "SECTION", "SELECT", "SESSION", "SESSION_USER",
"SET", "SIZE", "SMALLINT", "SOME", "SPACE", "SQL", "SQLCODE", "SQLERROR",
"SQLSTATE", "SUBSTRING", "SUM", "SYSTEM_USER",
"TABLE", "TEMPORARY", "THEN", "TIME", "TIMESTAMP", "TIMEZONE_HOUR",
"TIMEZONE_MINUTE", "TO", "TRAILING", "TRANSACTION", "TRANSLATE",
"TRANSLATION", "TRIM", "TRUE",
"UNION", "UNIQUE", "UNKNOWN", "UPDATE", "UPPER", "USAGE", "USER", "USING",
"VALUE", "VALUES", "VARCHAR", "VARYING", "VIEW",
"WHEN", "WHENEVER", "WHERE", "WITH", "WORK", "WRITE",
"YEAR",
"ZONE"
}));

try {
String keywordsArray[] = new String[] {
meta.getSQLKeywords(),
meta.getSystemFunctions(),
meta.getNumericFunctions(),
meta.getStringFunctions(),
meta.getTimeDateFunctions()
};
for (int i = 0; i < keywordsArray.length; ++i) {
StringTokenizer tokenizer = new StringTokenizer(keywordsArray[i].toUpperCase(), ",");

while (tokenizer.hasMoreTokens()) {
keywords.add(tokenizer.nextToken().trim());
}
}
} catch (Exception exc) {
// don't totally fail just because we can't extract these details...
System.err.println(exc);
}
}

return keywords;
}
}
25 changes: 25 additions & 0 deletions src/net/sourceforge/schemaspy/view/SqlFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.sourceforge.schemaspy.view;

import java.util.Set;
import net.sourceforge.schemaspy.model.Database;
import net.sourceforge.schemaspy.model.Table;

/**
* Implementations of this interface know how to take SQL and format it
* into (hopefully) readable HTML.
*
* @author John Currier
*/
public interface SqlFormatter {
/**
* Return a HTML-formatted representation of the specified SQL.
*
* @param sql SQL to be formatted
* @param db Database
* @param references set of tables referenced by this SQL
* (populated by the formatter) or left empty if the formatter already
* provides references to those tables
* @return HTML-formatted representation of the specified SQL
*/
String format(String sql, Database db, Set<Table> references);
}

0 comments on commit e3fe369

Please sign in to comment.