All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.numdata.oss.db.DbServices Maven / Gradle / Ivy

/*
 * Copyright (c) 2017, Numdata BV, The Netherlands.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Numdata nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL NUMDATA BV BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.numdata.oss.db;

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.Date;
import javax.sql.*;

import com.numdata.oss.*;
import com.numdata.oss.db.JdbcTools.*;
import com.numdata.oss.ensemble.*;
import com.numdata.oss.log.*;
import org.intellij.lang.annotations.*;
import org.jetbrains.annotations.*;

/**
 * This class provides a database abstraction layer that should help Java
 * developers with accessing database records through the java introspection
 * mechanism.
 *
 * Database access is provided by a {@link DataSource}.
 *
 * @author Peter S. Heijnen
 */
@SuppressWarnings( { "JDBCPrepareStatementWithNonConstantString", "JDBCExecuteWithNonConstantString", "unused" } )
public class DbServices
{
	/**
	 * Generate warning in log about a slow query when a query takes longer than
	 * this number of seconds to execute (and process).
	 */
	private static final double SLOW_QUERY_THRESHOLD;

	static
	{
		double slowQueryThreshold = 3.0;
		try
		{
			final String value = System.getProperty( "slow.query.threshold" );
			if ( value != null )
			{
				slowQueryThreshold = Double.parseDouble( value );
			}
		}
		catch ( final RuntimeException e )
		{
			/* ignore no access to system property */
		}
		SLOW_QUERY_THRESHOLD = slowQueryThreshold;
	}

	/**
	 * SQL dialect to use.
	 */
	public enum SqlDialect
	{
		/**
		 * MySQL / MariaDb.
		 */
		MYSQL,

		/**
		 * HSQLDB.
		 */
		HSQLDB,

		/**
		 * Microsoft SQL server.
		 */
		MSSQL,

		/**
		 * Oracle.
		 */
		ORACLE,
	}

	/**
	 * Log used for database related messages.
	 */
	private static final ClassLogger LOG = ClassLogger.getFor( DbServices.class );

	/**
	 * This special object is used to store a 'NOW' in the database. This must
	 * be used to set a correct time in the database (the local system time
	 * should not be used!). The value is the earliest possible value for a
	 * {@code Date} object, roughly 300 million years BC.
	 */
	public static final Date NOW = new Date( Long.MIN_VALUE );

	/**
	 * Database pool to use for database services.
	 */
	@NotNull
	protected final DataSource _dataSource;

	/**
	 * Connection for the current transaction.
	 */
	@NotNull
	private final ThreadLocal _transactionConnection = new ThreadLocal();

	/**
	 * Cached/registered {@link ClassHandler} instances.
	 */
	private static final Map, ClassHandler> CLASS_HANDLERS = new HashMap, ClassHandler>();

	/**
	 * Dialect used by the database.
	 */
	@NotNull
	private SqlDialect _sqlDialect;

	/**
	 * Create database services using the specified data source.
	 *
	 * @param dataSource Data source to use.
	 */
	public DbServices( @NotNull final DataSource dataSource )
	{
		this( dataSource, SqlDialect.MYSQL );
	}

	/**
	 * Create database services using the specified data source.
	 *
	 * @param dataSource Data source to use.
	 * @param sqlDialect Dialect used by the database.
	 */
	public DbServices( @NotNull final DataSource dataSource, @NotNull final SqlDialect sqlDialect )
	{
		_dataSource = dataSource;
		_sqlDialect = sqlDialect;
	}

	/**
	 * Gets dialect used by the database.
	 *
	 * @return Dialect used by the database.
	 */
	@NotNull
	public SqlDialect getSqlDialect()
	{
		return _sqlDialect;
	}

	/**
	 * Sets dialect used by the database.
	 *
	 * @param sqlDialect Dialect used by the database.
	 */
	public void setSqlDialect( @NotNull final SqlDialect sqlDialect )
	{
		_sqlDialect = sqlDialect;
	}

	/**
	 * Get {@link ClassHandler} for a class.
	 *
	 * @param clazz Class to get handler for.
	 *
	 * @return {@link ClassHandler} for class.
	 */
	@NotNull
	protected static ClassHandler getClassHandler( @NotNull final Class clazz )
	{
		ClassHandler result;

		final Map, ClassHandler> classHandlers = CLASS_HANDLERS;
		//noinspection SynchronizationOnLocalVariableOrMethodParameter
		synchronized ( classHandlers )
		{
			result = classHandlers.get( clazz );
			if ( result == null )
			{
				final TableRecord dbClass = clazz.getAnnotation( TableRecord.class );
				if ( dbClass != null )
				{
					final String handlerImpl = dbClass.handlerImpl();
					if ( !handlerImpl.isEmpty() )
					{
						try
						{
							final Class handlerClass = Class.forName( handlerImpl );
							result = (ClassHandler)handlerClass.getConstructor().newInstance();
						}
						catch ( final ClassNotFoundException e )
						{
							throw new RuntimeException( "Handler '" + handlerImpl + "' not found for class '" + clazz.getName() + '\'', e );
						}
						catch ( final InstantiationException e )
						{
							throw new RuntimeException( "Failed to initialize handler '" + handlerImpl + "' for class '" + clazz.getName() + '\'', e );
						}
						catch ( final IllegalAccessException e )
						{
							throw new RuntimeException( "Access denied to handler '" + handlerImpl + "' for class '" + clazz.getName() + '\'', e );
						}
						catch ( final NoSuchMethodException e )
						{
							throw new RuntimeException( "Missing default '" + handlerImpl + "' handler constructor for class '" + clazz.getName() + '\'', e );
						}
						catch ( final InvocationTargetException e )
						{
							throw new RuntimeException( "Failed to initialize handler '" + handlerImpl + "' for class '" + clazz.getName() + '\'', e );
						}
					}
					else
					{
						result = new ReflectedClassHandler( clazz );
					}
				}
				else
				{
					result = new ReflectedClassHandler( clazz );
				}

				classHandlers.put( clazz, result );
			}
		}

		return result;
	}

	/**
	 * Get data source.
	 *
	 * @return Data source.
	 */
	@NotNull
	public DataSource getDataSource()
	{
		return _dataSource;
	}

	/**
	 * Get table name for a query.
	 *
	 * @param query Query to get table name for.
	 *
	 * @return Table name.
	 */
	@NotNull
	protected String getTableName( @NotNull final AbstractQuery query )
	{
		String result = query.getTableName();
		if ( result == null )
		{
			final Class tableClass = query.getTableClass();
			if ( tableClass == null )
			{
				throw new IllegalArgumentException( "Table name or class must be set" );
			}

			result = getTableName( tableClass );
		}
		return result;
	}

	/**
	 * Get table name for a class.
	 *
	 * @param tableClass Table class to get table name for.
	 *
	 * @return Table name.
	 */
	@NotNull
	public String getTableName( @NotNull final Class tableClass )
	{
		final ClassHandler classHandler = getClassHandler( tableClass );
		return classHandler.getTableName();
	}

	/**
	 * Get record ID of the given record object.
	 *
	 * @param record Record to get id from.
	 *
	 * @return Record id; -1 if record has no id (or the id is actually -1).
	 */
	public long getRecordId( @NotNull final Object record )
	{
		final ClassHandler classHandler = getClassHandler( record.getClass() );
		return classHandler.hasRecordId() ? classHandler.getRecordId( record ) : -1L;
	}

	/**
	 * Get SQL function/variable used to get the current date and time. This
	 * depends on the type of SQL server.
	 *
	 * @return String containing SQL function/variable to get the current date
	 * and time.
	 */
	@NotNull
	public String getCurrentDateTimeFunction()
	{
		return "NOW()";
	}

	/**
	 * Create table in database.
	 *
	 * @param tableClass Class that defines the table.
	 *
	 * @throws IllegalArgumentException if reflection problems occur.
	 * @throws NullPointerException if {@code tableClass} is {@code null}.
	 * @throws SQLException the query could not be executed (due to a database
	 * error or invalid query).
	 */
	public void createTable( final Class tableClass )
	throws SQLException
	{
		if ( LOG.isDebugEnabled() )
		{
			LOG.debug( "createTable( " + tableClass + " )" );
		}

		final DataSource dataSource = getDataSource();

		final ClassHandler handler = getClassHandler( tableClass );
		final String createStatement = handler.getCreateStatement();

		JdbcTools.executeUpdate( dataSource, createStatement );
	}

	/**
	 * Drop table in database.
	 *
	 * @param tableClass Class that defines the table.
	 *
	 * @throws SQLException the table could not be dropped (due to a database
	 * error or invalid query).
	 */
	public void dropTable( final Class tableClass )
	throws SQLException
	{
		if ( LOG.isDebugEnabled() )
		{
			LOG.debug( "dropTable( " + tableClass + " )" );
		}

		final DataSource dataSource = getDataSource();
		final String tableName = getTableName( tableClass );

		JdbcTools.executeUpdate( dataSource, "DROP TABLE " + tableName );
	}

	/**
	 * Returns whether a table exists in the database for the given table
	 * class.
	 *
	 * @param dbClass Table class.
	 *
	 * @return {@code true} if the table exists; {@code false} if it doesn't or
	 * an error occurred.
	 *
	 * @throws SQLException if an error occurs while accessing the database.
	 */
	public boolean tableExists( @NotNull final Class dbClass )
	throws SQLException
	{
		final DataSource dataSource = getDataSource();
		final String tableName = getTableName( dbClass );

		return JdbcTools.tableExists( dataSource, tableName );
	}

	/**
	 * Execute delete query.
	 *
	 * @param deleteQuery {@link DeleteQuery} to execute.
	 *
	 * @return Number of rows that were deleted (may be {@code 0}).
	 *
	 * @throws SQLException the query could not be executed (due to a database
	 * error or invalid query).
	 */
	public int executeDelete( @NotNull final DeleteQuery deleteQuery )
	throws SQLException
	{
		final DataSource dataSource = getDataSource();
		final String tableName = getTableName( deleteQuery );
		final String queryString = deleteQuery.getQueryString( tableName );
		final Object[] queryParameters = deleteQuery.getQueryParameters();

		try
		{
			final long start = System.nanoTime();
			final int result = JdbcTools.executeUpdate( dataSource, queryString, queryParameters );
			if ( LOG.isDebugEnabled() )
			{
				LOG.debug( "executeDelete() time=" + ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0 + "s, query='" + queryString + "', parameters=" + Arrays.toString( queryParameters ) );
			}
			return result;
		}
		catch ( final SQLTransientException e )
		{
			if ( LOG.isDebugEnabled() )
			{
				LOG.debug( "executeDelete() FAILED query '" + queryString + "' with parameters " + Arrays.toString( queryParameters ) + " => " + e.getMessage(), e );
			}
			throw e;
		}
		catch ( final SQLException e )
		{
			LOG.error( "executeDelete() FAILED query '" + queryString + "' with parameters " + Arrays.toString( queryParameters ) + " => " + e.getMessage(), e );
			throw e;
		}
	}

	/**
	 * Execute update query.
	 *
	 * @param updateQuery {@link UpdateQuery} to execute.
	 *
	 * @return Number of rows that were updated (may be {@code 0}).
	 *
	 * @throws SQLException the query could not be executed (due to a database
	 * error or invalid query).
	 */
	public int executeUpdate( @NotNull final UpdateQuery updateQuery )
	throws SQLException
	{
		final DataSource dataSource = getDataSource();
		final String tableName = getTableName( updateQuery );
		final String queryString = updateQuery.getQueryString( tableName );
		final Object[] queryParameters = updateQuery.getQueryParameters();

		try
		{
			final long start = System.nanoTime();
			final int result = JdbcTools.executeUpdate( dataSource, queryString, queryParameters );
			if ( LOG.isDebugEnabled() )
			{
				LOG.debug( "executeUpdate() time=" + ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0 + "s, result=" + result + ", query='" + queryString + "', parameters=" + Arrays.toString( queryParameters ) );
			}
			return result;
		}
		catch ( final SQLTransientException e )
		{
			if ( LOG.isDebugEnabled() )
			{
				LOG.debug( "executeUpdate() FAILED query '" + queryString + "' with parameters " + Arrays.toString( queryParameters ) + " => " + e.getMessage(), e );
			}
			throw e;
		}
		catch ( final SQLException e )
		{
			LOG.error( "executeUpdate() FAILED query '" + queryString + "' with parameters " + Arrays.toString( queryParameters ) + " => " + e.getMessage(), e );
			throw e;
		}
	}

	/**
	 * Get auto-incremented ID value after INSERT query.
	 *
	 * 
IMPORTANT:
This method uses/requires vendor-specific * SQL statements. Explicit support will need to be implemented for * unsupported vendors.
* * @param connection Connection that was used for INSERT. * * @return Auto-increment ID value. * * @throws SQLException if the auto-increment value could not be retrieved. */ protected long getInsertID( final Connection connection ) throws SQLException { final String query; switch ( getSqlDialect() ) { case MYSQL: query = "SELECT LAST_INSERT_ID();"; break; case HSQLDB: query = "CALL IDENTITY()"; break; case MSSQL: query = "SELECT SCOPE_IDENTITY()"; break; default: throw new IllegalStateException( "Don't know how to determine auto-increment value for SQL dialect " + getSqlDialect() ); } final Statement statement = connection.createStatement(); try { final ResultSet resultSet = statement.executeQuery( query ); try { resultSet.next(); return resultSet.getLong( 1 ); } finally { try { resultSet.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } } finally { try { statement.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } } /** * Get number result from single-column query. * * @param query SELECT query to execute. * * @return Resulting number; {@code null} if query returned empty or NULL * result. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ @Nullable public Number selectNumber( @NotNull final SelectQuery query ) throws SQLException { return executeQuery( JdbcTools.GET_NUMBER, query ); } /** * Get integer result from single-column query. * * @param selectQuery SELECT query to execute. * @param defaultValue Default value to return. * * @return Resulting number; {@code defaultValue} if query returned empty or * NULL result. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public int selectInt( @NotNull final SelectQuery selectQuery, final int defaultValue ) throws SQLException { final Integer result = executeQuery( JdbcTools.GET_INTEGER, selectQuery ); return result == null ? defaultValue : result; } /** * Execute a {@link SelectQuery} and return the results as a {@link List}. * * @param selectQuery SELECT query to execute. * * @return List with result set. * * @throws SQLException if an error occurs while accessing the database. */ @NotNull public List retrieveList( @NotNull final SelectQuery selectQuery ) throws SQLException { return retrieveList( selectQuery.getTableClass(), selectQuery.getQueryString( getTableName( selectQuery ) ), selectQuery.getQueryParameters() ); } /** * Execute query and specify the result object type. If any arguments are * provided, they are passed to a prepared statement that is then used to * perform the query. * * @param dbClass Result set record object. * @param query SQL query to execute. * @param arguments Arguments used in the query. * * @return List with result set. * * @throws IllegalArgumentException if reflection problems occur. * @throws NullPointerException if an argument is {@code null}. * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ @NotNull public List retrieveList( @NotNull final Class dbClass, @NotNull final CharSequence query, @NotNull final Object... arguments ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.trace( "retrieveList() query='" + query + "', arguments: " + Arrays.toString( arguments ) ); } final ObjectListConverter processor = new ObjectListConverter( dbClass ); final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( true ); } final long start = System.nanoTime(); final List result = JdbcTools.executeQuery( connection, processor, query, arguments ); if ( LOG.isDebugEnabled() ) { final double seconds = ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0; if ( seconds > SLOW_QUERY_THRESHOLD ) { final String message = "retrieveList() SLOW QUERY: time=" + seconds + "s, result=" + dbClass.getSimpleName() + '[' + result.size() + "], query='" + query + "', parameters=" + Arrays.toString( arguments ); LOG.warn( message, new DatabaseException( message ) ); } else { LOG.debug( "retrieveList() time=" + seconds + "s, result=" + dbClass.getSimpleName() + '[' + result.size() + "], query='" + query + "', parameters=" + Arrays.toString( arguments ) ); } } return result; } catch ( final SQLTransientException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "retrieveList() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); } throw e; } catch ( final SQLException e ) { LOG.error( "retrieveList() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); throw e; } finally { releaseConnection( connection ); } } /** * Execute query and feed its result set through a {@link ResultProcessor}. * * @param processor Result set processor. * @param query Query to be executed. * * @return Result of processor. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public R executeQuery( @NotNull final ResultProcessor processor, @NotNull final SelectQuery query ) throws SQLException { return executeQuery( processor, query.getQueryString( getTableName( query ) ), query.getQueryParameters() ); } /** * Execute query and feed its result set through a {@link ResultProcessor}. * * @param processor Result set processor. * @param query Query to be executed. * @param arguments Arguments used in the query. * * @return Result of processor. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public R executeQuery( @NotNull final ResultProcessor processor, @NotNull final CharSequence query, @NotNull final Object... arguments ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.trace( "executeQuery() query='" + query + "', arguments: " + Arrays.toString( arguments ) ); } final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( true ); } final long start = System.nanoTime(); final R result = JdbcTools.executeQuery( connection, processor, query, arguments ); if ( LOG.isDebugEnabled() ) { final double seconds = ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0; if ( seconds > SLOW_QUERY_THRESHOLD ) { final String message = "executeQuery() SLOW QUERY: time=" + seconds + "s, query='" + query + "', parameters=" + Arrays.toString( arguments ); LOG.warn( message, new DatabaseException( message ) ); } else { LOG.debug( "executeQuery() time=" + seconds + "s, query='" + query + "', parameters=" + Arrays.toString( arguments ) ); } } return result; } catch ( final SQLTransientException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "executeQuery() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); } throw e; } catch ( final SQLException e ) { LOG.error( "executeQuery() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); throw e; } finally { releaseConnection( connection ); } } /** * Execute query and feed its result set through a {@link ResultProcessor}. * * @param processor Result set processor. * @param query Query to be executed. * @param arguments Arguments used in the query. * * @return Result of processor. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public R executeQueryStreaming( @NotNull final ResultProcessor processor, @NotNull final CharSequence query, @NotNull final Object... arguments ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.trace( "executeQueryStreaming() query='" + query + "', arguments: " + Arrays.toString( arguments ) ); } final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( true ); } final long start = System.nanoTime(); final R result = JdbcTools.executeQueryStreaming( connection, processor, query, arguments ); if ( LOG.isDebugEnabled() ) { final double seconds = ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0; if ( seconds > SLOW_QUERY_THRESHOLD ) { final String message = "executeQueryStreaming() SLOW QUERY: time=" + seconds + "s, query='" + query + "', parameters=" + Arrays.toString( arguments ); LOG.warn( message, new DatabaseException( message ) ); } else { LOG.debug( "executeQueryStreaming() time=" + seconds + "s, query='" + query + "', parameters=" + Arrays.toString( arguments ) ); } } return result; } catch ( final SQLTransientException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "executeQueryStreaming() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); } throw e; } catch ( final SQLException e ) { LOG.error( "executeQueryStreaming() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); throw e; } finally { releaseConnection( connection ); } } /** * Execute a {@link SelectQuery} that returns exactly one result or none at * all. * * @param selectQuery SELECT query to execute. * * @return Result object; {@code null} if the result is empty. * * @throws SQLException if an error occurs while accessing the database or * multiple results were returned). */ @Nullable public DbObject retrieveObject( @NotNull final SelectQuery selectQuery ) throws SQLException { final Class dbClass = selectQuery.getTableClass(); final CharSequence query = selectQuery.getQueryString( getTableName( selectQuery ) ); final Object[] arguments = selectQuery.getQueryParameters(); final SingleObjectConverter processor = new SingleObjectConverter( dbClass ); if ( LOG.isTraceEnabled() ) { LOG.trace( "retrieveObject() query='" + query + "', arguments: " + Arrays.toString( arguments ) ); } final SingleObjectConverter result; final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( true ); } final long start = System.nanoTime(); result = JdbcTools.executeQuery( connection, processor, query, arguments ); if ( LOG.isDebugEnabled() ) { final double seconds = ( ( System.nanoTime() - start ) / 1000000L ) / 1000.0; if ( seconds > SLOW_QUERY_THRESHOLD ) { final String message = "retrieveObject() SLOW QUERY: time=" + seconds + "s, result=" + ( ( result.getFirst() != null ) ? result.isMultiple() ? "multiple" : "single" : "empty" ) + ", query='" + query + "', parameters=" + Arrays.toString( arguments ); LOG.warn( message, new DatabaseException( message ) ); } else { if ( LOG.isTraceEnabled() ) { LOG.trace( "retrieveObject() time=" + seconds + "s, result=" + ( ( result.getFirst() != null ) ? result.isMultiple() ? "multiple" : "single" : "empty" ) + ", query='" + query + "', parameters=" + Arrays.toString( arguments ) ); } } } } catch ( final SQLTransientException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "retrieveObject() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); } throw e; } catch ( final SQLException e ) { LOG.error( "retrieveObject() FAILED query '" + query + "' with parameters " + Arrays.toString( arguments ), e ); throw e; } finally { releaseConnection( connection ); } if ( result.isMultiple() ) { throw new SQLException( "Got multiple results on query: " + selectQuery.getQueryString( getTableName( selectQuery ) ) + " (with arguments " + Arrays.toString( selectQuery.getQueryParameters() ) + ')' ); } return result.getFirst(); } /** * Refresh the state of the instance from the database, overwriting changes * made to the entity, if any. * * @param object Object to refresh. * * @throws SQLException if an error occurs while accessing the database. */ public void refresh( @NotNull final DbObject object ) throws SQLException { final Class dbClass = object.getClass(); final ClassHandler classHandler = getClassHandler( dbClass ); if ( !classHandler.hasRecordId() ) { throw new IllegalArgumentException( "Can't refresh object without record id: " + object ); } final SelectQuery select = new SelectQuery( (Class)dbClass ); select.whereEqual( classHandler.getRecordIdColumn(), classHandler.getRecordId( object ) ); final String query = select.getQueryString(); final Object[] arguments = select.getQueryParameters(); try { final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( true ); } final Statement statement = connection.prepareStatement( query ); try { final PreparedStatement preparedStatement = (PreparedStatement)statement; JdbcTools.prepareStatement( preparedStatement, arguments ); final ResultSet resultSet = preparedStatement.executeQuery(); try { if ( !resultSet.next() ) { throw new SQLException( "Object not found in database: " + query + " (with arguments " + Arrays.toString( arguments ) + ')' ); } final ResultSetMetaData metaData = resultSet.getMetaData(); final FieldHandler[] fieldHandlers = new FieldHandler[ metaData.getColumnCount() ]; for ( int columnIndex = 0; columnIndex < fieldHandlers.length; columnIndex++ ) { final String column = metaData.getColumnLabel( columnIndex + 1 ); fieldHandlers[ columnIndex ] = classHandler.getFieldHandlerForColumn( column ); } for ( int columnIndex = 0; columnIndex < fieldHandlers.length; columnIndex++ ) { final FieldHandler field = fieldHandlers[ columnIndex ]; if ( field != null ) { field.getColumnData( object, resultSet, columnIndex + 1 ); } } if ( resultSet.next() ) { throw new SQLException( "Got multiple results on query: " + query + " (with arguments " + Arrays.toString( arguments ) + ')' ); } } finally { try { resultSet.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } } finally { try { statement.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } } finally { releaseConnection( connection ); } } catch ( final SQLTransientException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "Failed query: " + query, e ); } throw e; } catch ( final SQLException e ) { LOG.error( "Failed query: " + query, e ); throw e; } } /** * Stores a single database record in the database. An INSERT or UPDATE * query is generated depending on the value of the 'ID' field. * * @param object Object to store in the database. * * @throws IllegalArgumentException if reflection problems occur. * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public void storeObject( @NotNull final Object object ) throws SQLException { final Class objectClass = object.getClass(); final ClassHandler classHandler = getClassHandler( objectClass ); if ( classHandler.hasRecordId() && ( classHandler.getRecordId( object ) >= 0L ) ) { updateObject( object ); } else { insertObjectImpl( object ); } } /** * Updates a single object in the database, setting all fields. * * @param object Object to be updated in the database. * * @throws IllegalArgumentException if reflection problems occur. * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public void updateObject( @NotNull final Object object ) throws SQLException { final Class objectClass = object.getClass(); final ClassHandler classHandler = getClassHandler( objectClass ); updateObjectImpl( object, classHandler.getFieldHandlers() ); } /** * Updates a single object in the database, setting only the values for the * specified fields. All other field values are left as-is. * * To update all fields of an object, use {@link #updateObject(Object)} * instead. * * @param object Object to be updated in the database. * @param fieldNames Names of the fields to be updated. * * @throws IllegalArgumentException if reflection problems occur. * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public void updateObject( @NotNull final Object object, @NotNull final String... fieldNames ) throws SQLException { updateObject( object, Arrays.asList( fieldNames ) ); } /** * Updates a single object in the database, setting only the values for the * specified fields. All other field values are left as-is. * * To update all fields of an object, use {@link #updateObject(Object)} * instead. * * @param object Object to be updated in the database. * @param fieldNames Names of the fields to be updated. * * @throws IllegalArgumentException if reflection problems occur. * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ public void updateObject( @NotNull final Object object, @NotNull final Collection fieldNames ) throws SQLException { final Class objectClass = object.getClass(); final ClassHandler classHandler = getClassHandler( objectClass ); final List fieldHandlers = new ArrayList( fieldNames.size() ); for ( final String fieldName : fieldNames ) { final FieldHandler fieldHandler = classHandler.getFieldHandlerForColumn( fieldName ); if ( fieldHandler == null ) { throw new IllegalArgumentException( "Unknown field: " + fieldName ); } fieldHandlers.add( fieldHandler ); } updateObjectImpl( object, fieldHandlers ); } /** * Updates a single object in the database, setting only the values for the * specified fields. All other field values are left as-is. * * @param object Object to be updated in the database. * @param fieldHandlers Fields to be updated. * * @throws SQLException the query could not be executed (due to a database * error or invalid query). */ protected void updateObjectImpl( @NotNull final Object object, @NotNull final List fieldHandlers ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.trace( "updateObjectImpl( " + object + ", " + fieldHandlers + " )" ); } final Class objectClass = object.getClass(); final ClassHandler classHandler = getClassHandler( objectClass ); final String tableName = getTableName( objectClass ); final String recordIdColumn = classHandler.getRecordIdColumn(); final long recordId = classHandler.getRecordId( object ); if ( recordId < 0L ) { throw new IllegalArgumentException( "recordId: " + recordId ); } final StringBuilder query = new StringBuilder(); query.append( "UPDATE " ); query.append( tableName ); query.append( " SET " ); final List nowFields = new ArrayList(); boolean haveOne = false; for ( final FieldHandler fieldHandler : fieldHandlers ) { final String fieldName = fieldHandler.getName(); if ( !fieldName.equals( recordIdColumn ) ) { if ( haveOne ) { query.append( ',' ); } if ( NOW.equals( fieldHandler.getFieldValue( object ) ) ) { query.append( fieldName ); query.append( '=' ); query.append( getCurrentDateTimeFunction() ); nowFields.add( fieldHandler ); } else { query.append( fieldName ); query.append( "=?" ); } haveOne = true; } } query.append( " WHERE " ); query.append( recordIdColumn ); query.append( '=' ); query.append( recordId ); if ( !haveOne ) { throw new IllegalArgumentException( "Nothing to update: object=" + object + ", fieldHandlers=" + fieldHandlers ); } final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( false ); } final String queryString = query.toString(); if ( LOG.isDebugEnabled() ) { LOG.debug( "updateObjectImpl() query: " + queryString ); } final PreparedStatement preparedStatement = connection.prepareStatement( queryString ); try { int columnIndex = 1; for ( final FieldHandler fieldHandler : fieldHandlers ) { final String fieldName = fieldHandler.getName(); if ( !fieldName.equals( recordIdColumn ) && !NOW.equals( fieldHandler.getFieldValue( object ) ) ) { fieldHandler.setColumnData( object, preparedStatement, columnIndex++ ); } } /* * UPDATE should generate exactly 1 update. */ final int updateCount = preparedStatement.executeUpdate(); if ( updateCount != 1 ) { throw new SQLException( "updateCount: " + updateCount + ", query: " + queryString ); } } finally { try { preparedStatement.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } if ( !nowFields.isEmpty() ) { updateNowFields( object, nowFields, connection ); } } finally { releaseConnection( connection ); } } /** * This internal method is used to prepare a statement to store an object in * the database. * * @param object Object to store. * * @throws SQLException if an error occurred while preparing the statement. * @throws IllegalArgumentException if no table name is defined for the * given object's type. */ protected void insertObjectImpl( @NotNull final Object object ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.trace( "insertObjectImpl( " + object + " )" ); } final Class objectClass = object.getClass(); final ClassHandler classHandler = getClassHandler( objectClass ); final boolean hasRecordId = classHandler.hasRecordId(); final String skipField = ( hasRecordId && ( classHandler.getRecordId( object ) < 0L ) ) ? classHandler.getRecordIdColumn() : null; final List fieldHandlers = classHandler.getFieldHandlers(); final String tableName = getTableName( objectClass ); final StringBuilder query = new StringBuilder(); query.append( "INSERT INTO " ); query.append( tableName ); query.append( " (" ); boolean first = true; for ( final FieldHandler fieldHandler : fieldHandlers ) { final String fieldName = fieldHandler.getName(); if ( !fieldName.equals( skipField ) ) { if ( !first ) { query.append( ',' ); } query.append( fieldName ); first = false; } } query.append( ") VALUES (" ); first = true; final List nowFields = new ArrayList(); for ( final FieldHandler fieldHandler : fieldHandlers ) { final String fieldName = fieldHandler.getName(); if ( !fieldName.equals( skipField ) ) { if ( !first ) { query.append( ',' ); } if ( NOW.equals( fieldHandler.getFieldValue( object ) ) ) { query.append( getCurrentDateTimeFunction() ); nowFields.add( fieldHandler ); } else { query.append( '?' ); } first = false; } } query.append( ')' ); final Connection connection = acquireConnection(); try { if ( !isTransactionActive() ) { connection.setReadOnly( false ); } final String queryString = query.toString(); if ( LOG.isDebugEnabled() ) { LOG.debug( "Prepare/execute: " + queryString ); } final PreparedStatement preparedStatement = connection.prepareStatement( queryString ); try { int columnIndex = 1; for ( final FieldHandler fieldHandler : fieldHandlers ) { final String fieldName = fieldHandler.getName(); if ( !fieldName.equals( skipField ) && !NOW.equals( fieldHandler.getFieldValue( object ) ) ) { fieldHandler.setColumnData( object, preparedStatement, columnIndex++ ); } } /* * INSERT should generate exactly 1 update. */ final int updateCount = preparedStatement.executeUpdate(); if ( updateCount != 1 ) { throw new SQLException( "updateCount: " + updateCount + ", query: " + queryString ); } if ( hasRecordId ) { final long insertRecordId = getInsertID( connection ); if ( insertRecordId < 0L ) { throw new SQLException( "insertRecordId: " + insertRecordId + ", query: " + queryString ); } if ( LOG.isTraceEnabled() ) { LOG.trace( "object: " + object + ", insertRecordId: " + insertRecordId ); } classHandler.setRecordId( object, insertRecordId ); } } finally { try { preparedStatement.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } if ( !nowFields.isEmpty() ) { updateNowFields( object, nowFields, connection ); } } finally { releaseConnection( connection ); } } /** * This method is called from the {@link #updateObjectImpl} and {@link * #insertObjectImpl} methods to update the value of {@link #NOW} fields * from the database. * * @param object Object that was just updated in the database. * @param nowFields Fields that were set to {@link #NOW}. * @param connection Database connection to use. * * @throws SQLException if an error occurs while accessing the database. */ private void updateNowFields( @NotNull final Object object, @NotNull final List nowFields, @NotNull final Connection connection ) throws SQLException { final Class objectClass = object.getClass(); final String tableName = getTableName( objectClass ); final ClassHandler classHandler = getClassHandler( objectClass ); final long recordId = classHandler.getRecordId( object ); final String recordIdColumn = classHandler.getRecordIdColumn(); final StringBuilder query = new StringBuilder(); query.setLength( 0 ); query.append( "SELECT " ); for ( int i = 0; i < nowFields.size(); i++ ) { if ( i > 0 ) { query.append( ',' ); } query.append( nowFields.get( i ).getName() ); } query.append( " FROM " ); query.append( tableName ); query.append( " WHERE " ); query.append( recordIdColumn ); query.append( '=' ); query.append( recordId ); final Statement statement = connection.createStatement(); try { statement.execute( query.toString() ); final ResultSet resultSet = statement.getResultSet(); if ( ( resultSet != null ) && resultSet.next() ) { for ( int i = 0; i < nowFields.size(); i++ ) { nowFields.get( i ).getColumnData( object, resultSet, i + 1 ); if ( i > 0 ) { query.append( ',' ); } query.append( nowFields.get( i ).getName() ); } } } finally { try { statement.close(); } catch ( final SQLException e ) { /* ignored, would hide real exception */ LOG.warn( "Ignored: " + e, e ); } } } /** * Acquires a connection for the current transaction or a single query (if * no transaction is active). * * @return Database connection. * * @throws SQLException if an error occurs while accessing the database. */ @SuppressWarnings( "JDBCResourceOpenedButNotSafelyClosed" ) @NotNull protected Connection acquireConnection() throws SQLException { Connection result = getTransactionConnection(); if ( result == null ) { final DataSource dataSource = getDataSource(); result = dataSource.getConnection(); } return result; } /** * Releases a connection for a single query. If a transaction is active, the * connection is not released until the transaction is committed. * * @param connection Database connection. */ protected void releaseConnection( @NotNull final Connection connection ) { if ( getTransactionConnection() == null ) { try { connection.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } } /** * Starts a transaction. * * @throws SQLException if an error occurs while accessing the database. */ public void startTransaction() throws SQLException { LOG.entering( "startTransaction" ); final Connection transactionConnection = getTransactionConnection(); if ( transactionConnection != null ) { throw new SQLException( "Another transaction is already active" ); } final Connection connection = _dataSource.getConnection(); connection.setReadOnly( false ); connection.setAutoCommit( false ); _transactionConnection.set( connection ); } /** * Starts a transaction with the specified isolation level. * * @param level Transaction isolation level; see {@link Connection}. * * @throws SQLException if an error occurs while accessing the database. */ public void startTransaction( @MagicConstant( intValues = { Connection.TRANSACTION_READ_UNCOMMITTED, Connection.TRANSACTION_READ_COMMITTED, Connection.TRANSACTION_REPEATABLE_READ, Connection.TRANSACTION_SERIALIZABLE } ) final int level ) throws SQLException { if ( LOG.isTraceEnabled() ) { LOG.entering( "startTransaction", "level=" + level ); } startTransaction(); final Connection connection = getTransactionConnectionNotNull(); connection.setTransactionIsolation( level ); } /** * Commits the current transaction. * * @throws SQLException if an error occurs while accessing the database. */ public void commit() throws SQLException { LOG.entering( "commit" ); final Connection connection = getTransactionConnectionNotNull(); try { connection.commit(); } finally { try { connection.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } _transactionConnection.remove(); } } /** * Performs a rollback of the current transaction. * * @throws SQLException if an error occurs while accessing the database. */ public void rollback() throws SQLException { LOG.entering( "rollback" ); final Connection connection = getTransactionConnectionNotNull(); try { connection.rollback(); } finally { try { connection.close(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } _transactionConnection.remove(); } } /** * Performs a rollback of the current transaction to the given savepoint. * * @param savepoint Savepoint to rollback. * * @throws SQLException if an error occurs while accessing the database. */ public void rollback( final Savepoint savepoint ) throws SQLException { LOG.entering( "rollback", savepoint ); final Connection connection = getTransactionConnectionNotNull(); connection.rollback( savepoint ); } /** * Creates an unnamed savepoint in the current transaction and returns the * object that represents it. * * @return Created savepoint. * * @throws SQLException if an error occurs while accessing the database. */ public Savepoint setSavepoint() throws SQLException { LOG.entering( "setSavepoint" ); return getTransactionConnectionNotNull().setSavepoint(); } /** * Creates a savepoint with the given name in the current transaction and * returns the object that represents it. * * @param name Name of the savepoint. * * @return Created savepoint. * * @throws SQLException if an error occurs while accessing the database. */ public Savepoint setSavepoint( final String name ) throws SQLException { LOG.entering( "setSavepoint", name ); return getTransactionConnectionNotNull().setSavepoint( name ); } /** * Removes the specified savepoint and subsequent savepoint objects from the * current transaction. * * @param savepoint Savepoint to release. * * @throws SQLException if an error occurs while accessing the database. */ public void releaseSavepoint( @NotNull final Savepoint savepoint ) throws SQLException { LOG.entering( "releaseSavepoint", savepoint ); getTransactionConnectionNotNull().releaseSavepoint( savepoint ); } /** * Returns the database connection for the current transaction. * * @return Database connection. */ @Nullable private Connection getTransactionConnection() { return _transactionConnection.get(); } /** * Returns the database connection for the current transaction. * * @return Database connection. * * @throws SQLException if there is no current transaction. */ @NotNull private Connection getTransactionConnectionNotNull() throws SQLException { final Connection connection = getTransactionConnection(); if ( connection == null ) { throw new SQLException( "No transaction in progress" ); } if ( connection.getAutoCommit() ) { throw new SQLException( "Connection is in auto-commit mode" ); } return connection; } /** * Returns whether a transaction is active. * * @return {@code true} if a transaction is active. v */ public boolean isTransactionActive() { boolean result = false; final Connection connection = getTransactionConnection(); if ( connection != null ) { try { result = !connection.isClosed(); } catch ( final SQLException ignored ) { /* ignored, would hide real exception */ } } return result; } /** * Executes the given operations within a database transaction. * * @param body Operations to perform within a transaction. * * @throws SQLException if an error occurs while accessing the database. */ public void transaction( final TransactionBody body ) throws SQLException { startTransaction( Connection.TRANSACTION_SERIALIZABLE ); try { body.execute(); commit(); } finally { if ( isTransactionActive() ) { try { rollback(); } catch ( final SQLException e ) { LOG.warn( "Rollback failed: " + e ); } } } } /** * Executes the given operations within a database transaction. If the * transaction fails due to an automatic rollback (indicated by {@link * SQLTransactionRollbackException}), repeated attempts will be made until * the transaction succeeds (or until 100 attempts have failed). There will * be a short random delay (10 to 100 ms) between attempts. * * @param body Operations to perform within a transaction. * * @throws SQLException if an error occurs while accessing the database. * @noinspection MethodWithMultipleReturnPoints */ public void transactionWithRetry( final TransactionBody body ) throws SQLException { final int maximumAttempts = 100; for ( int attempt = 1; attempt < maximumAttempts; attempt++ ) { try { transaction( body ); return; } catch ( final SQLTransactionRollbackException e ) { LOG.trace( "Will retry transaction " + body + " after " + e ); try { //noinspection UnsecureRandomNumberGeneration Thread.sleep( (long)( Math.random() * 90.0 ) + 10L ); } catch ( final InterruptedException ignored ) { throw new SQLException( "Interrupted before transaction could be retried.", e ); } } } try { transaction( body ); } catch ( final SQLTransactionRollbackException e ) { throw new SQLException( "Transaction failed after trying " + maximumAttempts + " times", e ); } } /** * Converter that converts a {@link ResultSet} to tuple objects. * * @param Database record object. */ public static class ObjectConverter { /** * Database record class. */ final Class _dbClass; /** * Cache for {@link #getColumnHandlers}. */ private List> _columnHandlers = null; /** * Prefix to remove from the column names in the result set. This can be * used to separate columns for multiple objects. */ private String _columnPrefix = null; /** * Construct processor. * * @param dbClass Database record class. */ public ObjectConverter( @NotNull final Class dbClass ) { _dbClass = dbClass; } /** * Convert row from result set to database record object. * * @param resultSet {@link ResultSet} to get record properties from. * * @return Database record object. * * @throws SQLException if an error occurs while accessing the * database. */ @NotNull public T convert( @NotNull final ResultSet resultSet ) throws SQLException { final T result = createObject(); for ( final Duet columnHandler : getColumnHandlers( resultSet ) ) { columnHandler.getValue2().getColumnData( result, resultSet, columnHandler.getValue1() ); } return result; } /** * Get handlers that for result set columns. * * @param resultSet {@link ResultSet} to get column handlers for. * * @return List of column numbers and their handlers. * * @throws SQLException if an error occurs while accessing the * database. */ @NotNull protected List> getColumnHandlers( @NotNull final ResultSet resultSet ) throws SQLException { List> result = _columnHandlers; if ( result == null ) { final ResultSetMetaData metaData = resultSet.getMetaData(); final int columnCount = metaData.getColumnCount(); result = new ArrayList>( columnCount ); final ClassHandler classHandler = getClassHandler( _dbClass ); final String columnPrefix = _columnPrefix; for ( int column = 1; column <= columnCount; column++ ) { final String name = metaData.getColumnLabel( column ).toLowerCase(); FieldHandler handler = null; if ( columnPrefix == null ) { handler = classHandler.getFieldHandlerForColumn( name ); } else if ( name.startsWith( columnPrefix ) ) { final String baseName = name.substring( columnPrefix.length() ); handler = classHandler.getFieldHandlerForColumn( baseName ); } if ( handler != null ) { result.add( new BasicDuet( column, handler ) ); } } _columnHandlers = result; } return result; } /** * Create database record object. * * @return Database record object. */ @NotNull public T createObject() { return BeanTools.newInstance( _dbClass ); } public void setColumnPrefix( final String columnPrefix ) { _columnPrefix = columnPrefix; } public String getColumnPrefix() { return _columnPrefix; } } /** * SQL result processor that converts a result set into a list of database * records. * * @param Database record object. */ public static class ObjectListConverter implements ResultProcessor> { /** * Converts {@link ResultSet} to record objects. */ private final ObjectConverter _converter; /** * Construct processor. * * @param dbClass Database record class. */ public ObjectListConverter( @NotNull final Class dbClass ) { _converter = new ObjectConverter( dbClass ); } @Override public List process( @NotNull final ResultSet resultSet ) throws SQLException { final List result = new ArrayList(); while ( resultSet.next() ) { result.add( _converter.convert( resultSet ) ); } return result; } } /** * SQL result processor that converts a result set into a single database * record. * * @param Database record object. */ public static class SingleObjectConverter implements ResultProcessor> { /** * Database record class. */ private final Class _dbClass; /** * First object from result set. This is {@code null} after processing * if the result set was empty. */ @Nullable private T _first = null; /** * Flag to indicate that more than one row was present in the result * set. */ private boolean _multiple = false; /** * Construct processor. * * @param dbClass Database record class. */ public SingleObjectConverter( @NotNull final Class dbClass ) { _dbClass = dbClass; } @Override public SingleObjectConverter process( @NotNull final ResultSet resultSet ) throws SQLException { final T first; final boolean multiple; if ( resultSet.next() ) { final ObjectConverter converter = new ObjectConverter( _dbClass ); first = converter.convert( resultSet ); multiple = resultSet.next(); } else { first = null; multiple = false; } _first = first; _multiple = multiple; return this; } /** * Returns whether more than one row was present in the result set. * * @return {@code true} if more than one row was present in the result * set. */ public boolean isMultiple() { return _multiple; } /** * Returns first object from result set. This returns {@code null} if * the result set was empty. * * @return First object from result; {@code null} if result set was * empty. */ @Nullable public T getFirst() { return _first; } /** * Returns first object from result set. This throws a {@link * NoSuchElementException} if the result set was empty. * * @return First object from result. */ @NotNull public T getOne() { final T result = _first; if ( result == null ) { throw new NoSuchElementException( "Got no result from query, while one result is required" ); } return result; } /** * Returns first object from result set. This throws a {@link * NoSuchElementException} if the result set was empty or a {@link * IllegalStateException} if there were multiple rows in the result * set. * * @return Only object from result. */ @NotNull public T getOnly() { final T result = _first; if ( result == null ) { throw new NoSuchElementException( "Got no result from query, while one and only one result is required" ); } if ( _multiple ) { throw new IllegalStateException( "Got multiple results from query, while one and only one is required" ); } return result; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy