/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.engine.jdbc.spi;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.hibernate.JDBCException;
import org.hibernate.exception.internal.SQLStateConverter;
import org.hibernate.exception.spi.SQLExceptionConverter;
import org.hibernate.exception.spi.ViolatedConstraintNameExtracter;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;

import org.jboss.logging.Logger;
import org.jboss.logging.Logger.Level;

/**
 * Helper for handling SQLExceptions in various manners.
 *
 * @author Steve Ebersole
 */
public class SqlExceptionHelper {
	private static final CoreMessageLogger LOG = Logger.getMessageLogger(
			CoreMessageLogger.class,
			SqlExceptionHelper.class.getName()
	);

	private static final String DEFAULT_EXCEPTION_MSG = "SQL Exception";
	private static final String DEFAULT_WARNING_MSG = "SQL Warning";
	private final boolean logWarnings;

	private static final SQLExceptionConverter DEFAULT_CONVERTER = new SQLStateConverter(
			new ViolatedConstraintNameExtracter() {
				public String extractConstraintName(SQLException e) {
					return null;
				}
			}
	);

	private SQLExceptionConverter sqlExceptionConverter;

	/**
	 * Create an exception helper with a default exception converter.
	 */
	public SqlExceptionHelper( boolean logWarnings) {
		this( DEFAULT_CONVERTER, logWarnings );
	}

	/**
	 * Create an exception helper with a specific exception converter.
	 *
	 * @param sqlExceptionConverter The exception converter to use.
	 */
	public SqlExceptionHelper(SQLExceptionConverter sqlExceptionConverter, boolean logWarnings) {
		this.sqlExceptionConverter = sqlExceptionConverter;
		this.logWarnings = logWarnings;
	}

	/**
	 * Access the current exception converter being used internally.
	 *
	 * @return The current exception converter.
	 */
	public SQLExceptionConverter getSqlExceptionConverter() {
		return sqlExceptionConverter;
	}

	/**
	 * Inject the exception converter to use.
	 * <p/>
	 * NOTE : <tt>null</tt> is allowed and signifies to use the default.
	 *
	 * @param sqlExceptionConverter The converter to use.
	 */
	public void setSqlExceptionConverter(SQLExceptionConverter sqlExceptionConverter) {
		this.sqlExceptionConverter = ( sqlExceptionConverter == null ? DEFAULT_CONVERTER : sqlExceptionConverter );
	}

	// SQLException ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/**
	 * Convert an SQLException using the current converter, doing some logging first.
	 *
	 * @param sqlException The exception to convert
	 * @param message An error message.
	 *
	 * @return The converted exception
	 */
	public JDBCException convert(SQLException sqlException, String message) {
		return convert( sqlException, message, "n/a" );
	}

	/**
	 * Convert an SQLException using the current converter, doing some logging first.
	 *
	 * @param sqlException The exception to convert
	 * @param message An error message.
	 * @param sql The SQL being executed when the exception occurred
	 *
	 * @return The converted exception
	 */
	public JDBCException convert(SQLException sqlException, String message, String sql) {
		logExceptions( sqlException, message + " [" + sql + "]" );
		return sqlExceptionConverter.convert( sqlException, message, sql );
	}

	/**
	 * Log the given (and any nested) exception.
	 *
	 * @param sqlException The exception to log
	 * @param message The message text to use as a preamble.
	 */
	public void logExceptions(SQLException sqlException, String message) {
		if ( LOG.isEnabled( Level.ERROR ) ) {
			if ( LOG.isDebugEnabled() ) {
				message = StringHelper.isNotEmpty( message ) ? message : DEFAULT_EXCEPTION_MSG;
				LOG.debug( message, sqlException );
			}
			final boolean warnEnabled = LOG.isEnabled( Level.WARN );

			List<String> previousWarnMessages = new ArrayList<>();
			List<String> previousErrorMessages = new ArrayList<>();

			while ( sqlException != null ) {
				if ( warnEnabled ) {
					String warnMessage = "SQL Error: " + sqlException.getErrorCode() + ", SQLState: " + sqlException.getSQLState();
					if ( !previousWarnMessages.contains( warnMessage ) ) {
						LOG.warn( warnMessage );
						previousWarnMessages.add( warnMessage );
					}
				}
				if ( !previousErrorMessages.contains( sqlException.getMessage() ) ) {
					LOG.error( sqlException.getMessage() );
					previousErrorMessages.add( sqlException.getMessage() );
				}
				sqlException = sqlException.getNextException();
			}
		}
	}

	// SQLWarning ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/**
	 * Contract for handling {@link SQLWarning warnings}
	 */
	public interface WarningHandler {
		/**
		 * Should processing be done? Allows short-circuiting if not.
		 *
		 * @return True to process warnings, false otherwise.
		 */
		boolean doProcess();

		/**
		 * Prepare for processing of a {@link SQLWarning warning} stack.
		 * <p/>
		 * Note that the warning here is also the first passed to {@link #handleWarning}
		 *
		 * @param warning The first warning in the stack.
		 */
		void prepare(SQLWarning warning);

		/**
		 * Handle an individual warning in the stack.
		 *
		 * @param warning The warning to handle.
		 */
		void handleWarning(SQLWarning warning);
	}

	/**
	 * Basic support for {@link WarningHandler} implementations which handle {@link SQLWarning warnings}
	 */
	public abstract static class WarningHandlerLoggingSupport implements WarningHandler {
		@Override
		public final void handleWarning(SQLWarning warning) {
			logWarning(
					"SQL Warning Code: " + warning.getErrorCode() + ", SQLState: " + warning.getSQLState(),
					warning.getMessage()
			);
		}

		/**
		 * Delegate to log common details of a {@link SQLWarning warning}
		 *
		 * @param description A description of the warning
		 * @param message The warning message
		 */
		protected abstract void logWarning(String description, String message);
	}

	/**
	 * Standard SQLWarning handler for logging warnings
	 */
	public static class StandardWarningHandler extends WarningHandlerLoggingSupport {
		private final String introMessage;

		/**
		 * Creates a StandardWarningHandler
		 *
		 * @param introMessage The introduction message for the hierarchy
		 */
		public StandardWarningHandler(String introMessage) {
			this.introMessage = introMessage;
		}

		@Override
		public boolean doProcess() {
			return LOG.isEnabled( Level.WARN );
		}

		@Override
		public void prepare(SQLWarning warning) {
			LOG.debug( introMessage, warning );
		}

		@Override
		protected void logWarning(
				String description,
				String message) {
			LOG.warn( description );
			LOG.warn( message );
		}
	}

	/**
	 * Static access to the standard handler for logging warnings
	 */
	public static final StandardWarningHandler STANDARD_WARNING_HANDLER = new StandardWarningHandler(
			DEFAULT_WARNING_MSG
	);

	/**
	 * Generic algorithm to walk the hierarchy of SQLWarnings
	 *
	 * @param warning The warning to walk
	 * @param handler The handler
	 */
	public void walkWarnings(
			SQLWarning warning,
			WarningHandler handler) {
		if ( warning == null || !handler.doProcess() ) {
			return;
		}
		handler.prepare( warning );
		while ( warning != null ) {
			handler.handleWarning( warning );
			warning = warning.getNextWarning();
		}
	}

	/**
	 * Standard (legacy) behavior for logging warnings associated with a JDBC {@link Connection} and clearing them.
	 * <p/>
	 * Calls {@link #handleAndClearWarnings(Connection, WarningHandler)} using {@link #STANDARD_WARNING_HANDLER}
	 *
	 * @param connection The JDBC connection potentially containing warnings
	 */
	public void logAndClearWarnings(Connection connection) {
		handleAndClearWarnings( connection, STANDARD_WARNING_HANDLER );
	}

	public void logAndClearWarnings(Statement statement) {
		handleAndClearWarnings( statement, STANDARD_WARNING_HANDLER );
	}

	/**
	 * General purpose handling of warnings associated with a JDBC {@link Connection}.
	 *
	 * @param connection The JDBC connection potentially containing warnings
	 * @param handler The handler for each individual warning in the stack.
	 *
	 * @see #walkWarnings
	 */
	@SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
	public void handleAndClearWarnings(
			Connection connection,
			WarningHandler handler) {
		try {
			if ( logWarnings ) {
				walkWarnings( connection.getWarnings(), handler );
			}
		}
		catch (SQLException sqle) {
			// workaround for WebLogic
			LOG.debug( "could not log warnings", sqle );
		}
		try {
			// Sybase fail if we don't do that, sigh...
			connection.clearWarnings();
		}
		catch (SQLException sqle) {
			LOG.debug( "could not clear warnings", sqle );
		}
	}

	/**
	 * General purpose handling of warnings associated with a JDBC {@link Statement}.
	 *
	 * @param statement The JDBC statement potentially containing warnings
	 * @param handler The handler for each individual warning in the stack.
	 *
	 * @see #walkWarnings
	 */
	@SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
	public void handleAndClearWarnings(
			Statement statement,
			WarningHandler handler) {
		// See HHH-9174.  Statement#getWarnings can be an expensive call for many JDBC libs.  Don't do it unless
		// the log level would actually allow a warning to be logged.
		if ( logWarnings ) {
			try {
				walkWarnings( statement.getWarnings(), handler );
			}
			catch (SQLException sqlException) {
				// workaround for WebLogic
				LOG.debug( "could not log warnings", sqlException );
			}
		}
		try {
			// Sybase fail if we don't do that, sigh...
			statement.clearWarnings();
		}
		catch (SQLException sqle) {
			LOG.debug( "could not clear warnings", sqle );
		}
	}
}
