/*
* JBoss, Home of Professional Open Source.
*
* See the LEGAL.txt file distributed with this work for information regarding copyright ownership and licensing.
*
* See the AUTHORS.txt file distributed with this work for a full listing of individual contributors.
*/
package org.teiid.query.ui.sqleditor.sql;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import org.teiid.query.ui.UiConstants;
import org.teiid.query.ui.UiPlugin;
/**
* SqlFormattingStrategy
* This class will take a valid sql string and rerender it with indentation
* based on our indenting rules and corresponding user prefs. It is not intended to work within the
* jface Document/DocumentCommand scheme. That scheme is intended to deal with
* incremental changes (one keystroke at a time), and is quite inefficient. This class will apply the
* formatting rules efficiently to a whole string. (jh)
*
*
* @since 8.0
*/
public class SqlFormattingStrategy
implements SqlFormattingConstants,
UiConstants {
private String sInSql;
private String sFormattedSql;
private HashMap hmParseResult;
private ArrayList arylKeywordInstances;
/**
* Construct an instance of SqlFormattingStrategy.
*
*/
public SqlFormattingStrategy() {
super();
}
private void init() {
sInSql = null;
sFormattedSql = null;
hmParseResult = null;
arylKeywordInstances = null;
}
private HashMap getParseMap() {
if (hmParseResult == null ) {
hmParseResult = new HashMap();
}
return hmParseResult;
}
private ArrayList getOffsetList( String sKeyword ) {
if( getParseMap().get( sKeyword ) == null ) {
getParseMap().put( sKeyword, new ArrayList() );
}
return (ArrayList)getParseMap().get( sKeyword );
}
private ArrayList getKeywordOffsetsArray() {
if (arylKeywordInstances == null ) {
arylKeywordInstances = new ArrayList();
}
return arylKeywordInstances;
}
public String format( String sInSql ) {
init();
this.sInSql = sInSql;
sFormattedSql = sInSql;
if ( sInSql != null && sInSql.trim().length() > 0 ) {
if ( okToProcess( sInSql ) ) {
sInSql = filter( sInSql );
parse( sInSql );
markupTheKeywordInstances();
sFormattedSql = formatTheString( sInSql );
}
} else {
// System.out.println("[SqlFormattingStrategy.format] empty string; no action"); //$NON-NLS-1$
}
return sFormattedSql;
}
/*
* parse and filter the sql string in a single pass
*/
private String filter( String s ) {
StringBuffer sbTarget = new StringBuffer( s.length() );
char chCurrentChar = SPACE;
// boolean bOkToProcess =
okToProcess( s );
// parse the sql string looking for the keywords; store the results in the map
for ( int iPos = 0; iPos < sInSql.length(); iPos++ ) {
chCurrentChar = s.charAt( iPos );
// 2. Filter out certain whitespace characters (mostly the ones our process might re-add)
if ( chCurrentChar == chNEWLINE
|| chCurrentChar == chINDENT ) {
// skip the char
} else {
// pass it on
sbTarget.append( chCurrentChar );
}
}
return sbTarget.toString();
}
/*
* parse and filter the sql string in a single pass
*/
private String parse( String s ) {
boolean bMatch = false;
StringBuffer sbTarget = new StringBuffer( s.length() );
int iUnclosedParenCount = 0;
char chCurrentChar = ' ';
// parse the sql string looking for the keywords; store the results in the map
for ( int iPos = 0; iPos < s.length(); iPos++ ) {
chCurrentChar = s.charAt( iPos );
// 0. '(' and ')'
// Rule: no formatting for keywords that appear in subselects.
// Implementation: Keep track of open parens found that have not yet been
// matched by a close paren. When this is non-zero, we are
// in subselect territory.
if ( chCurrentChar == PAREN_OPEN ) {
iUnclosedParenCount++;
continue;
}
if ( chCurrentChar == PAREN_CLOSE ) {
iUnclosedParenCount--;
continue;
}
// 1. Capture keywords
// only try to match keywords at breaks after words
if ( iUnclosedParenCount == 0 && chCurrentChar == SPACE ) {
for ( int ixKeyword = 0; ixKeyword < KEYWORDS.length; ixKeyword++ ) {
int iWordStart = iPos - KEYWORDS[ixKeyword].length();
String sKeyword = KEYWORDS[ixKeyword];
bMatch = s.regionMatches( true,
iWordStart,
sKeyword,
0,
sKeyword.length() );
if( bMatch ) {
// store it
ArrayList arylOffsets = getOffsetList( sKeyword );
arylOffsets.add( new Integer( iWordStart ) );
KeywordInstance kwi = new KeywordInstance( sKeyword, iWordStart );
// System.out.println("[SqlFormattingStrategy.parseAndFilter] About to add kwi: " + kwi.sKeyword ); //$NON-NLS-1$
getKeywordOffsetsArray().add( kwi );
// quit this loop when you find and process a match
break;
}
}
}
}
return sbTarget.toString();
}
private boolean okToProcess( String s ) {
boolean bOk = true; // default to true
if ( s.indexOf( CREATE_PROCEDURE ) > -1 ) {
bOk = false;
}
return bOk;
}
private void markupTheKeywordInstances() {
/*
* Process the parse map. The general idea is to work out a strategy,
* then apply it backwards to the input string (in a StringBuffer). This
* way the meaning of the offsets in the parse map is not damaged as mods
* are applied to the result string. however...to truly apply them backwards
* we must sort them by offset value! And the simplest way to do that is
* to have a second store that is just a pure array we can read backwards...
* Hence the arylKeywordOffsets array.
*/
Iterator it = getKeywordOffsetsArray().iterator();
while( it.hasNext() ) {
KeywordInstance kwiTemp = (KeywordInstance)it.next();
// System.out.println("[SqlFormattingStrategy.markupTheKewordInstances] About to setApplyFlag on: " + kwiTemp ); //$NON-NLS-1$
// special case: FROM in a DELETE clause should NOT be set applicable
if( kwiTemp.sKeyword.equals( FROM ) && getParseMap().get( SELECT ) == null ) {
// skip this FROM, since it goes with the DELETE
} else {
kwiTemp.setApplyFlag( true );
}
}
}
private String formatTheString( String sSql ) {
String sResultString = null;
StringBuffer sb = new StringBuffer( sSql );
// apply the mods required by each 'applicable' keywordinstance
// do this backwards so the the meaning of the remaining
// offsets is not mangled.
ArrayList arylKwis = getKeywordOffsetsArray();
int iSize = arylKwis.size();
for( int i = iSize - 1; i >= 0; i-- ) {
KeywordInstance kwi = (KeywordInstance)arylKwis.get( i );
if( kwi.isApplicable() ) {
formatKeyword( sb, kwi );
}
}
// convert the string
sResultString = sb.toString();
// System.out.println("[SqlFormattingStrategy.formatTheString] Final string: " + sResultString ); //$NON-NLS-1$
return sResultString;
}
private void formatKeyword( StringBuffer sb, KeywordInstance kwi ) {
// 0. construct replacement string
String sNewString = EMPTY_STRING;
// 1. put the keyword on a new line, depending on preference
if ( kwi.iOffset > 0 && startClausesOnNewLine() ) {
sNewString = NEWLINE;
}
// 2. add in the keyword
sNewString += kwi.sKeyword
+ NEWLINE;
// 3. indent the content, depending on preference
// jh note: the indent makes no sense unless the newline is also requested,
// so I am linking them.
if ( startClausesOnNewLine() && indentClauseContent() ) {
sNewString += INDENT;
}
// mod the buffer
// System.out.println("[SqlFormattingStrategy.formatKeyword] About to replace, kwi.iOffset / kwi.sKeyword.length() / sNewString / sb.length() " //$NON-NLS-1$
// + kwi.iOffset
// + "/" //$NON-NLS-1$
// + kwi.sKeyword.length()
// + "/" //$NON-NLS-1$
// + sNewString
// + "/" //$NON-NLS-1$
// + sb.length() );
//
// System.out.println("[SqlFormattingStrategy.formatKeyword] About to replace: " + sNewString ); //$NON-NLS-1$
sb.replace( kwi.iOffset,
kwi.iOffset + kwi.sKeyword.length(),
sNewString );
}
private boolean startClausesOnNewLine() {
return UiPlugin.getDefault().getPreferenceStore()
.getBoolean( UiConstants.Prefs.START_CLAUSES_ON_NEW_LINE );
}
private boolean indentClauseContent() {
return UiPlugin.getDefault().getPreferenceStore()
.getBoolean( UiConstants.Prefs.INDENT_CLAUSE_CONTENT );
}
// =================================
// inner class: KeywordInstance
// =================================
class KeywordInstance {
String sKeyword;
int iOffset;
boolean bApply;
KeywordInstance( String sKeyword, int iOffset ) {
this.sKeyword = sKeyword;
this.iOffset = iOffset;
}
public String getKeyword() {
return sKeyword;
}
public int getOffset() {
return iOffset;
}
public void setApplyFlag( boolean b ) {
bApply = b;
}
public boolean isApplicable() {
return bApply;
}
}
}