Logo Search packages:      
Sourcecode: henplus version File versions  Download package

SQLStatementSeparator.java

/*
 * This is free software, licensed under the Gnu Public License (GPL)
 * get a copy from <http://www.gnu.org/licenses/gpl.html>
 * $Id: SQLStatementSeparator.java,v 1.17 2004/02/01 14:12:52 hzeller Exp $ 
 * author: Henner Zeller <H.Zeller@acm.org>
 */
package henplus;

import java.util.Stack;

import henplus.property.PropertyHolder;
import henplus.property.BooleanPropertyHolder;

/**
 * Simple parser that separates SQLStatements.
 * Example.
 * <pre>
 *-----------------------
 statementSeparator.append("select * from foo; echo $foobar \n");
 while (statementSeparator.hasNext()) {
      String stmt = statementSeparator.next();
      if (stmt.startsWith("echo")) {
          // is ok, this command works always without ';'
        statementSeparator.consumed();
        System.err.println(stmt.substring("echo ".length());
      }
      else {  // SQL-command. we require a semicolon at the end.
         if (!stmt.charAt(stmt.length()-1) == ';') {
          statementSeparator.cont(); // continue.
         }
         else {
            statementSeparator.consumed();
          SQLExecute(stmt);
         }
      }
 }
 *----------------------- 
 *</pre>
 *
 * FIXME: this is a bit rough and accummulated some ideas. Should be 
 * cleaned up.
 *
 * @author Henner Zeller <H.Zeller@acm.org>
 */
00045 public class SQLStatementSeparator {
    private static final byte NEW_STATEMENT   = 0;
    private static final byte START           = 1;  // statement == start
    private static final byte STATEMENT       = 1;
    private static final byte START_COMMENT   = 3;
    private static final byte COMMENT         = 4;
    private static final byte PRE_END_COMMENT = 5;
    private static final byte START_ANSI      = 6;
    private static final byte ENDLINE_COMMENT = 7;
    private static final byte STRING          = 8;
    private static final byte STRING_QUOTE    = 9;
    private static final byte SQLSTRING       = 10;
    private static final byte SQLSTRING_QUOTE = 11;
    private static final byte STATEMENT_QUOTE = 12;  // backslash in statement
    private static final byte FIRST_SEMICOLON_ON_LINE_SEEN  = 13;
    private static final byte POTENTIAL_END_FOUND = 14;

    private static class ParseState {
      private byte         _state;
      private StringBuffer _inputBuffer;
      private StringBuffer _commandBuffer;
      /*
       * instead of adding new states, we store the
       * fact, that the last 'potential_end_found' was
       * a newline here.
       */
      private boolean      _eolineSeen;

      public ParseState() {
          _eolineSeen = true; // we start with a new line.
          _state = NEW_STATEMENT;
          _inputBuffer = new StringBuffer();
          _commandBuffer = new StringBuffer();
      }
      
      public byte getState() { return _state; }
      public void setState(byte s) { _state = s; }
      public boolean hasNewlineSeen() { return _eolineSeen; }
      public void setNewlineSeen(boolean n) { _eolineSeen = n; }
      public StringBuffer getInputBuffer() { return _inputBuffer; }
      public StringBuffer getCommandBuffer() { return _commandBuffer; }
    };

    private boolean      _removeComments;
    private ParseState   _currentState;
    private Stack        _stateStack;

    public SQLStatementSeparator() {
      _currentState = new ParseState();
      _stateStack = new Stack();
        _removeComments = true;
    }

    /**
     * push the current state and start with a clean one. Use to parse
     * other files (like includes), and continue then with the old
     * state.
     * like 
     * load foobar.sql ; select * from foobar
     */
00105     public void push() {
      _stateStack.push(_currentState);
      _currentState = new ParseState();
    }

    public void pop() {
      _currentState = (ParseState) _stateStack.pop();
    }

    /**
     * add a new line including the '\n' to the input buffer.
     */
00117     public void append(String s) {
      _currentState.getInputBuffer().append(s);
    }
    
    /**
     * discard any input.
     */
00124     public void discard() {
      _currentState.getInputBuffer().setLength(0);
      _currentState.getCommandBuffer().setLength(0);
      _currentState.setState( NEW_STATEMENT );
    }

    /**
     * after having called next(), call cont(), if you are not yet
     * pleased with the result; the parser should read to the next
     * possible end.
     */
00135     public void cont() {
      _currentState.setState( START );
    }

    /**
     * after having called next() and you were pleased with the result
     * call this method to state, that you consumed it.
     */
00143     public void consumed() {
      _currentState.setState( NEW_STATEMENT );
    }

    /**
     * returns true, if the parser can find a complete command that either
     * ends with newline or with ';'
     */
00151     public boolean hasNext() throws IllegalStateException {
      if (_currentState.getState() == POTENTIAL_END_FOUND)
          throw new IllegalStateException ("call cont() or consumed() before hasNext()");
      if (_currentState.getInputBuffer().length() == 0)
          return false;
      parsePartialInput();
      return (_currentState.getState() == POTENTIAL_END_FOUND);
    }

    /**
     * returns the next command; requires to call hasNext() before.
     */
00163     public String next() throws IllegalStateException {
      if (_currentState.getState() != POTENTIAL_END_FOUND)
          throw new IllegalStateException ("next() called without hasNext()");
      return _currentState.getCommandBuffer().toString();
    }

    /**
     * returns a property holder for the remove comments property.
     */
00172     public PropertyHolder getRemoveCommentsProperty() {
        return new RemoveCommentsProperty();
    }

    /**
     * set, whether comments should be removed.
     */
00179     public void removeComments(boolean b) {
        _removeComments = b;
    }

    /**
     * parse partial input and set state to POTENTIAL_END_FOUND if we
     * either reached end-of-line or a semicolon.
     */
00187     private void parsePartialInput () {
      int pos = 0;
      char current;
      byte oldstate = -1;

      // local variables: faster access.
      byte state = _currentState.getState();
      boolean lastEoline = _currentState.hasNewlineSeen();

      final StringBuffer input  = _currentState.getInputBuffer();
      final StringBuffer parsed = _currentState.getCommandBuffer();

      if (state == NEW_STATEMENT) {
          parsed.setLength(0);
          /* skip leading whitespaces of next statement .. */
          while (pos < input.length()
               && Character.isWhitespace (input.charAt(pos))) {
                _currentState.setNewlineSeen( input.charAt(pos) == '\n' );
            ++pos;
          }
          input.delete(0, pos);
          pos = 0;
      }
      
      if (input.length() == 0) {
          state = POTENTIAL_END_FOUND;
      }

        //System.err.println("Startstate: " + state + "; LEOL: " + lastEoline);

      while (state != POTENTIAL_END_FOUND && pos < input.length()) {
          boolean vetoAppend = false;
          boolean reIterate;
          current = input.charAt(pos);
          if (current == '\r') {
            current = '\n'; // canonicalize.
          }

            if (current == '\n') {
                _currentState.setNewlineSeen( true );
            }

          //System.out.print ("Pos: " + pos + "\t");
          do {
            reIterate = false;
            switch (state) {
            case NEW_STATEMENT:
                //case START: START == STATEMENT.
            case STATEMENT :
                if (current == '\n') {
                  state = POTENTIAL_END_FOUND;
                  _currentState.setNewlineSeen(true);
                }

                    /*
                     * special handling of the 'first-two-semicolons-after
                     * a-newline-comment'.
                     */
                else if (_removeComments && lastEoline && current== ';' ) {
                  state = FIRST_SEMICOLON_ON_LINE_SEEN;
                }
                else if (!lastEoline && current==';') {
                  _currentState.setNewlineSeen(false);
                  state = POTENTIAL_END_FOUND;
                }
                else if (_removeComments && current == '/') {
                        state = START_COMMENT;
                    }

                    /*
                     * only if '#' this is the first character, make it
                     * a comment..
                     */
                else if (_removeComments && lastEoline && current == '#') {
                        state = ENDLINE_COMMENT;
                    }
                else if (current == '"')  state = STRING;
                else if (current == '\'') state = SQLSTRING;
                else if (current == '-')  state = START_ANSI;
                else if (current == '\\') state = STATEMENT_QUOTE;
                break;
            case STATEMENT_QUOTE:
                state = STATEMENT;
                break;
            case FIRST_SEMICOLON_ON_LINE_SEEN:
                if (current == ';') state = ENDLINE_COMMENT;
                else {
                  state = POTENTIAL_END_FOUND;
                  current = ';';
                  /*
                   * we've read too much. Reset position.
                   */
                  --pos;
                }
                break;
            case START_COMMENT:
                if (current == '*')         state = COMMENT;
                /*
                 * Endline comment in the style '// comment' is not a
                 * good idea, since many JDBC-urls contain the '//' as
                 * part of the URL .. and this should _not_ be regarded as
                 * commend of course.
                 */
                //else if (current == '/')    state = ENDLINE_COMMENT;
                else { 
                  parsed.append ('/'); 
                  state = STATEMENT; 
                  reIterate = true;
                }
                break;
            case COMMENT:
                if (current == '*') state = PRE_END_COMMENT;
                break;
            case PRE_END_COMMENT:
                if (current == '/')      state = STATEMENT;
                else if (current == '*') state = PRE_END_COMMENT;
                else state = COMMENT;
                break;
            case START_ANSI:
                if (current == '-')        state = ENDLINE_COMMENT;
                else { 
                  parsed.append('-'); 
                  state = STATEMENT; 
                  reIterate = true;
                }
                break;
            case ENDLINE_COMMENT:
                if (current == '\n')      state = POTENTIAL_END_FOUND;
                break;
            case STRING:     
                if (current == '\\') state = STRING_QUOTE;
                else if (current == '"') state = STATEMENT;
                break;
            case SQLSTRING:
                if (current == '\\') state = SQLSTRING_QUOTE;
                if (current == '\'') state = STATEMENT;
                break;
            case STRING_QUOTE:
                if (current == '\"') parsed.append("\"");
                vetoAppend = (current == '\n');
                state = STRING;
                break;
            case SQLSTRING_QUOTE:
                if (current == '\'') parsed.append("'");
                vetoAppend = (current == '\n');
                state = SQLSTRING;
                break;
            }
          }
          while (reIterate);

          /* append to parsed; ignore comments */
          if (!vetoAppend
            && ((state == STATEMENT && oldstate != PRE_END_COMMENT)
                || state == NEW_STATEMENT
                || state == STATEMENT_QUOTE
                || state == STRING
                || state == SQLSTRING
                || state == POTENTIAL_END_FOUND)) {
            parsed.append(current);
          }
          
          oldstate = state;
          pos++;
          /*
           * we maintain the state of 'just seen newline' as long
           * as we only skip whitespaces..
           */
          lastEoline &= Character.isWhitespace(current);
      }
      // we reached: POTENTIAL_END_FOUND. Store the rest, that
      // has not been parsed in the input-buffer.
      input.delete(0, pos);
      _currentState.setState(state);
    }

    private class RemoveCommentsProperty extends BooleanPropertyHolder {

        public RemoveCommentsProperty() {
            super(SQLStatementSeparator.this._removeComments);
        }

        public void booleanPropertyChanged(boolean value){
            removeComments( value );
        }
        
        public String getDefaultValue() {
            return "on";
        }

        /**
         * return a short descriptive string.
         */
        public String getShortDescription() {
            return "switches the removal of SQL-comments";
        }
        
        public String getLongDescription() {
            String dsc;
            dsc= "\tSwitch the behaviour to remove all comments\n"
                +"\tfound in the string sent to the database. Some databases\n"
                +"\tcan not handle comments in JDBC-Strings.\n\nValues\n"
                
                +"\ttrue\n"
                +"\t\tDEFAULT. Remove all SQL92 comments found in the given\n"
                +"\t\tSQL Strings before sending them to the database.\n\n"
                
                +"\tfalse\n"
                +"\t\tSwitch off the default behaviour to remove all\n"
                +"\t\tcomments found in the string sent to the database.\n"
                +"\t\tUsually, this is not necessary, but there are\n"
                +"\t\tconditions where comments actually convey a meaning\n"
                +"\t\tto the database. For instance hinting in oracle works\n"
                +"\t\twith comments, like\n"
                +"\t\t   select /*+ index(foo,foo_fk_idx) */ ....\n"
                +"\t\t..so removing of comments should be off in this case";
            return dsc;
        }
    }
}

/*
 * Local variables:
 * c-basic-offset: 4
 * compile-command: "ant -emacs -find build.xml"
 * End:
 */

Generated by  Doxygen 1.6.0   Back to index