DerbyScriptRunner.java

/* 
 * Copyright 2015 Development Entropy (deventropy.org) Contributors
 * 
 * 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 org.deventropy.junithelper.derby.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.charset.Charset;
import java.sql.Connection;

import org.apache.commons.io.IOUtils;
import org.apache.derby.tools.ij;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.deventropy.shared.utils.ArgumentCheck;
import org.deventropy.shared.utils.UrlResourceUtil;

/**
 * Executes a SQL script on the given Derby embedded instance connection.
 * 
 * <p>The script uses a single character set for reading the scripts as well as writing the logs. The class has a
 * {@link #setDefaultScriptLogStream(OutputStream) Default Log Stream} which it uses when no log stream or file is
 * specified along with the script for execution ({@link #executeScript(String)}). A log stream may be specified with
 * the script resource using the {@link #executeScript(String, File)} or
 * {@link #executeScript(String, OutputStream, boolean)} methods.
 * 
 * <p>The script resources provided to this class should be in the format supported by {@link UrlResourceUtil}.
 * 
 * @author Bindul Bhowmik
 */
public class DerbyScriptRunner {
	
	/**
	 * Default system character set to use if none is specified by the user.
	 */
	public static final String DEFAULT_CHARSET = Charset.defaultCharset().name();
	
	private final Logger log = LogManager.getLogger();
	
	/**
	 * DB connection to use
	 */
	private final Connection dbConnection;
	
	/**
	 * Character set to use for this script runner
	 */
	private final String charset;
	
	/**
	 * Default script log stream if none is specified with the script.
	 */
	private OutputStream defaultScriptLogStream;
	
	/**
	 * Initializes a new script runner.
	 * 
	 * @param dbConnection The database connection to run the scripts on.
	 * @param charset Character set to use for reading the script and writing the log
	 */
	public DerbyScriptRunner (final Connection dbConnection, final String charset) {
		this.dbConnection = dbConnection;
		this.charset = charset;
		this.defaultScriptLogStream = DerbyUtils.DEV_NULL;
	}
	
	/**
	 * Initializes a new script runner for the connection. Uses the {@link #DEFAULT_CHARSET} as the character set.
	 * 
	 * @param dbConnection The database connection to run the scripts on.
	 */
	public DerbyScriptRunner (final Connection dbConnection) {
		this(dbConnection, DEFAULT_CHARSET);
	}
	
	/**
	 * Sets the log stream to log results to. This can be overridden on a per script basis, and if not set defaults to
	 * {@link DerbyUtils#DEV_NULL} stream, which simply ignores all output.
	 * 
	 * <p>Data will be written to this stream using the character set set in the constructor.
	 * 
	 * @param defaultScriptLogStream The default script log stream
	 */
	public void setDefaultScriptLogStream (final OutputStream defaultScriptLogStream) {
		ArgumentCheck.notNull(defaultScriptLogStream, "Default Script Log Stream");
		this.defaultScriptLogStream = defaultScriptLogStream;
	}
	
	/**
	 * Executes the given script. The script run logs are written to the
	 * {@link #setDefaultScriptLogStream(OutputStream)}.
	 * 
	 * @param scriptResource The script source; should be in a format compatible with {@linkplain UrlResourceUtil}
	 * @return <code>0</code> if the execution succeeds without any errors; <code>non zero</code> value otherwise.
	 * @throws IOException Error reading script file, or writing to the log file.
	 */
	public int executeScript (final String scriptResource) throws IOException {
		return executeScript(scriptResource, defaultScriptLogStream, false);
	}
	
	/**
	 * Executes the given script. The script run logs are written to the provided <code>scriptLogFile</code>.
	 * 
	 * @param scriptResource The script source; should be in a format compatible with {@linkplain UrlResourceUtil}
	 * @param scriptLogFile The file to which the output logs are written. If the file already exists, it is appended to
	 * @return <code>0</code> if the execution succeeds without any errors; <code>non zero</code> value otherwise.
	 * @throws IOException Error reading script file, or writing to the log file.
	 */
	public int executeScript (final String scriptResource, final File scriptLogFile) throws IOException {
		try {
			final OutputStream scriptLogStream = new FileOutputStream(scriptLogFile, true);
			return executeScript(scriptResource, scriptLogStream, true);
		} catch (FileNotFoundException e) {
			log.error("Error opening script log stream: {}", scriptLogFile);
			throw new IOException("Error opening script log stream", e);
		}
	}
	
	/**
	 * Executes the given script. The script run logs are written to the provided <code>scriptLogStream</code>.
	 * 
	 * @param script The script source; should be in a format compatible with {@linkplain UrlResourceUtil}
	 * @param scriptLogStream The stream to which the output logs are written.
	 * @param closeScriptLogStream If set to <code>true</code>, the log stream is closed at the end of the execution.
	 * @return <code>0</code> if the execution succeeds without any errors; <code>non zero</code> value otherwise.
	 * @throws IOException Error reading script file, or writing to the log file.
	 */
	public int executeScript (final String script, final OutputStream scriptLogStream,
			final boolean closeScriptLogStream) throws IOException {

		InputStream scriptStream = null;

		try {

			final URL scriptUrl = UrlResourceUtil.getUrl(script);
			scriptStream = scriptUrl.openStream();
	
			log.debug("Executing script: {}", script);
			final int exceptionCount = ij.runScript(dbConnection, scriptStream, charset,
					scriptLogStream, charset);
			if (exceptionCount > 0) {
				log.warn("Error executing script {}. See output for details", script);
			}
			return exceptionCount;

		} catch (UnsupportedEncodingException e) {
			log.warn("Incompatible encoding for script file. Current characterset {0}", charset);
			throw e;
		} catch (IOException e) {
			log.warn("Error opening or reading script file: {0}", script);
			throw e;
		} finally {
			IOUtils.closeQuietly(scriptLogStream);
			IOUtils.closeQuietly(scriptStream);
		}
	}
}