org.springframework.data.crate.core.CrateTemplate.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.crate.core.CrateTemplate.java

Source

/*
 * Copyright 2002-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.data.crate.core;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
import static org.apache.commons.lang3.ArrayUtils.add;
import static org.apache.commons.lang3.ArrayUtils.addAll;
import static org.apache.commons.lang3.ArrayUtils.isEmpty;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.data.crate.core.ActionType.DELETE;
import static org.springframework.data.crate.core.ActionType.INSERT;
import static org.springframework.data.crate.core.ActionType.UPDATE;
import static org.springframework.data.crate.core.ActionType.values;
import static org.springframework.data.crate.core.convert.CrateTypeMapper.DEFAULT_TYPE_KEY;
import static org.springframework.data.crate.core.mapping.CratePersistentProperty.INITIAL_VERSION_VALUE;
import static org.springframework.data.crate.core.mapping.CratePersistentProperty.RESERVED_VESRION_FIELD_NAME;
import static org.springframework.util.Assert.hasText;
import static org.springframework.util.Assert.notEmpty;
import static org.springframework.util.Assert.notNull;
import io.crate.action.sql.SQLActionException;
import io.crate.action.sql.SQLBulkRequest;
import io.crate.action.sql.SQLBulkResponse;
import io.crate.action.sql.SQLBulkResponse.Result;
import io.crate.action.sql.SQLRequest;
import io.crate.action.sql.SQLResponse;
import io.crate.client.CrateClient;
import io.crate.types.DataType;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;

import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.crate.CrateSQLActionException;
import org.springframework.data.crate.core.BulkActionResult.ActionResult;
import org.springframework.data.crate.core.convert.CrateConverter;
import org.springframework.data.crate.core.convert.CrateDocumentConverter;
import org.springframework.data.crate.core.convert.MappingCrateConverter;
import org.springframework.data.crate.core.mapping.CrateDocument;
import org.springframework.data.crate.core.mapping.CrateMappingContext;
import org.springframework.data.crate.core.mapping.CratePersistentEntity;
import org.springframework.data.crate.core.mapping.CratePersistentProperty;
import org.springframework.data.crate.core.mapping.event.AfterConvertEvent;
import org.springframework.data.crate.core.mapping.event.AfterDeleteEvent;
import org.springframework.data.crate.core.mapping.event.AfterLoadEvent;
import org.springframework.data.crate.core.mapping.event.AfterSaveEvent;
import org.springframework.data.crate.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.crate.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.crate.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.crate.core.mapping.event.CrateMappingEvent;
import org.springframework.data.crate.core.sql.AbstractStatement;
import org.springframework.data.crate.core.sql.CrateSQLStatement;
import org.springframework.data.crate.core.sql.Insert;
import org.springframework.data.crate.core.sql.RefreshTable;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.util.StringUtils;

/**
 * @author Hasnain Javed
 * @author Rizwan Idrees
 * @since 1.0.0
 */
public class CrateTemplate implements CrateOperations, ApplicationContextAware {

    private final Logger logger = getLogger(CrateTemplate.class);

    private final CrateClient client;
    private final PersistenceExceptionTranslator exceptionTranslator;
    private CrateConverter crateConverter;
    private ApplicationEventPublisher eventPublisher;

    private static final Collection<ActionType> ALLOWED_BULK_OPERATIONS;

    private static final String PRIMARY_KEY = "Primary Key must not be null";
    private static final String SQL_STATEMENT = "executing statement '{}' with args '{}'";
    private static final String NO_ID_WARNING = "Persitent Entity '{}' has no id property defined. Saving the same instance will result in a duplicate row";
    private static final String BULK_ACTION = "Invalid bulk sql action type '%s'. Allowed types are '%s'";
    private static final String ID_COLUMN = "Persistent Entity '%s' must define an id column";

    static {
        ALLOWED_BULK_OPERATIONS = unmodifiableCollection(asList(INSERT, UPDATE, DELETE));
    }

    public CrateTemplate(CrateClient client) {
        this(client, null);
    }

    public CrateTemplate(CrateClient client, CrateConverter crateConverter) {
        this.client = client;
        this.crateConverter = crateConverter == null ? new MappingCrateConverter(new CrateMappingContext())
                : crateConverter;
        this.exceptionTranslator = new CrateExceptionTranslator();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.eventPublisher = applicationContext;
    }

    @Override
    public CrateConverter getConverter() {
        return this.crateConverter;
    }

    @Override
    public SQLResponse execute(CrateAction action) throws DataAccessException {
        return execute(action, new CrateActionResponseHandler<SQLResponse>() {
            @Override
            public SQLResponse handle(SQLResponse response) {
                return response;
            }
        });
    }

    @Override
    public <T> T execute(CrateAction action, CrateActionResponseHandler<T> handler) throws DataAccessException {

        notNull(action, "An implementation of CrateAction is required");
        notNull(handler, "An implementation of CrateActionResponseHandler<T> is required");

        try {
            SQLRequest request = action.getSQLRequest();
            logger.debug(SQL_STATEMENT, request.stmt(), Arrays.toString(request.args()));
            return (T) handler.handle(client.sql(request).actionGet());
        } catch (SQLActionException e) {
            throw tryConvertingRuntimeException(e);
        }
    }

    @Override
    public <T> BulkOperartionResult<T> execute(CrateBulkAction action, CrateBulkActionResponseHandler<T> handler)
            throws DataAccessException {

        notNull(action, "An implementation of CrateBulkAction is required");
        notNull(action.getActionType(), "Action Type is required");
        notNull(handler, "An implementation of CrateBulkActionResponseHandler<T> is required");

        if (!ALLOWED_BULK_OPERATIONS.contains(action.getActionType())) {
            throw new CrateSQLActionException(format(BULK_ACTION, action.getActionType(), ALLOWED_BULK_OPERATIONS));
        }

        try {
            SQLBulkRequest request = action.getSQLRequest();
            if (logger.isDebugEnabled()) {
                logger.debug(SQL_STATEMENT, request.stmt(), Arrays.deepToString(request.bulkArgs()));
            }
            return handler.handle(client.bulkSql(request).get());
        } catch (SQLActionException e) {
            throw tryConvertingRuntimeException(e);
        } catch (InterruptedException e) {
            throw new CrateSQLActionException(e.getMessage(), e);
        } catch (ExecutionException e) {
            throw new CrateSQLActionException(e.getMessage(), e);
        }
    }

    @Override
    public void insert(Object entity) {

        notNull(entity);
        insert(entity, getTableName(entity.getClass()));
    }

    @Override
    public void insert(Object entity, String tableName) {

        notNull(entity);
        hasText(tableName);

        executeInternal(new InsertAction(entity, tableName));
    }

    @Override
    public <T> BulkOperartionResult<T> bulkInsert(List<T> entities, Class<T> entityClass) {

        notNull(entityClass);
        return bulkInsert(entities, entityClass, getTableName(entityClass));
    }

    @Override
    public <T> BulkOperartionResult<T> bulkInsert(List<T> entities, Class<T> entityClass, String tableName) {

        boolean hasId = isIdPropertyDefined(entityClass);

        if (!hasId) {
            logger.warn(NO_ID_WARNING, entityClass.getName());
        }

        return executeBulkInternal(new BulkInsertOperation<T>(entityClass, tableName, entities));
    }

    @Override
    public void update(Object entity) {

        notNull(entity);
        update(entity, getTableName(entity.getClass()));
    }

    @Override
    public void update(Object entity, String tableName) {

        notNull(entity);
        hasText(tableName);

        executeInternal(new WholesaleUpdateByIdAction(entity, tableName));
    }

    @Override
    public <T> BulkOperartionResult<T> bulkUpdate(List<T> entities, Class<T> entityClass) {

        notNull(entityClass);
        return bulkUpdate(entities, entityClass, getTableName(entityClass));
    }

    @Override
    public <T> BulkOperartionResult<T> bulkUpdate(List<T> entities, Class<T> entityClass, String tableName) {

        notNull(entityClass);
        notEmpty(entities);

        return executeBulkInternal(new BulkUpdateOperation<T>(entityClass, tableName, entities));
    }

    @Override
    public <T> List<T> findAll(Class<T> entityClass) {

        notNull(entityClass);
        return findAll(entityClass, getTableName(entityClass));
    }

    @Override
    public <T> List<T> findAll(Class<T> entityClass, String tableName) {

        notNull(entityClass);
        hasText(tableName);

        return execute(new SelectAction(entityClass, tableName, null), new ReadDbHandler<T>(entityClass));
    }

    @Override
    public <T> T findById(Object id, Class<T> entityClass) {

        notNull(id);
        return findById(id, entityClass, getTableName(entityClass));
    }

    // TODO: re factor when the Criteria API is in place
    @Override
    public <T> T findById(Object id, Class<T> entityClass, String tableName) {

        notNull(id);

        if (!isIdPropertyDefined(entityClass)) {
            throw new MappingException(format("Entity '%s' has no id property defined", entityClass.getName()));
        }

        List<T> dbEntity = execute(new SelectAction(entityClass, tableName, id), new ReadDbHandler<T>(entityClass));
        if (dbEntity.isEmpty()) {
            logger.info("No row found with id '{}'", id);
            return null;
        } else {
            return dbEntity.iterator().next();
        }
    }

    @Override
    public <T> void deleteAll(Class<T> entityClass) {

        notNull(entityClass);
        deleteAll(getTableName(entityClass));
    }

    // TODO: re factor when the Criteria API is in place
    @Override
    public void deleteAll(String tableName) {

        hasText(tableName);

        final String name = tableName;

        execute(new CrateAction() {

            @Override
            public String getSQLStatement() {
                return format("DELETE FROM %s", name);
            }

            @Override
            public SQLRequest getSQLRequest() {
                return new SQLRequest(getSQLStatement());
            }
        });
    }

    @Override
    public <T> boolean delete(Object id, Class<T> entityClass) {

        notNull(entityClass);
        return delete(id, entityClass, getTableName(entityClass));
    }

    @Override
    public <T> boolean delete(Object id, Class<T> entityClass, String tableName) {

        notNull(entityClass);
        hasText(tableName);

        if (id == null) {
            return false;
        }

        DeleteByIdActionHandler actionHandler = new DeleteByIdActionHandler(tableName, entityClass, id);

        return execute(actionHandler, actionHandler);
    }

    @Override
    public <T> BulkOperartionResult<Object> bulkDelete(List<Object> ids, Class<T> entityClass) {

        notEmpty(ids);
        notNull(entityClass);
        return bulkDelete(ids, entityClass, getTableName(entityClass));
    }

    @Override
    public <T> BulkOperartionResult<Object> bulkDelete(List<Object> ids, Class<T> entityClass, String tableName) {

        notEmpty(ids);
        notNull(entityClass);
        hasText(tableName);

        BulkDeleteOperation actionHandler = new BulkDeleteOperation(entityClass, tableName, ids);

        return execute(actionHandler, actionHandler);
    }

    @Override
    public <T> void refreshTable(Class<T> entityClass) {

        notNull(entityClass);
        refreshTable(getTableName(entityClass));
    }

    @Override
    public void refreshTable(String tableName) {

        hasText(tableName);

        logger.info("refreshing table '{}'", tableName);
        this.execute(new RefreshTableAction(tableName));
    }

    protected <T> void maybeEmitEvent(CrateMappingEvent<T> event) {
        if (eventPublisher != null) {
            eventPublisher.publishEvent(event);
        }
    }

    private void executeInternal(WriteDbAction action) {
        action.beforeSave();
        execute(action, action);
    }

    private <T> BulkOperartionResult<T> executeBulkInternal(BaseSQLBulkOperation<T> op) {
        op.beforeSave();
        return execute(op, op);
    }

    private String getTableName(Class<?> clazz) {
        return getPersistentEntityFor(clazz).getTableName();
    }

    private CratePersistentEntity<?> getPersistentEntityFor(Class<?> clazz) {
        notNull(clazz, "Class parameter provided can not be null");
        return crateConverter.getMappingContext().getPersistentEntity(clazz);
    }

    /**
    * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException}. The original
    * exception is returned if the conversion fails.
    * 
    * @param ex
    * @return the translated exception or the thrown exception
    */
    private RuntimeException tryConvertingRuntimeException(RuntimeException ex) {
        RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex);
        return resolved == null ? ex : resolved;
    }

    private boolean isIdPropertyDefined(Class<?> clazz) {
        return getPersistentEntityFor(clazz).hasIdProperty();
    }

    private boolean isVersionPropertyDefined(Class<?> clazz) {
        return getPersistentEntityFor(clazz).hasVersionProperty();
    }

    private CratePersistentProperty getIdPropertyFor(Class<?> type) {
        return getPersistentEntityFor(type).getIdProperty();
    }

    private Object getIdPropertyValue(Object object) {

        CratePersistentEntity<?> entity = getPersistentEntityFor(object.getClass());

        if (entity.hasIdProperty()) {
            return entity.getPropertyAccessor(object).getProperty(entity.getIdProperty());
        } else {
            return null;
        }
    }

    private Long getVersionPropertyValue(Object object) {

        CratePersistentEntity<?> entity = getPersistentEntityFor(object.getClass());

        if (entity.hasVersionProperty()) {
            return (Long) entity.getPropertyAccessor(object).getProperty(entity.getVersionProperty());
        } else {
            return null;
        }
    }

    private void setVersionPropertyValue(Object object, Long versionValue) {

        CratePersistentEntity<?> entity = getPersistentEntityFor(object.getClass());

        if (entity.hasVersionProperty()) {
            entity.getPropertyAccessor(object).setProperty(entity.getVersionProperty(), versionValue);
        }
    }

    private void validateIdValue(Object entity) {

        Object idValue = getIdPropertyValue(entity);

        if (idValue == null) {
            throw new MappingException(PRIMARY_KEY);
        }
    }

    private <T> void doBeforeSave(T entity, CrateDocument document) {

        notNull(document);

        maybeEmitEvent(new BeforeConvertEvent<T>(entity));

        crateConverter.write(entity, document);

        maybeEmitEvent(new BeforeSaveEvent<Object>(entity, document));
    }

    private <T> void doAfterSave(T entity, CrateDocument document) {

        notNull(document);

        if (isVersionPropertyDefined(entity.getClass())) {
            setVersionPropertyValue(entity, INITIAL_VERSION_VALUE);
        }

        maybeEmitEvent(new AfterSaveEvent<T>(entity, document));
    }

    @SuppressWarnings("unchecked")
    private <T> void doAfterUpdate(T entity, CrateDocument document) {

        notNull(document);

        if (isVersionPropertyDefined(entity.getClass())) {

            T dbEntity = findById(getIdPropertyValue(entity), (Class<T>) entity.getClass());

            setVersionPropertyValue(entity, getVersionPropertyValue(dbEntity));
        }

        maybeEmitEvent(new AfterConvertEvent<T>(document, entity));
    }

    private void doBeforeDelete(Object id) {

        notNull(id);
        maybeEmitEvent(new BeforeDeleteEvent<Object>(id));
    }

    private void doAfterDelete(Object id) {

        notNull(id);
        maybeEmitEvent(new AfterDeleteEvent<Object>(id));
    }

    // TODO: create a generic select statement in sql package when Criteria API is in place
    private class Select extends AbstractStatement {

        private String idColumn;
        private String tableName;
        private Set<String> columns;

        public Select(String idColumn, String tableName, Set<String> columns) {

            hasText(tableName);

            this.idColumn = idColumn;
            this.tableName = tableName;
            this.columns = columns != null ? columns : Collections.<String>emptySet();
        }

        @Override
        public String createStatement() {

            if (StringUtils.hasText(statement)) {
                return statement;
            }

            StringBuilder cols = new StringBuilder();

            Iterator<String> iterator = columns.iterator();

            while (iterator.hasNext()) {
                String column = iterator.next();
                cols.append(doubleQuote(column));
                if (iterator.hasNext()) {
                    cols.append(", ");
                }
            }

            String colNames = StringUtils.hasText(cols.toString()) ? cols.toString() : "*";

            StringBuilder sql = new StringBuilder(
                    format("SELECT %s, %s FROM %s", colNames, doubleQuote(RESERVED_VESRION_FIELD_NAME), tableName));
            if (StringUtils.hasText(idColumn)) {
                sql.append(SPACE).append(format("WHERE %s = ?", doubleQuote(idColumn)));
            }

            statement = sql.toString();

            return statement;
        }
    }

    // TODO: create a generic update statement in sql package when Criteria API is in place
    private class Update extends AbstractStatement {

        private String tableName;
        private String idColumn;
        private Set<String> columns;

        public Update(String tableName, String idColumn, Set<String> columns) {

            hasText(tableName);
            hasText(idColumn);
            notEmpty(columns);

            this.idColumn = idColumn;
            this.tableName = tableName;
            this.columns = columns;
        }

        @Override
        public String createStatement() {

            if (StringUtils.hasText(statement)) {
                return statement;
            }

            StringBuilder cols = new StringBuilder();

            Iterator<String> iterator = columns.iterator();

            while (iterator.hasNext()) {
                String column = iterator.next();
                cols.append(doubleQuote(column)).append(" = ?");
                if (iterator.hasNext()) {
                    cols.append(", ");
                }
            }

            statement = format("UPDATE %s set %s WHERE %s = ?", tableName, cols.toString(), doubleQuote(idColumn));

            return statement;
        }
    }

    // TODO: create a generic delete statement in sql package when Criteria API is in place
    private class Delete extends AbstractStatement {

        private String table;
        private String idColumn;

        public Delete(String table, String idColumn) {

            hasText(table);
            hasText(idColumn);

            this.table = table;
            this.idColumn = idColumn;
        }

        @Override
        public String createStatement() {

            if (StringUtils.hasText(statement)) {
                return statement;
            }

            statement = format("DELETE FROM %s WHERE %s = ?", table, doubleQuote(idColumn));

            return statement;
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     */
    private class SelectAction implements CrateAction {

        private CrateSQLStatement select;
        private Object id;

        public SelectAction(Class<?> entityClass, String tableName, Object id) {

            notNull(entityClass);

            this.id = crateConverter.convertToCrateType(id, null);
            this.select = initSelectStatement(entityClass, tableName);
        }

        @Override
        public SQLRequest getSQLRequest() {

            SQLRequest request = new SQLRequest(getSQLStatement());
            request.includeTypesOnResponse(true);

            if (id != null) {
                request.args(new Object[] { id });
            }

            return request;
        }

        @Override
        public String getSQLStatement() {
            return select.createStatement();
        }

        private CrateSQLStatement initSelectStatement(Class<?> entityClass, String tableName) {

            CratePersistentEntity<?> entity = getPersistentEntityFor(entityClass);

            boolean isVersioned = entity.hasVersionProperty();

            Set<String> columns = isVersioned ? entity.getPropertyNames(entity.getVersionProperty().getFieldName())
                    : entity.getPropertyNames();

            String idColumn = id != null ? entity.getIdProperty().getFieldName() : null;

            return new Select(idColumn, tableName, columns);
        }
    }

    /**
     * @author Hasnain Javed
     * @since 1.0.0 
     */
    private class WholesaleUpdateByIdAction extends WriteDbAction {

        private Object idValue;

        public WholesaleUpdateByIdAction(Object entity, String tableName) {

            super(entity, tableName, UPDATE);
            validateEntity();

            this.idValue = crateConverter.convertToCrateType(getIdPropertyValue(entity), null);
        }

        private void validateEntity() {

            if (!persistentEntity.hasIdProperty()) {
                throw new MappingException(format(ID_COLUMN, entity.getClass().getName()));
            }

            validateIdValue(entity);
        }

        @Override
        protected void processDocument(CrateDocument document) {
            document.remove(persistentEntity.getIdProperty().getFieldName());
            document.remove(DEFAULT_TYPE_KEY);
        }

        @Override
        protected Object[] getArguments() {
            return add(document.values().toArray(), idValue);
        }

        @Override
        public String getSQLStatement() {
            return new Update(tableName, persistentEntity.getIdProperty().getFieldName(), document.keySet())
                    .createStatement();
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     */
    private class InsertAction extends WriteDbAction {

        public InsertAction(Object entity, String tableName) {

            super(entity, tableName, INSERT);
            validateEntity();
        }

        private void validateEntity() {

            if (!isIdPropertyDefined(entity.getClass())) {
                logger.warn(NO_ID_WARNING, entity.getClass().getName());
            } else {
                validateIdValue(entity);
            }
        }

        @Override
        protected void processDocument(CrateDocument document) {
            // no op
        }

        @Override
        protected Object[] getArguments() {
            return document.values().toArray();
        }

        @Override
        public String getSQLStatement() {
            return new Insert(tableName, document.keySet()).createStatement();
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     */
    private class DeleteByIdActionHandler implements CrateAction, CrateActionResponseHandler<Boolean> {

        private CrateSQLStatement delete;
        private Object idValue;

        public DeleteByIdActionHandler(String table, Class<?> entityClass, Object id) {

            notNull(entityClass);
            notNull(id);
            validateEntity(entityClass);

            this.idValue = crateConverter.convertToCrateType(id, null);
            this.delete = new Delete(table, getIdPropertyFor(entityClass).getFieldName());

            doBeforeDelete(id);
        }

        @Override
        public SQLRequest getSQLRequest() {
            SQLRequest request = new SQLRequest(getSQLStatement(), new Object[] { idValue });
            request.includeTypesOnResponse(true);
            return request;
        }

        @Override
        public String getSQLStatement() {
            return delete.createStatement();
        }

        @Override
        public Boolean handle(SQLResponse response) {

            boolean removed = response.rowCount() == 1L;

            if (removed) {
                logger.info("Removed row with id '{}'", idValue);
                doAfterDelete(idValue);
            } else {
                logger.info("No row removed with id '{}'", idValue);
            }

            return removed;
        }

        private void validateEntity(Class<?> entityClass) {

            CratePersistentProperty idProperty = getIdPropertyFor(entityClass);

            if (idProperty == null) {
                throw new MappingException("No id property found for object of type " + entityClass);
            }
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     * @param <T>
     */
    private class ReadDbHandler<T> implements CrateActionResponseHandler<List<T>> {

        private final Class<T> type;

        public ReadDbHandler(Class<T> type) {
            notNull(type);
            this.type = type;
        }

        @Override
        public List<T> handle(SQLResponse response) {

            if (response.hasRowCount()) {

                String[] columns = response.cols();
                DataType<?>[] types = response.columnTypes();
                Object[][] payload = response.rows();

                Long rows = new Long(response.rowCount());

                List<T> entities = new ArrayList<T>(rows.intValue());

                for (Object[] row : payload) {

                    CrateDocument source = new CrateDocumentConverter(columns, types, row).toDocument();

                    T entity = null;

                    if (!source.isEmpty()) {
                        entity = crateConverter.read(type, source);
                        maybeEmitEvent(new AfterLoadEvent<T>(source, type));
                    }

                    if (entity != null) {
                        maybeEmitEvent(new AfterConvertEvent<T>(source, entity));
                        entities.add(entity);
                    }
                }

                return entities;
            } else {
                return emptyList();
            }
        }
    }

    /**
     * @author Hasnain Javed
     * @since 1.0.0
     * @param <T>
     */
    private abstract class WriteDbAction implements CrateAction, CrateActionResponseHandler<Void> {

        private final Set<ActionType> allowedTypes;

        private ActionType actionType;

        protected String tableName;
        protected Object entity;
        protected CratePersistentEntity<?> persistentEntity;
        protected CrateDocument document;

        public WriteDbAction(Object entity, String tableName, ActionType actionType) {

            hasText(tableName);
            notNull(entity);
            notNull(actionType);

            allowedTypes = new HashSet<ActionType>(asList(INSERT, UPDATE));

            if (!allowedTypes.contains(actionType)) {
                throw new CrateSQLActionException(
                        format("Invalid sql action type '%s'. Allowed types are '%s'", actionType, allowedTypes));
            }

            this.tableName = tableName;
            this.entity = entity;
            this.persistentEntity = getPersistentEntityFor(entity.getClass());
            this.document = new CrateDocument();
            this.actionType = actionType;
        }

        final void beforeSave() {

            doBeforeSave(entity, document);

            if (persistentEntity.hasVersionProperty()) {
                document.remove(persistentEntity.getVersionProperty().getFieldName());
            }

            processDocument(document);
        }

        @Override
        public SQLRequest getSQLRequest() {

            SQLRequest request = new SQLRequest(getSQLStatement(), getArguments());
            request.includeTypesOnResponse(true);

            return request;
        }

        @Override
        public Void handle(SQLResponse response) {

            switch (actionType) {
            case INSERT:
                doAfterSave(entity, document);
                break;
            case UPDATE:
                Object id = getIdPropertyValue(entity);
                if (response.rowCount() > 0) {
                    logger.info("Updated row with id '{}'", id);

                    if (persistentEntity.hasVersionProperty()) {
                        // crate is eventually consistent. Data written with a former statement is not guaranteed to be fetched.
                        refreshTable(persistentEntity.getType());
                    }
                    doAfterUpdate(entity, document);
                } else {
                    logger.info("No row updated with id '{}'", id);
                }
                break;
            default:
                throw new IllegalArgumentException(format(BULK_ACTION, actionType, allowedTypes));
            }

            return null;
        }

        protected abstract void processDocument(CrateDocument document);

        protected abstract Object[] getArguments();
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     * @param <T>
     */
    private abstract class BaseSQLBulkOperation<T> implements CrateBulkAction, CrateBulkActionResponseHandler<T> {

        protected String tableName;
        protected List<T> entities;
        protected List<CrateDocument> documents;
        protected CratePersistentEntity<?> persistentEntity;

        private ActionType actionType;

        public BaseSQLBulkOperation(String tableName, Class<T> entityClass, List<T> entities,
                ActionType actionType) {

            hasText(tableName);
            notNull(entityClass);
            notNull(actionType);
            notEmpty(entities);

            this.tableName = tableName;
            this.persistentEntity = getPersistentEntityFor(entityClass);
            this.actionType = actionType;

            // preserve order
            this.entities = new ArrayList<T>(entities);
            this.documents = new ArrayList<CrateDocument>(this.entities.size());
        }

        /**
         * Converts entities to {@link CrateDocument}s and calls lifecycle callback method(s). 
         */
        final void beforeSave() {

            for (T entity : entities) {

                CrateDocument document = new CrateDocument();

                doBeforeSave(entity, document);

                processDocument(document);

                document.remove(RESERVED_VESRION_FIELD_NAME);

                documents.add(document);
            }
        }

        @Override
        public ActionType getActionType() {
            return actionType;
        }

        @Override
        public SQLBulkRequest getSQLRequest() {

            Object[][] bulkArgs = new Object[documents.size()][];

            for (int index = 0; index < documents.size(); index++) {

                Object entity = entities.get(index);
                CrateDocument document = documents.get(index);

                List<Object> extraArgs = appendArgs(entity);

                Object[] args = null;

                if (!extraArgs.isEmpty()) {
                    args = addAll(document.values().toArray(), extraArgs.toArray());
                } else {
                    args = document.values().toArray();
                }

                bulkArgs[index] = args;
            }

            SQLBulkRequest request = new SQLBulkRequest(getSQLStatement(), bulkArgs);

            return request;
        }

        @Override
        public BulkOperartionResult<T> handle(SQLBulkResponse response) {

            Result[] results = response.results();

            BulkActionResult<T> actionResults = new BulkActionResult<T>();

            if (persistentEntity.hasIdProperty()) {
                // crate is eventually consistent. Data written with a former statement is not guaranteed to be fetched.
                refreshTable(persistentEntity.getType());
            }

            for (int index = 0; index < results.length; index++) {

                T entity = entities.get(index);

                CrateDocument document = documents.get(index);

                ActionResult<T> actionResult = actionResults.addResult(results[index], entity);

                if (actionResult.isSuccess()) {

                    switch (actionType) {
                    case INSERT:
                        doAfterSave(entity, document);
                        break;
                    case UPDATE:
                        doAfterUpdate(entity, document);
                        break;
                    default:
                        throw new IllegalArgumentException(
                                format(BULK_ACTION, actionType, Arrays.toString(values())));
                    }
                }
            }

            return actionResults;
        }

        /**
         * 
         * @param exclude the field(s) to be removed from the set. The version field if defined will be
         * removed by default as the crate system column "_version" is readonly 
         * @return set of fields 
         */
        protected Set<String> getColumns(String... exclude) {

            String[] excludes = exclude;

            if (persistentEntity.hasVersionProperty()) {
                excludes = (String[]) add(exclude, persistentEntity.getVersionProperty().getFieldName());
            }

            return isEmpty(excludes) ? persistentEntity.getPropertyNames()
                    : persistentEntity.getPropertyNames(excludes);
        }

        /**
         * Custom hook for appending arguments to request payload 
         * 
         * @param entity the current entity being processed
         * @return list of additional args
         */
        protected List<Object> appendArgs(Object entity) {
            return emptyList();
        }

        protected abstract void processDocument(CrateDocument document);
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     * @param <T>
     */
    private class BulkInsertOperation<T> extends BaseSQLBulkOperation<T> {

        private final Set<String> columns;

        public BulkInsertOperation(Class<T> entityClass, String tableName, List<T> entities) {
            super(tableName, entityClass, entities, INSERT);
            columns = new TreeSet<String>();
            columns.add(DEFAULT_TYPE_KEY);
            columns.addAll(getColumns());
        }

        @Override
        public String getSQLStatement() {
            return new Insert(tableName, columns).createStatement();
        }

        @Override
        protected void processDocument(CrateDocument document) {
            // no op
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     * @param <T>
     */
    private class BulkUpdateOperation<T> extends BaseSQLBulkOperation<T> {

        public BulkUpdateOperation(Class<T> entityClass, String tableName, List<T> entities) {

            super(tableName, entityClass, entities, UPDATE);

            validateEntity(entityClass);
        }

        @Override
        public String getSQLStatement() {
            return new Update(tableName, persistentEntity.getIdProperty().getFieldName(),
                    getColumns(persistentEntity.getIdProperty().getFieldName())).createStatement();
        }

        @Override
        protected List<Object> appendArgs(Object entity) {
            return asList(getIdPropertyValue(entity));
        }

        @Override
        protected void processDocument(CrateDocument document) {
            document.remove(DEFAULT_TYPE_KEY);
            document.remove(persistentEntity.getIdProperty().getFieldName());
        }

        private void validateEntity(Class<T> entityClass) {

            if (!persistentEntity.hasIdProperty()) {
                throw new MappingException(format(ID_COLUMN, entityClass.getName()));
            }
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     */
    private class BulkDeleteOperation implements CrateBulkAction, CrateBulkActionResponseHandler<Object> {

        private CrateSQLStatement delete;
        private List<Object> convertedIds;

        public BulkDeleteOperation(Class<?> entityClass, String tableName, List<Object> ids) {

            notNull(entityClass);
            notEmpty(ids);
            validateEntity(entityClass);

            this.delete = new Delete(tableName, getPersistentEntityFor(entityClass).getIdProperty().getFieldName());

            this.convertedIds = new ArrayList<Object>(ids.size());

            for (Object id : ids) {
                convertedIds.add(crateConverter.convertToCrateType(id, null));
                doBeforeDelete(id);
            }
        }

        @Override
        public ActionType getActionType() {
            return DELETE;
        }

        @Override
        public String getSQLStatement() {
            return delete.createStatement();
        }

        @Override
        public BulkOperartionResult<Object> handle(SQLBulkResponse response) {

            Result[] results = response.results();

            BulkActionResult<Object> actionResults = new BulkActionResult<Object>();

            for (int index = 0; index < results.length; index++) {

                Object id = convertedIds.get(index);

                ActionResult<Object> actionResult = actionResults.addResult(results[index], id);

                if (actionResult.isSuccess()) {
                    doAfterDelete(id);
                }
            }

            return actionResults;
        }

        @Override
        public SQLBulkRequest getSQLRequest() {

            Object[][] bulkArgs = new Object[convertedIds.size()][];

            for (int index = 0; index < bulkArgs.length; index++) {
                bulkArgs[index] = new Object[] { convertedIds.get(index) };
            }

            return new SQLBulkRequest(getSQLStatement(), bulkArgs);
        }

        public void validateEntity(Class<?> entityClass) {

            CratePersistentEntity<?> persistentEntity = getPersistentEntityFor(entityClass);

            if (!persistentEntity.hasIdProperty()) {
                throw new MappingException(format(ID_COLUMN, entityClass.getName()));
            }
        }
    }

    /**
     * 
     * @author Hasnain Javed
     * @since 1.0.0
     */
    private class RefreshTableAction implements CrateAction {

        private CrateSQLStatement refreshTable;

        public RefreshTableAction(String tableName) {
            refreshTable = new RefreshTable(tableName);
        }

        @Override
        public String getSQLStatement() {
            return refreshTable.createStatement();
        }

        @Override
        public SQLRequest getSQLRequest() {
            return new SQLRequest(getSQLStatement());
        }
    }
}