/*******************************************************************************
 * Copyright (c) 2010 Robert "Unlogic" Olofsson (unlogic@unlogic.se).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v3
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-3.0-standalone.html
 ******************************************************************************/
package se.unlogic.standardutils.dao;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;

import javax.sql.DataSource;

import se.unlogic.standardutils.annotations.UnsupportedFieldTypeException;
import se.unlogic.standardutils.bool.BooleanSignal;
import se.unlogic.standardutils.dao.annotations.DAOManaged;
import se.unlogic.standardutils.dao.annotations.Key;
import se.unlogic.standardutils.dao.annotations.ManyToMany;
import se.unlogic.standardutils.dao.annotations.ManyToOne;
import se.unlogic.standardutils.dao.annotations.OneToMany;
import se.unlogic.standardutils.dao.annotations.OneToOne;
import se.unlogic.standardutils.dao.annotations.OrderBy;
import se.unlogic.standardutils.dao.annotations.SimplifiedRelation;
import se.unlogic.standardutils.dao.annotations.Table;
import se.unlogic.standardutils.dao.enums.Order;
import se.unlogic.standardutils.dao.querys.ArrayListQuery;
import se.unlogic.standardutils.dao.querys.BooleanQuery;
import se.unlogic.standardutils.dao.querys.ObjectQuery;
import se.unlogic.standardutils.dao.querys.PreparedStatementQuery;
import se.unlogic.standardutils.dao.querys.UpdateQuery;
import se.unlogic.standardutils.db.DBUtils;
import se.unlogic.standardutils.numbers.IntegerCounter;
import se.unlogic.standardutils.populators.BeanStringPopulator;
import se.unlogic.standardutils.populators.EnumPopulator;
import se.unlogic.standardutils.populators.IntegerPopulator;
import se.unlogic.standardutils.populators.QueryParameterPopulator;
import se.unlogic.standardutils.populators.QueryParameterPopulatorRegistery;
import se.unlogic.standardutils.populators.annotated.AnnotatedResultSetPopulator;
import se.unlogic.standardutils.reflection.ReflectionUtils;
import se.unlogic.standardutils.string.StringUtils;

public class AnnotatedDAO<T> {

	private static final OrderByComparator ORDER_BY_COMPARATOR = new OrderByComparator();

	protected final AnnotatedResultSetPopulator<T> populator;
	protected final DataSource dataSource;
	protected final Class<T> beanClass;

	protected final List<QueryParameterPopulator<?>> queryParameterPopulators;
	protected final List<BeanStringPopulator<?>> beanStringPopulators;

	protected final ArrayList<Column<T, ?>> simpleKeys = new ArrayList<Column<T, ?>>();
	protected final ArrayList<Column<T, ?>> simpleColumns = new ArrayList<Column<T, ?>>();
	protected final HashMap<Field, Column<T, ?>> columnMap = new HashMap<Field, Column<T, ?>>();

	protected final ArrayList<ColumnKeyCollector<T>> columnKeyCollectors = new ArrayList<ColumnKeyCollector<T>>();

	protected final HashMap<Field, ManyToOneRelation<T, ?, ?>> manyToOneRelations = new HashMap<Field, ManyToOneRelation<T, ?, ?>>();
	protected final HashMap<Field, ManyToOneRelation<T, ?, ?>> manyToOneRelationKeys = new HashMap<Field, ManyToOneRelation<T, ?, ?>>();
	protected final HashMap<Field, OneToManyRelation<T, ?>> oneToManyRelations = new HashMap<Field, OneToManyRelation<T, ?>>();
	protected final HashMap<Field, ManyToManyRelation<T, ?>> manyToManyRelations = new HashMap<Field, ManyToManyRelation<T, ?>>();

	protected final TreeMap<OrderBy, Column<T, ?>> columnOrderMap = new TreeMap<OrderBy, Column<T, ?>>(ORDER_BY_COMPARATOR);

	protected final ArrayList<Field> autoAddRelations = new ArrayList<Field>();
	protected final ArrayList<Field> autoGetRelations = new ArrayList<Field>();
	protected final ArrayList<Field> autoUpdateRelations = new ArrayList<Field>();

	protected String tableName;

	protected String insertSQL;
	protected String updateSQL;
	protected String deleteSQL;
	protected String checkIfExistsSQL;
	protected String deleteByFieldSQL;
	protected String getSQL;
	protected String defaultSortingCriteria;

	public AnnotatedDAO(DataSource dataSource, Class<T> beanClass, AnnotatedDAOFactory daoFactory) {

		this(dataSource, beanClass, daoFactory, new AnnotatedResultSetPopulator<T>(beanClass), null, null);
	}

	public AnnotatedDAO(DataSource dataSource, Class<T> beanClass, AnnotatedDAOFactory daoFactory, AnnotatedResultSetPopulator<T> populator, QueryParameterPopulator<?>... queryParameterPopulators) {

		this(dataSource, beanClass, daoFactory, populator, Arrays.asList(queryParameterPopulators), null);
	}

	public AnnotatedDAO(DataSource dataSource, Class<T> beanClass, AnnotatedDAOFactory daoFactory, List<? extends QueryParameterPopulator<?>> queryParameterPopulators, List<? extends BeanStringPopulator<?>> typePopulators) {

		this(dataSource, beanClass, daoFactory, new AnnotatedResultSetPopulator<T>(beanClass, typePopulators), queryParameterPopulators, typePopulators);
	}

	public AnnotatedDAO(DataSource dataSource, Class<T> beanClass, AnnotatedDAOFactory daoFactory, AnnotatedResultSetPopulator<T> populator,
			List<? extends QueryParameterPopulator<?>> queryParameterPopulators, List<? extends BeanStringPopulator<?>> typePopulators) {

		super();
		this.populator = populator;
		this.dataSource = dataSource;
		this.beanClass = beanClass;

		if (queryParameterPopulators != null) {

			this.queryParameterPopulators = new ArrayList<QueryParameterPopulator<?>>(queryParameterPopulators);

		} else {

			this.queryParameterPopulators = null;
		}

		if (this.beanStringPopulators != null) {

			this.beanStringPopulators = new ArrayList<BeanStringPopulator<?>>(typePopulators);

		} else {

			this.beanStringPopulators = null;
		}

		Table table = beanClass.getAnnotation(Table.class);

		if (table == null) {

			throw new RuntimeException("No @Table annotation found in  " + beanClass);
		} else {
			tableName = table.name();
		}

		this.tableName = table.name();

		List<Field> fields = ReflectionUtils.getFields(beanClass);

		int generatedKeyColumnIndex = 1;

		for (Field field : fields) {

			DAOManaged daoManaged = field.getAnnotation(DAOManaged.class);
			OrderBy orderBy = field.getAnnotation(OrderBy.class);

			if (daoManaged != null) {

				ReflectionUtils.fixFieldAccess(field);

				if (field.isAnnotationPresent(OneToOne.class)) {

					this.checkAutoGeneration(daoManaged);

					// TODO Relation use this class pk, no extra field
					// TODO check auto fields
					throw new RuntimeException("OneToOne relations are not implemented yet!");

				} else if (field.isAnnotationPresent(OneToMany.class)) {

					OneToMany oneToMany = field.getAnnotation(OneToMany.class);

					this.checkOrderByAnnotation(field, orderBy);

					if (field.getType() != List.class) {

						throw new UnsupportedFieldTypeException("The annotated field " + field.getName() + " in  " + beanClass + " is of unsupported type "
								+ field.getType() + ". Fields annotated as @OneToMany have to be a genericly typed " + List.class, field, OneToMany.class,
								beanClass);
					}

					if (ReflectionUtils.getGenericlyTypeCount(field) != 1) {

						throw new UnsupportedFieldTypeException("The annotated field " + field.getName() + " in  " + beanClass
								+ " is genericly typed. Fields annotated as @OneToMany have to be a genericly typed " + List.class, field, OneToMany.class,
								beanClass);
					}

					// This is a bit ugly but still necessary until someone else
					// comes up with something smarter...
					Class<?> remoteClass = (Class<?>) ReflectionUtils.getGenericType(field);

					SimplifiedRelation simplifiedRelation = field.getAnnotation(SimplifiedRelation.class);

					if (simplifiedRelation != null) {

						this.oneToManyRelations.put(field, SimplifiedOneToManyRelation.getGenericInstance(beanClass, remoteClass, field, this, typePopulators, queryParameterPopulators));

					} else {

						// Use this class pks, no extra field
						this.oneToManyRelations.put(field, DefaultOneToManyRelation.getGenericInstance(beanClass, remoteClass, field, daoFactory, daoManaged));
					}

					if (oneToMany.autoAdd()) {
						this.autoAddRelations.add(field);
					}

					if (oneToMany.autoUpdate()) {
						this.autoUpdateRelations.add(field);
					}

					if (oneToMany.autoGet()) {
						this.autoGetRelations.add(field);
					}

				} else if (field.isAnnotationPresent(ManyToOne.class)) {

					this.checkAutoGeneration(daoManaged);

					ManyToOne manyToOne = field.getAnnotation(ManyToOne.class);

					List<Field> remoteClassFields = ReflectionUtils.getFields(field.getType());

					Field matchingRemoteField = null;

					if (remoteClassFields != null) {

						for (Field remoteField : remoteClassFields) {

							if (remoteField.isAnnotationPresent(DAOManaged.class) && remoteField.isAnnotationPresent(OneToMany.class)
									&& remoteField.getType() == List.class && ReflectionUtils.isGenericlyTyped(remoteField)
									&& ((Class<?>) ReflectionUtils.getGenericType(remoteField) == this.beanClass)) {

								matchingRemoteField = remoteField;

								break;
							}
						}
					}

					if (matchingRemoteField == null) {

						throw new RuntimeException("No corresponding @OneToMany annotated field found in  " + field.getType()
								+ " matching @ManyToOne relation of field " + field.getName() + " in  " + beanClass + "!");
					}

					Field remoteKeyField = null;

					if (!StringUtils.isEmpty(manyToOne.remoteKeyField())) {

						remoteKeyField = ReflectionUtils.getField(field.getType(), manyToOne.remoteKeyField());

						//TODO Check if the remote key field is @DAOPopluate annotated
						if (remoteKeyField == null) {

							throw new RuntimeException("Unable to find @Key annotated field " + manyToOne.remoteKeyField() + " in " + field.getType() + " specified for @ManyToOne annotated field "
									+ field.getName() + " in " + beanClass);
						}

					} else {

						for (Field remoteField : remoteClassFields) {

							if (remoteField.isAnnotationPresent(DAOManaged.class) && remoteField.isAnnotationPresent(Key.class)) {

								if (remoteKeyField != null) {

									throw new RuntimeException("Found multiple @Key annotated fields in " + field.getType() + ", therefore the remoteKeyField property needs to be specified for the @ManyToOne annotated field "
											+ field.getName() + " in " + beanClass);
								}

								remoteKeyField = remoteField;
							}
						}

						if (remoteKeyField == null) {

							throw new RuntimeException("Unable to find @Key annotated field in " + field.getType() + " while parsing @ManyToOne annotated field "
									+ field.getName() + " in " + beanClass);
						}
					}

					DefaultManyToOneRelation<T, ?, ?> relation = null;

					if (field.isAnnotationPresent(Key.class)) {

						relation = DefaultManyToOneRelation.getGenericInstance(beanClass, field.getType(), remoteKeyField.getType(), field, remoteKeyField, daoManaged, daoFactory);

						manyToOneRelationKeys.put(field, relation);

					} else {

						relation = DefaultManyToOneRelation.getGenericInstance(beanClass, field.getType(), remoteKeyField.getType(), field, remoteKeyField, daoManaged, daoFactory);

						this.manyToOneRelations.put(field, relation);
					}

					this.columnMap.put(field, relation);

					if (orderBy != null) {
						this.columnOrderMap.put(orderBy, relation);
					}

					if (manyToOne.autoAdd()) {
						this.autoAddRelations.add(field);
					}

					if (manyToOne.autoUpdate()) {
						this.autoUpdateRelations.add(field);
					}

					if (manyToOne.autoGet()) {
						this.autoGetRelations.add(field);
					}

				} else if (field.isAnnotationPresent(ManyToMany.class)) {

					this.checkAutoGeneration(daoManaged);

					this.checkOrderByAnnotation(field, orderBy);

					if (field.getType() != List.class) {

						throw new UnsupportedFieldTypeException("The annotated field " + field.getName() + " in  " + beanClass + " is of unsupported type "
								+ field.getType() + ". Fields annotated as @ManyToMany have to be a genericly typed " + List.class, field, ManyToMany.class,
								beanClass);
					}

					if (ReflectionUtils.getGenericlyTypeCount(field) != 1) {

						throw new UnsupportedFieldTypeException("The annotated field " + field.getName() + " in  " + beanClass
								+ " is genericly typed. Fields annotated as @ManyToMany have to be a genericly typed " + List.class, field, ManyToMany.class,
								beanClass);
					}

					// This is a bit ugly but still necessary until someone else
					// comes up with something smarter...
					Class<?> remoteClass = (Class<?>) ReflectionUtils.getGenericType(field);

					this.manyToManyRelations.put(field, DefaultManyToManyRelation.getGenericInstance(beanClass, remoteClass, field, daoFactory, daoManaged));

					ManyToMany manyToMany = field.getAnnotation(ManyToMany.class);

					if (manyToMany.autoAdd()) {
						this.autoAddRelations.add(field);
					}

					if (manyToMany.autoUpdate()) {
						this.autoUpdateRelations.add(field);
					}

					if (manyToMany.autoGet()) {
						this.autoGetRelations.add(field);
					}

				} else {

					QueryParameterPopulator<?> queryPopulator = this.getQueryParameterPopulator(field.getType());

					Method method = null;

					if (queryPopulator == null) {

						method = PreparedStatementQueryMethods.getQueryMethod(field.getType());

						if (method == null && field.getType().isEnum()) {

							queryPopulator = EnumPopulator.getInstanceFromField(field);
						}

						if (method == null && queryPopulator == null) {

							throw new RuntimeException("No query method or query parameter populator found for @DAOManaged annotate field " + field.getName() + " in  " + beanClass);
						}
					}

					String columnName = daoManaged.columnName();

					if (StringUtils.isEmpty(columnName)) {

						columnName = field.getName();
					}

					SimpleColumn<T, ?> simpleColumn = null;

					Key primaryKey = field.getAnnotation(Key.class);

					if (primaryKey != null) {

						simpleColumn = SimpleColumn.getGenericInstance(beanClass, field.getType(), field, method, queryPopulator, columnName, daoManaged
								.autoGenerated());

						this.simpleKeys.add(simpleColumn);

					} else {

						simpleColumn = SimpleColumn.getGenericInstance(beanClass, field.getType(), field, method, queryPopulator, columnName, daoManaged
								.autoGenerated());

						this.simpleColumns.add(simpleColumn);
					}

					this.columnMap.put(field, simpleColumn);

					if (daoManaged.autoGenerated()) {

						if (daoManaged.autGenerationColumnIndex() != 0) {

							this.columnKeyCollectors.add(new ColumnKeyCollector<T>(field, populator, daoManaged.autGenerationColumnIndex()));

						} else {

							this.columnKeyCollectors.add(new ColumnKeyCollector<T>(field, populator, generatedKeyColumnIndex));

							generatedKeyColumnIndex++;
						}
					}

					if (orderBy != null) {
						this.columnOrderMap.put(orderBy, simpleColumn);
					}
				}
			}
		}

		if (this.simpleKeys.isEmpty() && this.manyToOneRelationKeys.isEmpty()) {

			throw new RuntimeException("No @Key annotated field found in  " + beanClass + "!");
		}

		// Genearate SQL statements
		this.generateInsertSQL();
		this.generateUpdateSQL();
		this.generateDeleteSQL();
		this.generateCheckIfExistsSQL();
		this.generateDeleteByFieldSQL();
		this.generateGetSQL();
		this.generateDefaultSortingCriteria();
	}

	public DataSource getDataSource() {
		return dataSource;
	}
	
	private void checkAutoGeneration(DAOManaged daoManaged) {

		if (daoManaged.autoGenerated()) {

			throw new RuntimeException("Fields with relations cannot be auto generated!");
		}
	}

	@SuppressWarnings("unchecked")
	public <QPT> QueryParameterPopulator<QPT> getQueryParameterPopulator(Class<QPT> type) {

		if (queryParameterPopulators != null) {

			for (QueryParameterPopulator<?> queryParameterPopulator : queryParameterPopulators) {

				if (type.equals(queryParameterPopulator.getType())) {

					return (QueryParameterPopulator<QPT>) queryParameterPopulator;
				}
			}
		}

		for (QueryParameterPopulator<?> queryParameterPopulator : QueryParameterPopulatorRegistery.getQueryParameterPopulators()) {

			if (type.equals(queryParameterPopulator.getType())) {

				return (QueryParameterPopulator<QPT>) queryParameterPopulator;
			}
		}

		return null;
	}

	private void checkOrderByAnnotation(Field field, OrderBy orderBy) {

		if (orderBy != null) {

			throw new RuntimeException("Invalid @OrderBy annotation on field " + field.getName() + " in " + this.beanClass + ", the @OrderBy annotation is not allowed on @OneToMany and @ManyToMany annotated fields.");
		}
	}

	private void generateDefaultSortingCriteria() {

		if (!this.columnOrderMap.isEmpty()) {

			StringBuilder stringBuilder = new StringBuilder(" ORDER BY ");

			boolean first = true;

			for (Entry<OrderBy, Column<T, ?>> entry : this.columnOrderMap.entrySet()) {

				if (first) {

					first = false;

				} else {

					stringBuilder.append(", ");
				}

				stringBuilder.append(entry.getValue().getColumnName() + " " + entry.getKey().order().toString());
			}

			this.defaultSortingCriteria = stringBuilder.toString();

		} else {

			this.defaultSortingCriteria = "";
		}
	}

	protected void generateInsertSQL() {

		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append("INSERT INTO " + this.tableName + "(");

		boolean first = true;

		for (Column<T, ?> column : this.columnMap.values()) {

			if (first) {

				first = false;

			} else {

				stringBuilder.append(", ");
			}

			stringBuilder.append(column.getColumnName());
		}

		stringBuilder.append(") VALUES (");

		first = true;

		for (@SuppressWarnings("unused")
				Column<T, ?> column : this.columnMap.values()) {

			if (first) {

				first = false;

			} else {

				stringBuilder.append(", ");
			}

			stringBuilder.append("?");
		}

		stringBuilder.append(")");

		this.insertSQL = stringBuilder.toString();
	}

	protected void generateUpdateSQL() {

		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append("UPDATE " + this.tableName + " SET ");

		boolean first = true;

		for (Column<T, ?> column : this.columnMap.values()) {

			if (first) {

				first = false;

			} else {

				stringBuilder.append(", ");
			}

			stringBuilder.append(column.getColumnName() + " = ?");
		}

		stringBuilder.append(" WHERE ");

		this.appendPrimaryKeyWhereStatement(stringBuilder);

		this.updateSQL = stringBuilder.toString();
	}

	protected void generateCheckIfExistsSQL() {

		StringBuilder stringBuilder = new StringBuilder("SELECT 1 FROM " + tableName + " WHERE ");

		this.appendPrimaryKeyWhereStatement(stringBuilder);

		this.checkIfExistsSQL = stringBuilder.toString();
	}

	private void appendPrimaryKeyWhereStatement(StringBuilder stringBuilder) {

		boolean first = true;

		for (Column<T, ?> column : this.simpleKeys) {

			if (first) {

				first = false;

			} else {

				stringBuilder.append(" AND ");
			}

			stringBuilder.append(column.getColumnName() + " = ?");
		}

		for (Column<T, ?> column : this.manyToOneRelationKeys.values()) {

			if (first) {

				first = false;

			} else {

				stringBuilder.append(" AND ");
			}

			stringBuilder.append(column.getColumnName() + " = ?");
		}
	}

	protected void generateDeleteSQL() {

		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append("DELETE FROM ");
		stringBuilder.append(tableName);
		stringBuilder.append(" WHERE ");

		this.appendPrimaryKeyWhereStatement(stringBuilder);

		this.deleteSQL = stringBuilder.toString();
	}

	protected void generateDeleteByFieldSQL() {

		this.deleteByFieldSQL = "DELETE FROM " + tableName;
	}

	protected void generateGetSQL() {

		this.getSQL = "SELECT * FROM " + tableName;
	}

	public TransactionHandler createTransaction() throws SQLException {

		return new TransactionHandler(dataSource);
	}

	public void add(T bean) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.add(bean, transactionHandler.getConnection(), null);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void add(T bean, RelationQuery relationQuery) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.add(bean, transactionHandler.getConnection(), relationQuery);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void addAll(Collection<T> beans, RelationQuery relationQuery) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.addAll(beans, transactionHandler.getConnection(), relationQuery);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void add(T bean, TransactionHandler transactionHandler, RelationQuery relationQuery) throws SQLException {

		this.add(bean, transactionHandler.getConnection(), relationQuery);
	}

	public void addAll(List<T> beans, TransactionHandler transactionHandler, RelationQuery relationQuery) throws SQLException {

		this.addAll(beans, transactionHandler.getConnection(), relationQuery);
	}

	public void addAll(Collection<T> beans, Connection connection, RelationQuery relationQuery) throws SQLException {

		for (T bean : beans) {

			this.add(bean, connection, relationQuery);
		}
	}

	public void add(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		this.preAddRelations(bean, connection, relationQuery);

		UpdateQuery query = null;

		try {

			query = new UpdateQuery(connection, false, this.insertSQL);

			IntegerCounter integerCounter = new IntegerCounter();

			setQueryValues(bean, query, integerCounter, this.columnMap.values());

			this.executeUpdateQuery(query, bean);

			this.addRelations(bean, connection, relationQuery);

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	private void executeUpdateQuery(UpdateQuery query, T bean) throws SQLException {

		if (!this.columnKeyCollectors.isEmpty()) {

			query.executeUpdate(new ColumnKeyCollectorWrapper<T>(this.columnKeyCollectors, bean));

		} else {

			query.executeUpdate();
		}
	}

	private void preAddRelations(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				preAddRelation(bean, connection, relationQuery, relation);
			}
		}

		if (relationQuery == null || !relationQuery.isDisableAutoRelations()) {

			for (Field relation : autoAddRelations) {

				if (relationQuery == null || (!relationQuery.containsRelation(relation) && !relationQuery.containsExcludedRelation(relation))) {

					preAddRelation(bean, connection, relationQuery, relation);
				}
			}
		}
	}

	private void preAddRelation(T bean, Connection connection, RelationQuery relationQuery, Field relation) throws SQLException {

		ManyToOneRelation<T, ?, ?> manyToOneRelation = this.manyToOneRelations.get(relation);

		if (manyToOneRelation != null) {

			manyToOneRelation.add(bean, connection, relationQuery);
			return;
		}

		manyToOneRelation = this.manyToOneRelationKeys.get(relation);

		if (manyToOneRelation != null) {

			manyToOneRelation.add(bean, connection, relationQuery);
		}
	}

	private void addRelations(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				addRelation(bean, connection, relationQuery, relation);
			}
		}

		if (relationQuery == null || !relationQuery.isDisableAutoRelations()) {
			for (Field relation : autoAddRelations) {

				if (relationQuery == null || (!relationQuery.containsRelation(relation) && !relationQuery.containsExcludedRelation(relation))) {

					addRelation(bean, connection, relationQuery, relation);
				}
			}
		}

	}

	private void addRelation(T bean, Connection connection, RelationQuery relationQuery, Field relation) throws SQLException {

		OneToManyRelation<T, ?> oneToManyRelation = this.oneToManyRelations.get(relation);

		if (oneToManyRelation != null) {

			oneToManyRelation.add(bean, connection, relationQuery);
			return;
		}

		ManyToManyRelation<T, ?> manyToManyRelation = this.manyToManyRelations.get(relation);

		if (manyToManyRelation != null) {

			manyToManyRelation.add(bean, connection, relationQuery);
		}
	}

	private void preUpdateRelations(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				preUpdateRelation(bean, connection, relationQuery, relation);
			}
		}

		if (relationQuery == null || !relationQuery.isDisableAutoRelations()) {

			for (Field relation : autoUpdateRelations) {

				if (relationQuery == null || (relationQuery == null || (!relationQuery.containsRelation(relation) && !relationQuery.containsExcludedRelation(relation)))) {

					preUpdateRelation(bean, connection, relationQuery, relation);
				}
			}
		}
	}

	private void preUpdateRelation(T bean, Connection connection, RelationQuery relationQuery, Field relation) throws SQLException {

		ManyToOneRelation<T, ?, ?> manyToOneRelation = this.manyToOneRelations.get(relation);

		if (manyToOneRelation != null) {

			manyToOneRelation.update(bean, connection, relationQuery);
			return;
		}

		manyToOneRelation = this.manyToOneRelationKeys.get(relation);

		if (manyToOneRelation != null) {

			manyToOneRelation.update(bean, connection, relationQuery);
		}
	}

	private void updateRelations(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				updateRelation(bean, connection, relationQuery, relation);
			}
		}

		if (relationQuery == null || !relationQuery.isDisableAutoRelations()) {

			for (Field relation : autoUpdateRelations) {

				if (relationQuery == null || (!relationQuery.containsRelation(relation) && !relationQuery.containsExcludedRelation(relation))) {

					updateRelation(bean, connection, relationQuery, relation);
				}
			}
		}
	}

	private void updateRelation(T bean, Connection connection, RelationQuery relationQuery, Field relation) throws SQLException {

		OneToManyRelation<T, ?> oneToManyRelation = this.oneToManyRelations.get(relation);

		if (oneToManyRelation != null) {

			oneToManyRelation.update(bean, connection, relationQuery);
			return;
		}

		ManyToManyRelation<T, ?> manyToManyRelation = this.manyToManyRelations.get(relation);

		if (manyToManyRelation != null) {

			manyToManyRelation.update(bean, connection, relationQuery);
		}
	}

	private void setQueryValues(T bean, PreparedStatementQuery query, IntegerCounter integerCounter, Collection<? extends Column<T, ?>> columns) throws SQLException {

		for (Column<T, ?> column : columns) {

			if (column.getQueryParameterPopulator() != null) {

				column.getQueryParameterPopulator().populate(query, integerCounter.increment(), column.getBeanValue(bean));

			} else {

				try {
					column.getQueryMethod().invoke(query, integerCounter.increment(), column.getBeanValue(bean));

				} catch (IllegalArgumentException e) {

					throw new RuntimeException(e);

				} catch (IllegalAccessException e) {

					throw new RuntimeException(e);

				} catch (InvocationTargetException e) {

					throw new RuntimeException(e);
				}
			}
		}
	}

	private void setQueryValues(List<T> beans, PreparedStatementQuery query, IntegerCounter integerCounter, Collection<? extends Column<T, ?>> columns, Field excludedField) throws SQLException {

		for (Column<T, ?> column : columns) {

			if(column.getBeanField().equals(excludedField)) {
				continue;
			}

			for (T bean : beans) {

				if (column.getQueryParameterPopulator() != null) {

					column.getQueryParameterPopulator().populate(query, integerCounter.increment(), column.getBeanValue(bean));

				} else {

					try {
						column.getQueryMethod().invoke(query, integerCounter.increment(), column.getBeanValue(bean));

					} catch (IllegalArgumentException e) {

						throw new RuntimeException(e);

					} catch (IllegalAccessException e) {

						throw new RuntimeException(e);

					} catch (InvocationTargetException e) {

						throw new RuntimeException(e);
					}
				}
			}
		}
	}

	public void addOrUpdateAll(Collection<T> beans, TransactionHandler transactionHandler, RelationQuery relationQuery) throws SQLException {
		this.addOrUpdateAll(beans, transactionHandler.getConnection(), relationQuery);
	}
	
	public void addOrUpdateAll(Collection<T> beans, Connection connection, RelationQuery relationQuery) throws SQLException {

		for (T bean : beans) {
			this.addOrUpdate(bean, connection, relationQuery);
		}
	}

	public void addOrUpdate(T bean, RelationQuery relationQuery) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.addOrUpdate(bean, transactionHandler.getConnection(), relationQuery);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void addOrUpdate(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (!isNewBean(bean, connection)) {

			this.update(bean, connection, relationQuery);

		} else {

			this.add(bean, connection, relationQuery);
		}
	}

	public boolean isNewBean(T bean, Connection connection) throws SQLException {

		if(!hasKeysSet(bean)){

			return true;
		}

		//Unable to determine if beans is new or not so far, do db query to check
		return !beanExists(bean, connection);
	}

	public boolean hasKeysSet(T bean) throws SQLException {

		for (Column<T, ?> column : this.simpleKeys) {

			if (column.getBeanValue(bean) == null) {

				//Key not set, presume new bean
				return false;
			}
		}

		for (Column<T, ?> column : this.manyToOneRelationKeys.values()) {

			if (column.getBeanValue(bean) == null) {

				//Key not set, presume new bean
				return false;
			}
		}

		return true;
	}

	public boolean beanExists(T bean) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return beanExists(bean, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public boolean beanExists(T bean, TransactionHandler transactionHandler) throws SQLException {

		return beanExists(bean, transactionHandler.getConnection());
	}

	public boolean beanExists(T bean, Connection connection) throws SQLException {

		BooleanQuery query = null;

		try {

			query = new BooleanQuery(connection, false, this.checkIfExistsSQL);

			IntegerCounter integerCounter = new IntegerCounter();

			// Keys from SimpleColumns for where statement
			this.setQueryValues(bean, query, integerCounter, this.simpleKeys);

			// Keys from many to one relations for where statement
			this.setQueryValues(bean, query, integerCounter, this.manyToOneRelationKeys.values());

			return query.executeQuery();

		} finally {

			BooleanQuery.autoCloseQuery(query);
		}
	}

	public void update(T bean) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.update(bean, transactionHandler.getConnection(), null);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void update(T bean, RelationQuery relationQuery) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.update(bean, transactionHandler.getConnection(), relationQuery);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void update(T bean, TransactionHandler transactionHandler, RelationQuery relationQuery) throws SQLException {

		this.update(bean, transactionHandler.getConnection(), relationQuery);
	}

	public Integer update(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		UpdateQuery query = null;

		try {

			this.preUpdateRelations(bean, connection, relationQuery);

			query = new UpdateQuery(connection, false, this.updateSQL);

			IntegerCounter integerCounter = new IntegerCounter();

			// All fields
			this.setQueryValues(bean, query, integerCounter, this.columnMap.values());

			// Keys from SimpleColumns for where statement
			this.setQueryValues(bean, query, integerCounter, this.simpleKeys);

			// Keys from many to one relations for where statement
			this.setQueryValues(bean, query, integerCounter, this.manyToOneRelationKeys.values());

			query.executeUpdate();

		} finally {

			UpdateQuery.autoCloseQuery(query);
		}

		this.updateRelations(bean, connection, relationQuery);

		return query.getAffectedRows();
	}

	public void update(List<T> beans, RelationQuery relationQuery) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.update(beans, transactionHandler.getConnection(), relationQuery);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void update(List<T> beans, TransactionHandler transactionHandler, RelationQuery relationQuery) throws SQLException {

		this.update(beans, transactionHandler.getConnection(), relationQuery);
	}	
	
	public void update(List<T> beans, Connection connection, RelationQuery relationQuery) throws SQLException {
		
		for(T bean : beans){
			
			update(bean, connection, relationQuery);
		}
	}
	
	public void update(LowLevelQuery<T> lowLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			this.update(lowLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public void update(LowLevelQuery<T> lowLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		this.update(lowLevelQuery, transactionHandler.getConnection());
	}

	public void update(LowLevelQuery<T> lowLevelQuery, Connection connection) throws SQLException {

		UpdateQuery query = null;

		try {

			query = new UpdateQuery(connection, false, lowLevelQuery.getSql());

			this.setCustomQueryParameters(query, lowLevelQuery.getParameters());

			if (lowLevelQuery.getGeneratedKeyCollectors() != null) {

				query.executeUpdate(lowLevelQuery.getGeneratedKeyCollectors());

			} else {

				query.executeUpdate();
			}

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public T get(LowLevelQuery<T> lowLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.get(lowLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public T get(LowLevelQuery<T> lowLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.get(lowLevelQuery, transactionHandler.getConnection());
	}

	public T get(LowLevelQuery<T> lowLevelQuery, Connection connection) throws SQLException {

		BeanResultSetPopulator<T> populator = this.getPopulator(connection, lowLevelQuery);

		ObjectQuery<T> query = null;

		try {

			query = new ObjectQuery<T>(connection, false, lowLevelQuery.getSql(), populator);

			this.setCustomQueryParameters(query, lowLevelQuery.getParameters());

			T bean = query.executeQuery();

			if (bean != null && (RelationQuery.hasRelations(lowLevelQuery) || !autoGetRelations.isEmpty())) {

				this.populateRelations(bean, connection, lowLevelQuery);
			}

			return bean;

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}









	public boolean getBoolean(LowLevelQuery<T> lowLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.getBoolean(lowLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public boolean getBoolean(LowLevelQuery<T> lowLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.getBoolean(lowLevelQuery, transactionHandler.getConnection());
	}

	public boolean getBoolean(LowLevelQuery<T> lowLevelQuery, Connection connection) throws SQLException {

		BooleanQuery query = null;

		try {

			query = new BooleanQuery(connection, false, lowLevelQuery.getSql());

			this.setCustomQueryParameters(query, lowLevelQuery.getParameters());

			return query.executeQuery();

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}




	public T get(HighLevelQuery<T> highLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.get(highLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public T get(HighLevelQuery<T> highLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.get(highLevelQuery, transactionHandler.getConnection());
	}

	public T get(HighLevelQuery<T> highLevelQuery, Connection connection) throws SQLException {

		BeanResultSetPopulator<T> populator = this.getPopulator(connection, highLevelQuery);

		ObjectQuery<T> query = null;

		try {
			query = new ObjectQuery<T>(connection, false, this.getSQL + this.getCriterias(highLevelQuery, true), populator);

			if (highLevelQuery.getParameters() != null) {

				setQueryParameters(query, highLevelQuery, 1);
			}

			T bean = query.executeQuery();

			if (bean != null && (RelationQuery.hasRelations(highLevelQuery) || !autoGetRelations.isEmpty())) {

				this.populateRelations(bean, connection, highLevelQuery);
			}

			return bean;

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public boolean getBoolean(HighLevelQuery<T> highLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.getBoolean(highLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public boolean getBoolean(HighLevelQuery<T> highLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.getBoolean(highLevelQuery, transactionHandler.getConnection());
	}

	public boolean getBoolean(HighLevelQuery<T> highLevelQuery, Connection connection) throws SQLException {

		BooleanQuery query = null;

		try {
			query = new BooleanQuery(connection, false, this.getSQL + this.getCriterias(highLevelQuery, true));

			if (highLevelQuery.getParameters() != null) {

				setQueryParameters(query, highLevelQuery, 1);
			}

			return query.executeQuery();

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	private String getCriterias(HighLevelQuery<T> highLevelQuery, boolean orderBy) {

		if (highLevelQuery == null) {

			if (orderBy) {

				return this.defaultSortingCriteria;

			} else {

				return "";
			}
		}

		StringBuilder stringBuilder = new StringBuilder();

		if (highLevelQuery.getParameters() != null) {

			boolean first = true;

			for (QueryParameter<T, ?> queryParameter : highLevelQuery.getParameters()) {

				if (first) {

					stringBuilder.append(" WHERE ");

					first = false;

				} else {

					stringBuilder.append(" AND ");
				}

				stringBuilder.append(queryParameter.getColumn().getColumnName() + " " + queryParameter.getOperator());

				if (queryParameter.hasValues()) {

					if (queryParameter.hasMultipleValues()) {

						//stringBuilder.append(queryParameter.getOperator() + " " + queryParameter.getColumn().getColumnName());

						stringBuilder.append(" (?");

						if (queryParameter.getValues().size() > 1) {

							StringUtils.repeatString(",?", queryParameter.getValues().size() - 1, stringBuilder);
						}

						stringBuilder.append(")");

					} else {

						stringBuilder.append(" ?");
					}
				}
			}
		}

		if (orderBy && highLevelQuery.getOrderByCriterias() != null) {

			boolean first = true;

			for (OrderByCriteria<T> criteria : highLevelQuery.getOrderByCriterias()) {

				if (first) {

					stringBuilder.append(" ORDER BY " + criteria.getColumn().getColumnName() + " " + criteria.getOrder().toString());

					first = false;

				} else {

					stringBuilder.append(", " + criteria.getColumn().getColumnName() + " " + criteria.getOrder().toString());
				}
			}

		} else {

			stringBuilder.append(this.defaultSortingCriteria);
		}

		return stringBuilder.toString();
	}

	@SuppressWarnings("unchecked")
	private <ColumnType> Column<T, ? super ColumnType> getColumn(Field field, Class<ColumnType> paramClass) {

		Column<T, ?> column = this.columnMap.get(field);

		if (column == null) {

			throw new RuntimeException("Field " + field + " not found in  " + this.beanClass + "!");

		} else if (!column.getParamType().isAssignableFrom(paramClass)) {

			throw new RuntimeException(" " + paramClass + " is not compatible with type " + column.getParamType() + " of field " + field + " in  "
					+ this.beanClass + "!");
		}

		return (Column<T, ColumnType>) column;
	}

	protected BeanResultSetPopulator<T> getPopulator(Connection connection, LowLevelQuery<T> query) {

		BeanResultSetPopulator<T> populator = getPopulator(connection, (RelationQuery)query);

		if(query.hasChainedBeanResultSetPopulators()){

			return new BeanChainPopulator<T>(query.getChainedResultSetPopulators(), populator);
		}

		return populator;
	}

	protected BeanResultSetPopulator<T> getPopulator(Connection connection, RelationQuery relationQuery) {

		ArrayList<ManyToOneRelation<T, ?, ?>> manyToOneRelations = null;

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				ManyToOneRelation<T, ?, ?> manyToOneRelation = this.manyToOneRelations.get(relation);

				if (manyToOneRelation == null) {

					manyToOneRelation = this.manyToOneRelationKeys.get(relation);
				}

				if (manyToOneRelation != null) {

					if (manyToOneRelations == null) {

						manyToOneRelations = new ArrayList<ManyToOneRelation<T, ?, ?>>();
					}

					manyToOneRelations.add(manyToOneRelation);
				}
			}
		}

		if((!autoGetRelations.isEmpty() && (relationQuery == null || !relationQuery.isDisableAutoRelations()))){

			for (Field relation : autoGetRelations) {

				ManyToOneRelation<T, ?, ?> manyToOneRelation = this.manyToOneRelations.get(relation);

				if (manyToOneRelation == null) {

					manyToOneRelation = this.manyToOneRelationKeys.get(relation);
				}

				if (manyToOneRelation != null) {

					if (manyToOneRelations == null) {

						manyToOneRelations = new ArrayList<ManyToOneRelation<T, ?, ?>>();
					}

					manyToOneRelations.add(manyToOneRelation);
				}
			}
		}

		if (manyToOneRelations != null) {

			return new BeanRelationPopulator<T>(this.populator, manyToOneRelations, connection, relationQuery);
		}

		return this.populator;
	}

	protected void populateRelations(T bean, Connection connection, RelationQuery relationQuery) throws SQLException {

		if (RelationQuery.hasRelations(relationQuery)) {

			for (Field relation : relationQuery.getRelations()) {

				populateRelation(bean, connection, relationQuery, relation);
			}
		}

		if (relationQuery == null || !relationQuery.isDisableAutoRelations()) {

			for (Field relation : autoGetRelations) {

				if (relationQuery == null || (!relationQuery.containsRelation(relation) && !relationQuery.containsExcludedRelation(relation))) {

					populateRelation(bean, connection, relationQuery, relation);
				}
			}
		}
	}

	protected void populateRelation(T bean, Connection connection, RelationQuery relationQuery, Field relation) throws SQLException {

		OneToManyRelation<T, ?> oneToManyRelation = this.oneToManyRelations.get(relation);

		if (oneToManyRelation != null) {

			oneToManyRelation.getRemoteValue(bean, connection, relationQuery);
			return;
		}

		ManyToManyRelation<T, ?> manyToManyRelation = this.manyToManyRelations.get(relation);

		if (manyToManyRelation != null) {

			manyToManyRelation.getRemoteValue(bean, connection, relationQuery);
		}
	}

	public List<T> getAll(LowLevelQuery<T> lowLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.getAll(lowLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public List<T> getAll(LowLevelQuery<T> lowLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.getAll(lowLevelQuery, transactionHandler.getConnection());
	}

	public List<T> getAll(LowLevelQuery<T> lowLevelQuery, Connection connection) throws SQLException {

		BeanResultSetPopulator<T> populator = this.getPopulator(connection, lowLevelQuery);

		ArrayListQuery<T> query = null;

		try {

			query = new ArrayListQuery<T>(connection, false, lowLevelQuery.getSql(), populator);

			setCustomQueryParameters(query, lowLevelQuery.getParameters());

			ArrayList<T> beans = query.executeQuery();

			if (beans != null && (RelationQuery.hasRelations(lowLevelQuery) || !autoGetRelations.isEmpty())) {

				for (T bean : beans) {

					this.populateRelations(bean, connection, lowLevelQuery);
				}
			}

			return beans;

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	private void setCustomQueryParameters(PreparedStatementQuery query, List<?> parameters) {

		if (parameters != null) {

			int i = 1;

			for (Object object : parameters) {

				Method queryMethod;

				if (object == null) {

					queryMethod = PreparedStatementQueryMethods.getObjectQueryMethod();

				} else if(object.getClass().isEnum()){

					object = object.toString();

					queryMethod = PreparedStatementQueryMethods.getQueryMethod(String.class);

				}else {

					queryMethod = PreparedStatementQueryMethods.getQueryMethod(object.getClass());
				}

				if (queryMethod == null) {
					throw new RuntimeException("Unable to find suitable prepared statement query method for parameter " + object.getClass());
				}

				try {
					queryMethod.invoke(query, i++, object);

				} catch (IllegalArgumentException e) {

					throw new RuntimeException(e);

				} catch (IllegalAccessException e) {

					throw new RuntimeException(e);

				} catch (InvocationTargetException e) {

					throw new RuntimeException(e);
				}
			}
		}
	}

	protected List<T> getAll(String sql, CustomQueryParameter<?> queryParameter, Connection connection,
			RelationQuery relationQuery) throws SQLException {

		BeanResultSetPopulator<T> populator = this.getPopulator(connection, relationQuery);

		ArrayListQuery<T> query = null;

		try {

			query = new ArrayListQuery<T>(connection, false, sql, populator);

			if (queryParameter.getQueryParameterPopulator() != null) {

				queryParameter.getQueryParameterPopulator().populate(query, 1, queryParameter.getParamValue());

			} else {

				try {
					queryParameter.getQueryMethod().invoke(query, 1, queryParameter.getParamValue());

				} catch (IllegalArgumentException e) {

					throw new RuntimeException(e);

				} catch (IllegalAccessException e) {

					throw new RuntimeException(e);

				} catch (InvocationTargetException e) {

					throw new RuntimeException(e);
				}
			}

			ArrayList<T> beans = query.executeQuery();

			if (beans != null && (RelationQuery.hasRelations(relationQuery) || !autoGetRelations.isEmpty())) {

				for (T bean : beans) {

					this.populateRelations(bean, connection, relationQuery);
				}
			}

			return beans;

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public List<T> getAll(HighLevelQuery<T> highLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.getAll(highLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public List<T> getAll(HighLevelQuery<T> highLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.getAll(highLevelQuery, transactionHandler.getConnection());
	}

	public List<T> getAll(HighLevelQuery<T> highLevelQuery, Connection connection)
	throws SQLException {

		BeanResultSetPopulator<T> populator = this.getPopulator(connection, highLevelQuery);

		ArrayListQuery<T> query = null;

		try {
			query = new ArrayListQuery<T>(connection, false, this.getSQL + this.getCriterias(highLevelQuery, true), populator);

			setQueryParameters(query, highLevelQuery, 1);

			ArrayList<T> beans = query.executeQuery();

			if (beans != null && (RelationQuery.hasRelations(highLevelQuery) || !autoGetRelations.isEmpty())) {

				for (T bean : beans) {

					this.populateRelations(bean, connection, highLevelQuery);
				}
			}

			return beans;

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public List<T> getAll() throws SQLException {

		return this.getAll((HighLevelQuery<T>) null);
	}

	public void delete(T bean) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.delete(bean, transactionHandler);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void delete(T bean, TransactionHandler transactionHandler) throws SQLException {

		this.delete(bean, transactionHandler.getConnection());
	}

	public void delete(T bean, Connection connection) throws SQLException {

		UpdateQuery query = null;

		try {

			query = new UpdateQuery(connection, false, this.deleteSQL);

			IntegerCounter integerCounter = new IntegerCounter();

			// Keys from SimpleColumns for where statement
			this.setQueryValues(bean, query, integerCounter, this.simpleKeys);

			// Keys from many to one relations for where statement
			this.setQueryValues(bean, query, integerCounter, this.manyToOneRelationKeys.values());

			query.executeUpdate();

		} finally {
			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public void delete(List<T> beans) throws SQLException {

		TransactionHandler transactionHandler = null;

		try {

			transactionHandler = new TransactionHandler(dataSource);

			this.delete(beans, transactionHandler);

			transactionHandler.commit();
		} finally {
			TransactionHandler.autoClose(transactionHandler);
		}
	}

	public void delete(List<T> beans, TransactionHandler transactionHandler) throws SQLException {

		this.delete(beans, transactionHandler.getConnection());
	}

	public void delete(List<T> beans, Connection connection) throws SQLException {
		
		for(T bean : beans){
			
			delete(bean, connection);
		}
	}
	
	public Integer delete(HighLevelQuery<T> highLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return this.delete(highLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public Integer delete(HighLevelQuery<T> highLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return this.delete(highLevelQuery, transactionHandler.getConnection());
	}

	public Integer delete(HighLevelQuery<T> highLevelQuery, Connection connection) throws SQLException {

		UpdateQuery query = null;

		try {
			query = new UpdateQuery(connection, false, this.deleteByFieldSQL + this.getCriterias(highLevelQuery, false));

			setQueryParameters(query, highLevelQuery, 1);

			query.executeUpdate();

			return query.getAffectedRows();

		} finally {

			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	public boolean deleteWhereNotIn(List<T> beans, TransactionHandler transactionHandler, Field excludedField, QueryParameter<T, ?>... queryParameters) throws SQLException {
		return this.deleteWhereNotIn(beans, transactionHandler.getConnection(), excludedField, queryParameters);
	}
		
	public boolean deleteWhereNotIn(List<T> beans, Connection connection, Field excludedField, QueryParameter<T, ?>... queryParameters)
	throws SQLException {

		// Generate SQL
		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append("DELETE FROM ");
		stringBuilder.append(tableName);
		stringBuilder.append(" WHERE");

		//Filter away any beans that don't have their keys set
		ArrayList<T> filteredBeans = new ArrayList<T>(beans);

		Iterator<T> iterator = filteredBeans.iterator();

		while(iterator.hasNext()){

			if(!hasKeysSet(iterator.next())){

				iterator.remove();
			}
		}

		if(filteredBeans.isEmpty()){

			//No beans left, abort
			return false;
		}

		int beanCount = filteredBeans.size();

		BooleanSignal signal = new BooleanSignal();

		this.generateColumWhereNotInSQL(this.simpleKeys, excludedField, stringBuilder, beanCount, signal);
		this.generateColumWhereNotInSQL(this.manyToOneRelationKeys.values(), excludedField, stringBuilder, beanCount, signal);

		if (queryParameters != null) {

			for (QueryParameter<T, ?> queryParameter : queryParameters) {

				stringBuilder.append(" AND " + queryParameter.getColumn().getColumnName() + " " + queryParameter.getOperator() + " ?");
			}
		}

		UpdateQuery query = null;

		try {

			query = new UpdateQuery(connection, false, stringBuilder.toString());

			IntegerCounter integerCounter = new IntegerCounter();

			// Keys values from SimpleColumns for where not in statement
			this.setQueryValues(filteredBeans, query, integerCounter, this.simpleKeys, excludedField);

			// Keys values from many to one relations for where not in statement
			this.setQueryValues(filteredBeans, query, integerCounter, this.manyToOneRelationKeys.values(), excludedField);

			if (queryParameters != null) {

				// Set query param values
				this.setQueryParameters(query, new HighLevelQuery<T>(queryParameters), integerCounter.increment());
			}

			query.executeUpdate();

		} finally {
			PreparedStatementQuery.autoCloseQuery(query);
		}

		return true;
	}

	public Integer getCount(HighLevelQuery<T> highLevelQuery) throws SQLException {

		Connection connection = null;

		try {

			connection = this.dataSource.getConnection();

			return getCount(highLevelQuery, connection);

		} finally {
			DBUtils.closeConnection(connection);
		}
	}

	public Integer getCount(HighLevelQuery<T> highLevelQuery, TransactionHandler transactionHandler) throws SQLException {

		return getCount(highLevelQuery, transactionHandler.getConnection());
	}

	public Integer getCount(HighLevelQuery<T> highLevelQuery, Connection connection) throws SQLException {

		ObjectQuery<Integer> query = null;

		try{
			query = new ObjectQuery<Integer>(connection, false, "SELECT COUNT(*) FROM " + tableName + this.getCriterias(highLevelQuery, false), IntegerPopulator.getPopulator());

			if (highLevelQuery != null && highLevelQuery.getParameters() != null) {

				setQueryParameters(query, highLevelQuery, 1);
			}

			return query.executeQuery();

		}finally{
			PreparedStatementQuery.autoCloseQuery(query);
		}
	}

	private void generateColumWhereNotInSQL(Collection<? extends Column<T, ?>> keyColumns, Field excludedField, StringBuilder stringBuilder, int beanCount, BooleanSignal signal) {

		for (Column<T, ?> column : keyColumns) {

			if(column.getBeanField().equals(excludedField)) {
				continue;
			}

			if (signal.isSignal()) {

				stringBuilder.append(" AND");

			} else {

				signal.setSignal(true);
			}

			stringBuilder.append(" ");
			stringBuilder.append(column.getColumnName());
			stringBuilder.append(" NOT IN (");

			this.addQuestionMarks(beanCount, stringBuilder);

			stringBuilder.append(")");
		}

	}

	private void addQuestionMarks(int size, StringBuilder stringBuilder) {

		stringBuilder.append("?");

		if (size > 1) {

			for (int i = 2; i <= size; i++) {

				stringBuilder.append(",?");
			}
		}
	}

	public <ParamType> QueryParameterFactory<T, ParamType> getParamFactory(Field field, Class<ParamType> paramClass) {

		return new QueryParameterFactory<T, ParamType>(this.getColumn(field, paramClass));
	}

	public <ParamType> QueryParameterFactory<T, ParamType> getParamFactory(String fieldName, Class<ParamType> paramClass) {

		Field field = ReflectionUtils.getField(beanClass, fieldName);

		if (field == null) {
			throw new RuntimeException("Field " + fieldName + " not found in  " + this.beanClass + "!");
		}

		return new QueryParameterFactory<T, ParamType>(this.getColumn(field, paramClass));
	}

	public OrderByCriteria<T> getOrderByCriteria(Field field, Order order) {

		Column<T, ?> column = this.columnMap.get(field);

		if (column == null) {
			throw new RuntimeException("No @DAOManaged annotated field with name " + field.getName() + " not found in  " + this.beanClass + "!");
		}

		return new OrderByCriteria<T>(order, column);
	}

	public OrderByCriteria<T> getOrderByCriteria(String fieldName, Order order) {

		Field field = ReflectionUtils.getField(beanClass, fieldName);

		if (field == null) {
			throw new RuntimeException("Field " + fieldName + " not found in  " + this.beanClass + "!");
		}

		return this.getOrderByCriteria(field, order);
	}

	public String getTableName() {

		return tableName;
	}

	Column<T, ?> getColumn(Field field) {

		return this.columnMap.get(field);
	}

	public void setQueryParameters(PreparedStatementQuery query, HighLevelQuery<T> highLevelQuery, final int startIndex) throws SQLException {

		if (highLevelQuery != null && highLevelQuery.getParameters() != null) {

			int index = startIndex;

			for (QueryParameter<?, ?> queryParameter : highLevelQuery.getParameters()) {

				if (queryParameter.hasValues()) {

					if (queryParameter.hasMultipleValues()) {

						for (Object value : queryParameter.getValues()) {

							setQueryParameter(queryParameter, value, query, index++);
						}

					} else {

						setQueryParameter(queryParameter, queryParameter.getValue(), query, index++);
					}
				}
			}
		}
	}

	private void setQueryParameter(QueryParameter<?, ?> queryParameter, Object value, PreparedStatementQuery query, int index) throws SQLException {

		Column<?, ?> column = queryParameter.getColumn();

		if (column.getQueryParameterPopulator() != null) {

			column.getQueryParameterPopulator().populate(query, index, column.getParamValue(value));

		} else {

			try {
				column.getQueryMethod().invoke(query, index, column.getParamValue(value));

			} catch (IllegalArgumentException e) {

				throw new RuntimeException(e);

			} catch (IllegalAccessException e) {

				throw new RuntimeException(e);

			} catch (InvocationTargetException e) {

				throw new RuntimeException(e);
			}
		}
	}

	
	public Class<T> getBeanClass() {
	
		return beanClass;
	}
}
