SequenceExecutor.java

/***************************************************************************
   Copyright 2014 Emily Estes

   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.
***************************************************************************/
package net.metanotion.sql;


import java.io.Reader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;

/** This class will execute a list of sql statements against a connection, and split a reader into a list of sql statements. */
public final class SequenceExecutor {
	/** This method is a helper method to allow classes to implement {@link net.metanotion.sql.SchemaGenerator} with a simple
	method call. This method uses a default separator of ";"
		@param klazz The class to load the schema file from. The resource it will look for has the same name as the
			short name of the class followed by ".schema.sql".
		@return an String iterator that enumerates the sql statements in the file.
	*/
	public static Iterator<String> openSchema(final Class klazz) {
		return openSchema(klazz, ";");
	}

	/** This method is a helper method to allow classes to implement {@link net.metanotion.sql.SchemaGenerator} with a simple
	method call.
		@param klazz The class to load the schema file from. The resource it will look for has the same name as the
			short name of the class followed by ".schema.sql".
		@param separator The string that separates SQL statements.
		@return an String iterator that enumerates the sql statements in the file.
	*/
	public static Iterator<String> openSchema(final Class klazz, final String separator) {
		try (final InputStream stream =
				klazz.getClassLoader().getResourceAsStream(klazz.getName().replace('.', '/') + ".schema.sql")) {
			final Iterator<String> it =
				SequenceExecutor.readerSequencer(new InputStreamReader(stream, StandardCharsets.UTF_8), separator);
			final ArrayList<String> list = new ArrayList<>();
			while(it.hasNext()) {
				list.add(it.next());
			}
			return list.iterator();
		} catch (final IOException ioe) {
			throw new RuntimeException(ioe);
		}
	}

	/** Execute a list of sql statements against a connection.
		@param conn The connection to execute the statements against.
		@param sqlSequence The list of sql statements to execute.
		@return The number of successful results.
		@throws SQLException if one of the statements fails.
	*/
	public static int doSequence(final Connection conn, final Iterator<String> sqlSequence) throws SQLException {
		int count = 0;
		while(sqlSequence.hasNext()) {
			final String sql = sqlSequence.next();
			try (final PreparedStatement stmt = conn.prepareStatement(sql)) {
				if(stmt.execute()) { count++; }
			}
		}
		return count;
	}

	private static final class Sequencer implements Iterator<String> {
		private final Reader reader;
		private final String separator;
		private final char[] sepChars;

		public Sequencer(final Reader reader, final String separator) {
			this.reader = reader;
			this.separator = separator;
			this.sepChars = separator.toCharArray();
		}

		private boolean buffered = false;
		private StringBuilder sb = new StringBuilder("");
		private String sql = null;

		private boolean fillBuffer() {
			if(buffered) { return true; }
			if(sb == null) { return false; }
			final char[] buffer = new char[sepChars.length];
			try {
				int read = reader.read(buffer);
				while(read > 0) {
					if(String.valueOf(buffer).equals(separator)) {
						sb.append(String.valueOf(buffer));
						sql = sb.toString();
						buffered = true;
						sb = new StringBuilder("");
						return true;
					}
					sb.append(buffer[0]);
					for(int i=1; i < buffer.length; i++) {
						buffer[i-1] = buffer[i];
					}
					read = reader.read(buffer, buffer.length - 1, 1);
				}
				for(int i=0; i < (buffer.length - 1); i++) {
					sb.append(buffer[i]);
				}
			} catch (final IOException ioe) { throw new RuntimeException(ioe); }
			try {
				reader.close();
			} catch (final IOException ioe) { throw new RuntimeException(ioe); }
			sql = sb.toString();
			sb = null;
			if(sql.length() == 0) { return false; }
			buffered = true;
			return true;
		}
		@Override public boolean hasNext() {
			return fillBuffer();
		}
		@Override public String next() {
			if(fillBuffer()) {
				buffered = false;
				return sql;
			} else {
				throw new NoSuchElementException();
			}
		}
		@Override public void remove() { throw new UnsupportedOperationException(); }
	}

	/** Parse the input from a reader into sql statements using the default "statement separator"(<code>;\n</code>).
		This call is equivalent to {@link #readerSequencer}(reader, ";\n");
		@param reader The file to parse.
		@return an Iterator which interprets the file into a list of sql statements.
	*/
	public static Iterator<String> readerSequencer(final Reader reader) { return readerSequencer(reader, ";\n"); }

	/** Parse the input from a reader into sql statements using the provided statement separator.
		@param reader The file to parse.
		@param separator The string used to sequence sql statements.
		@return an Iterator which interprets the file into a list of sql statements.
	*/
	public static Iterator<String> readerSequencer(final Reader reader, final String separator) {
		return new Sequencer(reader, separator);
	}
}