/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* 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.
*
* Initial developer(s): Robert Hodges
* Contributor(s):
*/
package com.continuent.tungsten.replicator.database;
/**
* Implements a state machine-based algorithm to clean up MySQL queries for
* regex parsing by eliminating leading white space, stripping ordinary
* comments, and removing comment characters around MySQL '/*!NNNNN' style
* comments generated by mysqldump.
*
* @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a>
* @version 1.0
*/
public class MySQLOperationStringBuilder
{
// Parameters.
private int maxOutputLength;
// Input string and state thereof.
private String inputString;
private int inputLength;
private int inputIndex;
// Output string buffer.
private StringBuffer outputString = new StringBuffer();
/** Create instance. */
public MySQLOperationStringBuilder(int maxOutputLength)
{
this.maxOutputLength = maxOutputLength;
}
/** Build a string for parsing. */
public String build(String input)
{
// Set up parameter for build operation.
inputString = input;
inputIndex = 0;
inputLength = input.length();
outputString = new StringBuffer();
// Loop until we run out of input or output.
char nextChar;
while ((nextChar = get()) != 0
&& outputString.length() < maxOutputLength)
{
if (nextChar == '/')
{
if (inputStartsWith("*!"))
{
// Look ahead to ensure we have a bang comment.
String nextChars = peek(7);
boolean haveBangComment = false;
// Peek returns null if there is not enough data to be read.
if (nextChars != null && nextChars.length() == 7)
{
haveBangComment = true;
for (int i = 2; i < nextChars.length(); i++)
{
if (!Character.isDigit(nextChars.charAt(i)))
{
haveBangComment = false;
break;
}
}
}
if (haveBangComment)
{
// Strip the enclosing comment characters and add
// content.
skip(7);
String contents = getToDelimiter("*/");
if (contents != null)
{
put(contents);
skip(2);
}
}
else
{
// No comment after all, so just add it.
put(nextChar);
}
}
else if (inputStartsWith("*"))
{
// Skip the entire comment.
if (getToDelimiter("*/") != null)
skip(2);
}
else
{
// Just add it.
put(nextChar);
}
}
else if (nextChar == '-')
{
if (inputStartsWith("-"))
{
// Ensure there is whitespace or end-of-input after the
// comment. MySQL requires whitespace.
String tail = peek(2);
if (tail == null)
{
// Trailing comment, so we are done. It will be dropped.
break;
}
else
{
// Look for whitespace following "--" comment.
if (Character.isWhitespace(tail.charAt(1)))
{
// Skip the comment and put in a space instead.
String buf = getToEndOfLine();
skip(buf.length());
put(' ');
}
else
put(nextChar);
}
}
else
{
// Just add it.
put(nextChar);
}
}
else if (nextChar == '\n')
{
// Convert to space.
put(" ");
}
else
{
put(nextChar);
}
}
// Return what we found.
return outputString.toString();
}
// Returns the next character in the input string provided we
// have one.
private char get()
{
if (inputIndex < inputLength)
return inputString.charAt(inputIndex++);
else
return 0;
}
// Preview the next N characters or return null if there are not enough of
// them.
private String peek(int n)
{
int endIndex = inputIndex + n;
if (endIndex <= inputLength)
return inputString.substring(inputIndex, endIndex);
else
return null;
}
// Return string characters up to but not including delimiter.
private String getToDelimiter(String delimiter)
{
String content = null;
if (inputIndex < inputLength)
{
int delimiterIndex = inputString.indexOf(delimiter, inputIndex);
if (delimiterIndex > -1)
{
content = inputString.substring(inputIndex, delimiterIndex);
inputIndex = delimiterIndex;
}
}
return content;
}
// Skip characters up to but not including end-of-line.
private String getToEndOfLine()
{
String lineSeparator = System.getProperty("line.separator");
int endIndex = inputIndex;
while (endIndex < inputLength)
{
char c = inputString.charAt(endIndex++);
if (lineSeparator.indexOf(c) > -1)
break;
}
return inputString.substring(inputIndex, endIndex);
}
// Returns true if the input starts with the argument at the
// current index.
private boolean inputStartsWith(String prefix)
{
if (inputIndex < inputLength)
return inputString.startsWith(prefix, inputIndex);
else
return false;
}
// Skip over the desired number of characters.
private void skip(int n)
{
if (inputIndex < inputLength)
inputIndex += n;
}
// Adds a character to the output.
private void put(char c)
{
if (!Character.isWhitespace(c) || outputString.length() != 0)
{
outputString.append(c);
}
}
// Adds a string to the output.
private void put(String s)
{
for (int i = 0; i < s.length(); i++)
put(s.charAt(i));
}
}