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 1433608 commit 2c94d70
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 137 deletions.
9 changes: 9 additions & 0 deletions dist/releaseNotes.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ <h3><a href='http://schemaspy.sourceforge.net'>SchemaSpy</a> Release Notes</h3>
information or not (MySQL does not).
Thanks to Reiner Kräutle for the suggestion.
</li>
<li>Added the ability to specify a custom SQL formatter via the -sqlFormatter
option.
This formatter must implement the
<a href='http://schemaspy.svn.sourceforge.net/viewvc/schemaspy/trunk/src/net/sourceforge/schemaspy/view/SqlFormatter.java?view=log'>
SqlFormatter</a> interface and return the SQL formatted appropriately in HTML.
Implementing a "good" formatter is basically a project in itself and beyond
the scope of SchemaSpy, so plug in your own if desired.
Use -dp to make the specified class visible to SchemaSpy's class loader.
</li>
</ul>
</li>

Expand Down
77 changes: 74 additions & 3 deletions src/net/sourceforge/schemaspy/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import net.sourceforge.schemaspy.model.InvalidConfigurationException;
import net.sourceforge.schemaspy.util.DbSpecificConfig;
import net.sourceforge.schemaspy.util.Dot;
import net.sourceforge.schemaspy.view.DefaultSqlFormatter;
import net.sourceforge.schemaspy.view.SqlFormatter;

/**
* Configuration of a SchemaSpy run
Expand Down Expand Up @@ -73,6 +75,8 @@ public class Config
private Integer fontSize;
private String description;
private String dbPropertiesLoadedFrom;
private SqlFormatter sqlFormatter;
private String sqlFormatterClass;
private Boolean generateHtml;
private Boolean includeImpliedConstraints;
private Boolean logoEnabled;
Expand Down Expand Up @@ -612,6 +616,7 @@ public void setMaxDbThreads(int maxDbThreads) {

/**
* @see #setMaxDbThreads(int)
* @throws InvalidConfigurationException if unable to load properties
*/
public int getMaxDbThreads() throws InvalidConfigurationException {
if (maxDbThreads == null) {
Expand Down Expand Up @@ -897,6 +902,56 @@ public List<String> getSchemas() {
return schemas;
}

/**
* Set the name of the {@link SqlFormatter SQL formatter} class to use to
* format SQL into HTML.<p/>
* The implementation of the class must be made available to the class
* loader, typically by specifying the path to its jar with <em>-dp</em>
* ({@link #setDriverPath(String)}).
*/
public void setSqlFormatter(String formatterClassName) {
sqlFormatterClass = formatterClassName;
sqlFormatter = null;
}

/**
* Set the {@link SqlFormatter SQL formatter} to use to format
* SQL into HTML.
*/
public void setSqlFormatter(SqlFormatter sqlFormatter) {
this.sqlFormatter = sqlFormatter;
if (sqlFormatter != null)
sqlFormatterClass = sqlFormatter.getClass().getName();
}

/**
* Returns an implementation of {@link SqlFormatter SQL formatter} to use to format
* SQL into HTML. The default implementation is {@link DefaultSqlFormatter}.
*
* @return
* @throws InvalidConfigurationException if unable to instantiate an instance
*/
@SuppressWarnings("unchecked")
public SqlFormatter getSqlFormatter() throws InvalidConfigurationException {
if (sqlFormatter == null) {
if (sqlFormatterClass == null) {
sqlFormatterClass = pullParam("-sqlFormatter");

if (sqlFormatterClass == null)
sqlFormatterClass = DefaultSqlFormatter.class.getName();
}

try {
Class<SqlFormatter> clazz = (Class<SqlFormatter>)Class.forName(sqlFormatterClass);
sqlFormatter = clazz.newInstance();
} catch (Exception exc) {
throw new InvalidConfigurationException("Failed to initialize instance of " + sqlFormatterClass, exc);
}
}

return sqlFormatter;
}

public void setEvaluateAllEnabled(boolean enabled) {
evaluteAll = enabled;
}
Expand Down Expand Up @@ -1048,13 +1103,18 @@ public boolean isDbHelpRequired() {
return dbHelpRequired;
}


public static String getLoadedFromJar() {
String classpath = System.getProperty("java.class.path");
return new StringTokenizer(classpath, File.pathSeparator).nextToken();
}

public Properties getDbProperties(String type) throws IOException {
/**
* @param type
* @return
* @throws IOException
* @throws InvalidConfigurationException if db properties are incorrectly formed
*/
public Properties getDbProperties(String type) throws IOException, InvalidConfigurationException {
ResourceBundle bundle = null;

try {
Expand Down Expand Up @@ -1189,7 +1249,15 @@ private String pullRequiredParam(String paramId) {
return pullParam(paramId, true, false);
}

private String pullParam(String paramId, boolean required, boolean dbTypeSpecific) {
/**
* @param paramId
* @param required
* @param dbTypeSpecific
* @return
* @throws MissingRequiredParameterException
*/
private String pullParam(String paramId, boolean required, boolean dbTypeSpecific)
throws MissingRequiredParameterException {
int paramIndex = options.indexOf(paramId);
if (paramIndex < 0) {
if (required)
Expand All @@ -1202,6 +1270,9 @@ private String pullParam(String paramId, boolean required, boolean dbTypeSpecifi
return param;
}

/**
* Thrown to indicate that a required parameter is missing
*/
public static class MissingRequiredParameterException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final boolean dbTypeSpecific;
Expand Down
163 changes: 30 additions & 133 deletions src/net/sourceforge/schemaspy/view/HtmlTablePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

import java.io.File;
import java.io.IOException;
import java.sql.DatabaseMetaData;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import net.sourceforge.schemaspy.Config;
import net.sourceforge.schemaspy.model.Database;
Expand All @@ -30,7 +27,6 @@
*/
public class HtmlTablePage extends HtmlFormatter {
private static final HtmlTablePage instance = new HtmlTablePage();
private Set<String> keywords = null;
private int columnCounter = 0;

private final Map<String, String> defaultValueAliases = new HashMap<String, String>();
Expand Down Expand Up @@ -270,11 +266,14 @@ private void writeRelatives(TableColumn baseRelative, boolean dumpParents, Strin
}

private void writeNumRows(Database db, Table table, LineWriter out) throws IOException {
if (displayNumRows && !table.isView())
out.write("<p>Table contained " + NumberFormat.getIntegerInstance().format(table.getNumRows()) + " rows at ");
else
out.write("<p>Analyzed at ");
out.writeln(db.getConnectTime());
out.write("<p title='" + table.getColumns().size() + " columns'>");
if (displayNumRows && !table.isView()) {
out.write("Table contained " + NumberFormat.getIntegerInstance().format(table.getNumRows()) + " rows at ");
} else {
out.write("Analyzed at ");
}
out.write(db.getConnectTime());
out.writeln("<p/>");
}

private void writeCheckConstraints(Table table, LineWriter out) throws IOException {
Expand Down Expand Up @@ -400,135 +399,33 @@ private void writeView(Table table, Database db, LineWriter out) throws IOExcept
for (View v : db.getViews())
tables.put(v.getName(), v);

out.writeln("<div class='indent'>");
out.writeln("View Definition:");
out.writeln("<table class='dataTable' border='1' width='100%'>");
out.writeln("<tbody>");
out.writeln(" <tr>");

boolean alreadyFormatted = sql.contains("\n") || sql.contains("\r");
if (alreadyFormatted)
{
out.write(" <td class='viewDefinition preFormatted'>");

int len = sql.length();
for (int i = 0; i < len; i++) {
char ch = sql.charAt(i);
if (Character.isWhitespace(ch))
out.write(ch);
else
out.write(HtmlEncoder.encodeToken(ch));
}
}
else
{
out.write(" <td class='viewDefinition'>");
@SuppressWarnings("hiding")
Set<String> keywords = getKeywords(db.getMetaData());
StringTokenizer tokenizer = new StringTokenizer(sql, " \t\n\r\f()<>|.,", true);
while (tokenizer.hasMoreTokens()) {
String nextToken = tokenizer.nextToken();
if (keywords.contains(nextToken.toUpperCase())) {
out.write("<b>");
out.write(nextToken);
out.write("</b>");
} else {
Table t = tables.get(nextToken);
if (t != null) {
out.write("<a href='");
out.write(t.getName());
out.write(".html'>");
out.write(t.getName());
out.write("</a>");
} else {
out.write(HtmlEncoder.encodeToken(nextToken));
}
}
}
}
Set<Table> references = new TreeSet<Table>();
String formatted = Config.getInstance().getSqlFormatter().format(sql, db, references);

out.writeln("</td>");
out.writeln(" </tr>");
out.writeln("</table>");
out.writeln("<div class='indent spacer'>");
out.writeln(" View Definition:");
out.writeln(formatted);
out.writeln("</div>");
}
}

/**
* getSqlKeywords
*
* @return Object
*/
private 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());
}
out.writeln("<div class='spacer'>&nbsp;</div>");

if (!references.isEmpty()) {
out.writeln("<div class='indent'>");
out.writeln(" Possibly Referenced Tables/Views:");
out.writeln(" <div class='viewReferences'>");
out.write(" ");
for (Table t : references) {
out.write("<a href='");
out.write(t.getName());
out.write(".html'>");
out.write(t.getName());
out.write("</a>&nbsp;");
}
} catch (Exception exc) {
// don't totally fail just because we can't extract these details...
System.err.println(exc);

out.writeln(" </div>");
out.writeln("</div><p/>");
}
}

return keywords;
}
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/schemaSpy.css
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,27 @@ a:visited {
}

.viewDefinition {
font-size: 90%;
background-color: #ffffff;
border-style: solid;
border-width: 1px;
float: left;
padding: 4px;
font-family: "Courier New", Courier, monospace
}

.viewReferences {
font-size: 90%;
padding: 4px;
}

/* wrap around divs that float so they "take up space" */
div.spacer {
clear: both;
}

.preFormatted {
white-space:pre;
white-space: pre;
}

/* Tabs from http://www.alistapart.com/articles/slidingdoors/ */
Expand Down

0 comments on commit 2c94d70

Please sign in to comment.