org.batoo.jpa.jdbc.AbstractTable.java Source code

Java tutorial

Introduction

Here is the source code for org.batoo.jpa.jdbc.AbstractTable.java

Source

/*
 * Copyright (c) 2012-2013, Batu Alp Ceylan
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.batoo.jpa.jdbc;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.batoo.common.util.FinalWrapper;
import org.batoo.jpa.jdbc.model.EntityTypeDescriptor;
import org.batoo.jpa.parser.AbstractLocator;
import org.batoo.jpa.parser.MappingException;
import org.batoo.jpa.parser.metadata.ColumnTransformerMetadata;
import org.batoo.jpa.parser.metadata.TableMetadata;
import org.batoo.jpa.parser.metadata.UniqueConstraintMetadata;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Abstract implementation for Entity, Secondary and Join tables.
 * 
 * @author hceylan
 * @since 2.0.0
 */
public abstract class AbstractTable {

    private final AbstractLocator locator;

    private final String catalog;
    private final String schema;
    private String name;
    private final Map<String, AbstractColumn> columnMap = Maps.newHashMap();
    private final Map<String, String[]> uniqueConstraints = Maps.newHashMap();
    private final List<ForeignKey> foreignKeys = Lists.newArrayList();
    private BasicColumn versionColumn;

    private final HashMap<String, String> insertSqlMap = Maps.newHashMap();
    private final HashMap<EntityTypeDescriptor, String> updateSqlMap = Maps.newHashMap();
    private String updateSql;
    private FinalWrapper<String> versionUpdateSql;
    private FinalWrapper<String> versionSelectSql;
    private FinalWrapper<AbstractColumn[]> columns;

    private AbstractColumn[] updateColumns;
    private AbstractColumn[] selectVersionColumns;
    private final Map<String, AbstractColumn[]> insertColumnsMap = Maps.newHashMap();
    private final Map<EntityTypeDescriptor, AbstractColumn[]> updateColumnsMap = Maps.newHashMap();

    private FinalWrapper<String> restrictionSql;
    private AbstractColumn[] restrictionColumns;

    /**
     * @param defaultName
     *            the default name for the table
     * @param metadata
     *            the metadata
     * 
     * @since 2.0.0
     */
    public AbstractTable(String defaultName, TableMetadata metadata) {
        this(metadata);

        if (this.name == null) {
            this.name = defaultName;
        }
    }

    /**
     * @param metadata
     *            the metadata
     * 
     * @since 2.0.0
     */
    public AbstractTable(TableMetadata metadata) {
        super();

        this.locator = metadata != null ? metadata.getLocator() : null;
        this.catalog = (metadata != null) && StringUtils.isNotBlank(metadata.getCatalog()) ? metadata.getCatalog()
                : null;
        this.schema = (metadata != null) && StringUtils.isNotBlank(metadata.getSchema()) ? metadata.getSchema()
                : null;

        if (metadata != null) {
            if (StringUtils.isNotBlank(metadata.getName())) {
                this.name = metadata.getName();
            }

            for (final UniqueConstraintMetadata constraint : metadata.getUniqueConstraints()) {
                this.uniqueConstraints.put(constraint.getName(), constraint.getColumnNames());
            }
        }
    }

    /**
     * Adds the column to the table
     * 
     * @param column
     *            the column to add
     * 
     * @since 2.0.0
     */
    public void addColumn(AbstractColumn column) {
        if ((column instanceof BasicColumn) && ((BasicColumn) column).isVersion()) {
            if (this.versionColumn != null) {
                throw new MappingException("There can be only one version column", this.versionColumn.getLocator(),
                        column.getLocator());
            }

            this.versionColumn = (BasicColumn) column;
        }

        final AbstractColumn existing = this.columnMap.get(column.getName());

        if (existing != null) {
            if (column instanceof JoinColumn) {
                final JoinColumn joinColumn = (JoinColumn) column;
                if (!joinColumn.isInsertable() && !joinColumn.isUpdatable()) {
                    joinColumn.setVirtual(existing);
                }

                return;
            }

            // Allow read-only access to columns already defined 
            if (!column.isInsertable() && !column.isUpdatable()) {
                return;
            }

            throw new MappingException("Duplicate column names " + column.getName() + " on table " + this.name,
                    column.getLocator(), existing.getLocator());
        }

        this.columnMap.put(column.getName(), column);
    }

    /**
     * Adds a foreign key to the table.
     * 
     * @param foreignKey
     *            the foreign key to add
     * 
     * @since 2.0.0
     */
    public void addForeignKey(ForeignKey foreignKey) {
        this.foreignKeys.add(foreignKey);
    }

    /**
     * Generates the insert statement for the type.
     * 
     * @param type
     *            the type to generate the insert statement for
     * @param pkColumns
     *            the primary key columns
     * 
     * @since 2.0.0
     */
    private synchronized void generateInsertSql(final EntityTypeDescriptor type, int size) {
        final String sqlKey = type != null ? type.getName() + size : "" + size;

        String sql = this.insertSqlMap.get(sqlKey);
        if (sql != null) { // other thread finished the job for us
            return;
        }

        final List<AbstractColumn> insertColumns = Lists.newArrayList();

        // Filter out the identity physicalColumns
        final Collection<AbstractColumn> filteredColumns = type == null ? this.columnMap.values()
                : Collections2.filter(this.columnMap.values(), new Predicate<AbstractColumn>() {

                    @Override
                    public boolean apply(AbstractColumn input) {
                        return AbstractTable.this.isInsertableColumn(type, input);
                    }

                });

        // prepare the names tuple in the form of "COLNAME [, COLNAME]*"

        final Collection<String> columnNames = Collections2.transform(filteredColumns,
                new Function<AbstractColumn, String>() {

                    @Override
                    public String apply(AbstractColumn input) {
                        insertColumns.add(input);

                        return input.getName();
                    }
                });

        if (columnNames.size() == 0) {
            // TODO investigate in others with identity
            sql = "INSERT INTO " + this.getQName() + " DEFAULT VALUES";
        } else {

            final Collection<String> singleParams = Collections2.transform(filteredColumns,
                    new Function<AbstractColumn, String>() {

                        @Override
                        public String apply(AbstractColumn input) {
                            String writeParam = null;
                            if (input instanceof BasicColumn) {
                                final ColumnTransformerMetadata columnTransformer = ((BasicColumn) input)
                                        .getMapping().getColumnTransformer();
                                writeParam = columnTransformer != null ? columnTransformer.getWrite() : null;
                            }
                            writeParam = Strings.isNullOrEmpty(writeParam) ? "?" : writeParam;

                            return writeParam;
                        }
                    });

            // prepare the parameters in the form of "? [, ?]*"
            final String singleParamStr = "\t(" + Joiner.on(", ").join(singleParams) + ")";
            final String parametersStr = StringUtils.repeat(singleParamStr, ",\n", size);

            final String columnNamesStr = Joiner.on(", ").join(columnNames);

            // INSERT INTO SCHEMA.TABLE
            // (COL [, COL]*)
            // VALUES (PARAM [, PARAM]*)
            sql = "INSERT INTO " + this.getQName() //
                    + "\n(" + columnNamesStr + ")"//
                    + "\nVALUES\n" + parametersStr;
        }

        this.insertSqlMap.put(sqlKey, sql);
        this.insertColumnsMap.put(sqlKey, insertColumns.toArray(new AbstractColumn[insertColumns.size()]));
    }

    /**
     * Generates the update statement for the type.
     * 
     * @param type
     *            the type to generate the update statement for
     * 
     * @since 2.0.0
     * @param pkColumns
     */
    private synchronized void generateUpdateSql(final EntityTypeDescriptor type,
            Map<String, AbstractColumn> pkColumns) {
        String sql = this.updateSqlMap.get(type);
        if (sql != null) { // other thread finished the job for us
            return;
        }

        final List<AbstractColumn> updateColumns = Lists.newArrayList();
        // Filter out the identity physicalColumns
        final Collection<AbstractColumn> filteredColumns = type == null ? this.columnMap.values()
                : Collections2.filter(this.columnMap.values(), new Predicate<AbstractColumn>() {

                    @Override
                    public boolean apply(AbstractColumn input) {
                        return AbstractTable.this.isUpdatableColumn(type, input);
                    }
                });

        // prepare the names tuple in the form of "COLNAME = ? [, COLNAME = ?]*"
        final Collection<String> columnNames = Collections2.transform(filteredColumns,
                new Function<AbstractColumn, String>() {

                    @Override
                    public String apply(AbstractColumn input) {
                        if (!input.isPrimaryKey()) {
                            updateColumns.add(input);

                            return input.getName() + " = ?";
                        }

                        return null;
                    }
                });

        final String columnNamesStr = Joiner.on(", ").skipNulls().join(columnNames);

        // UPDATE SCHEMA.TABLE SET
        // (COL [, COL]*)
        // WHERE ID = ? [, ID = ?]*)
        sql = "UPDATE " + this.getQName() + " SET"//
                + "\n" + columnNamesStr //
                + "\nWHERE " + this.getRestrictionSql(pkColumns);

        if (type != null) {
            this.updateSqlMap.put(type, sql);
            this.updateColumnsMap.put(type, updateColumns.toArray(new AbstractColumn[updateColumns.size()]));
        } else {
            this.updateSql = sql;
            this.updateColumns = updateColumns.toArray(new AbstractColumn[updateColumns.size()]);
        }
    }

    /**
     * Returns the catalog.
     * 
     * @return the catalog
     * 
     * @since 2.0.0
     */
    public String getCatalog() {
        return this.catalog;
    }

    /**
     * Returns the columnMap of the AbstractTable.
     * 
     * @return the columnMap of the AbstractTable
     * 
     * @since 2.0.0
     */
    protected Map<String, AbstractColumn> getColumnMap() {
        return this.columnMap;
    }

    /**
     * Returns the set of column names.
     * 
     * @return the set of column names
     * 
     * @since 2.0.0
     */
    public Collection<String> getColumnNames() {
        return Collections2.transform(this.columnMap.values(), new Function<AbstractColumn, String>() {

            @Override
            public String apply(AbstractColumn input) {
                return input.getName();
            }
        });
    }

    /**
     * Returns the array of columns the table has
     * 
     * @return the array of columns the table has
     * 
     * @since 2.0.0
     */
    public AbstractColumn[] getColumns() {
        FinalWrapper<AbstractColumn[]> wrapper = this.columns;

        if (wrapper == null) {
            synchronized (this) {
                if (this.columns == null) {
                    this.columns = new FinalWrapper<AbstractColumn[]>(
                            this.columnMap.values().toArray(new AbstractColumn[this.columnMap.values().size()]));
                }

                wrapper = this.columns;
            }
        }

        return wrapper.value;
    }

    /**
     * Returns the foreign keys of the table.
     * 
     * @return the foreign keys of the table
     * 
     * @since 2.0.0
     */
    public List<ForeignKey> getForeignKeys() {
        return this.foreignKeys;
    }

    /**
     * Returns the columns for the insert.
     * 
     * @param entity
     *            the entity to returns columns for or null for generic columns
     * @param size
     *            the batch size
     * @return the insert columns
     * 
     * @since 2.0.0
     */
    protected AbstractColumn[] getInsertColumns(final EntityTypeDescriptor entity, int size) {
        return this.insertColumnsMap.get(entity != null ? entity.getName() + size : "" + size);
    }

    /**
     * Returns the insert statement for the table specifically.
     * 
     * @param entity
     *            the entity to return insert statement for or null for generic SQL
     * @param size
     *            the batch size
     * @return the insert statement
     * 
     * @since 2.0.0
     */
    protected String getInsertSql(EntityTypeDescriptor entity, int size) {
        final String sqlKey = entity != null ? entity.getName() + size : "" + size;

        final String sql = this.insertSqlMap.get(sqlKey);
        if (sql != null) { // other thread finished the job for us
            return sql;
        }

        this.generateInsertSql(entity, size);

        return this.insertSqlMap.get(sqlKey);
    }

    /**
     * Returns the locator.
     * 
     * @return the locator
     * 
     * @since 2.0.0
     */
    public AbstractLocator getLocator() {
        return this.locator;
    }

    /**
     * Returns the name.
     * 
     * @return the name
     * 
     * @since 2.0.0
     */
    public String getName() {
        return this.name;
    }

    /**
     * Returns the set of primary key column names.
     * 
     * @return the set of primary column names
     * 
     * @since 2.0.0
     */
    public Set<String> getPkColumnNames() {
        return Collections.emptySet();
    }

    /**
     * Returns the qualified name of the table.
     * 
     * @return the qualified name of the table
     * 
     * @since 2.0.0
     */
    public String getQName() {
        return Joiner.on(".").skipNulls().join(this.schema, this.name);
    }

    /**
     * Returns the restriction columns of the table.
     * 
     * @return the restriction columns of the table
     * 
     * @since 2.0.1
     */
    public AbstractColumn[] getRestrictionColumns() {
        return this.restrictionColumns;
    }

    /**
     * Returns the restriction SQL fragment.
     * 
     * @param pkColumns
     *            the primary key column
     * @return the restriction SQL fragment
     * 
     * @since 2.0.1
     */
    protected String getRestrictionSql(Map<String, AbstractColumn> pkColumns) {
        FinalWrapper<String> wrapper = this.restrictionSql;

        if (wrapper == null) {
            synchronized (this) {
                if (this.restrictionSql == null) {

                    final List<AbstractColumn> _restrictionColumns = Lists.newArrayList();
                    _restrictionColumns.addAll(pkColumns.values());

                    String _restrictionSql = Joiner.on(" AND ").join(
                            Collections2.transform(pkColumns.values(), new Function<AbstractColumn, String>() {

                                @Override
                                public String apply(AbstractColumn input) {
                                    return input.getName() + " = ?";
                                }
                            }));

                    if (this.versionColumn != null) {
                        _restrictionColumns.add(this.versionColumn);
                        _restrictionSql += " AND " + this.versionColumn.getName() + " = ?";
                    }

                    this.restrictionColumns = _restrictionColumns
                            .toArray(new AbstractColumn[_restrictionColumns.size()]);
                    this.restrictionSql = new FinalWrapper<String>(_restrictionSql);
                }

                wrapper = this.restrictionSql;
            }
        }

        return this.restrictionSql.value;
    }

    /**
     * Returns the schema.
     * 
     * @return the schema
     * 
     * @since 2.0.0
     */
    public String getSchema() {
        return this.schema;
    }

    /**
     * Returns the select version columns.
     * 
     * @return the select version columns
     * 
     * @since 2.0.0
     */
    public AbstractColumn[] getSelectVersionColumns() {
        return this.selectVersionColumns;
    }

    /**
     * Returns the version select statement for the table specifically.
     * 
     * @param pkColumns
     *            the primary key columns
     * @return the select statement
     * 
     * @since 2.0.0
     */
    protected String getSelectVersionSql(Map<String, AbstractColumn> pkColumns) {
        FinalWrapper<String> wrapper = this.versionSelectSql;

        if (wrapper == null) {
            synchronized (this) {
                if (this.versionSelectSql == null) {

                    AbstractColumn versionColumn = null;

                    for (final AbstractColumn column : this.getColumns()) {
                        if ((column instanceof BasicColumn) && ((BasicColumn) column).isVersion()) {
                            versionColumn = column;
                        }
                    }

                    final List<AbstractColumn> selectVersionColumns = Lists.newArrayList();

                    final Collection<String> restrictions = Collections2.transform(pkColumns.values(),
                            new Function<AbstractColumn, String>() {

                                @Override
                                public String apply(AbstractColumn input) {
                                    selectVersionColumns.add(input);

                                    return input.getName() + " = ?";
                                }
                            });

                    final String restrictionStr = Joiner.on(" AND ").join(restrictions);

                    if (versionColumn != null) {
                        // SELECT VERSION_COLUMN FROM SCHEMA.TABLE SET
                        // WHERE (PARAM [, PARAM]*)
                        this.versionSelectSql = new FinalWrapper<String>("SELECT " + versionColumn.getName() //
                                + " FROM " + this.getQName() //
                                + "\nWHERE " + restrictionStr);

                        this.selectVersionColumns = selectVersionColumns
                                .toArray(new AbstractColumn[selectVersionColumns.size()]);
                    }
                }

                wrapper = this.versionSelectSql;
            }
        }

        return wrapper.value;
    }

    /**
     * Returns the unique constraints.
     * 
     * @return the unique constraints
     * 
     * @since 2.0.0
     */
    public Map<String, String[]> getUniqueConstraints() {
        return this.uniqueConstraints;
    }

    /**
     * Returns the columns for the update.
     * 
     * @param entity
     *            the entity to returns columns for or null for generic columns
     * @return the insert columns
     * 
     * @since 2.0.0
     */
    protected AbstractColumn[] getUpdateColumns(final EntityTypeDescriptor entity) {
        if (entity == null) {
            return this.updateColumns;
        }

        return this.updateColumnsMap.get(entity);
    }

    /**
     * Returns the update statement for the table specifically.
     * 
     * @param entity
     *            the entity to return update statement for or null for generic SQL
     * @param pkColumns
     *            the primary key columns
     * @return the insert statement
     * 
     * @since 2.0.0
     */
    protected String getUpdateSql(EntityTypeDescriptor entity, Map<String, AbstractColumn> pkColumns) {
        if (entity == null) {
            if (this.updateSql == null) {
                this.generateUpdateSql(null, pkColumns);
            }

            return this.updateSql;
        }

        String sql = this.updateSqlMap.get(entity);
        if (sql == null) {
            this.generateUpdateSql(entity, pkColumns);

            sql = this.updateSqlMap.get(entity);
        }

        return sql;
    }

    /**
     * Returns the version update statement for the table specifically.
     * 
     * @param pkColumns
     *            the primary key columns
     * @return the update statement
     * 
     * @since 2.0.0
     */
    protected String getVersionUpdateSql(Map<String, AbstractColumn> pkColumns) {
        FinalWrapper<String> wrapper = this.versionUpdateSql;

        if (wrapper == null) {
            synchronized (this) {
                if (this.versionUpdateSql == null) {
                    // UPDATE SCHEMA.TABLE SET
                    // VERSION = ?
                    // VALUES (PARAM [, PARAM]*)
                    // WHERE ID = ? [AND ID = ?]* AND VERSION = ?
                    this.versionUpdateSql = new FinalWrapper<String>("UPDATE " + this.getQName() + " SET"//
                            + "\n" + this.versionColumn.getName() + " = ?" //
                            + "\nWHERE " + this.getRestrictionSql(pkColumns));
                }

                wrapper = this.versionUpdateSql;
            }
        }

        return wrapper.value;
    }

    private boolean isInsertableColumn(final EntityTypeDescriptor type, AbstractColumn input) {
        if (input.getIdType() == IdType.IDENTITY) {
            return false;
        }

        if (!input.isInsertable()) {
            return false;
        }

        if (input instanceof DiscriminatorColumn) {
            return true;
        }

        final EntityTypeDescriptor root;

        if ((input instanceof JoinColumn) && (input.getMapping() == null)) {
            root = (EntityTypeDescriptor) ((JoinColumn) input).getReferencedColumn().getMapping().getRoot()
                    .getTypeDescriptor();
        } else if ((input instanceof OrderColumn)) {
            return input.isInsertable();
        } else {
            root = (EntityTypeDescriptor) input.getMapping().getRoot().getTypeDescriptor();
        }

        final Class<?> parent = root.getJavaType();
        final Class<?> javaType = type.getJavaType();

        return parent.isAssignableFrom(javaType);
    }

    private boolean isUpdatableColumn(final EntityTypeDescriptor type, AbstractColumn input) {
        if ((input.isPrimaryKey()) || (input instanceof DiscriminatorColumn)) {
            return false;
        }

        if (!input.isUpdatable()) {
            return false;
        }

        final EntityTypeDescriptor root;

        if ((input instanceof JoinColumn) && (input.getMapping() == null)) {
            root = (EntityTypeDescriptor) ((JoinColumn) input).getReferencedColumn().getMapping().getRoot()
                    .getTypeDescriptor();
        } else {
            root = (EntityTypeDescriptor) input.getMapping().getRoot().getTypeDescriptor();
        }

        final Class<?> parent = root.getJavaType();
        final Class<?> javaType = type.getJavaType();

        return parent.isAssignableFrom(javaType);
    }

    /**
     * Updates the name of the table.
     * 
     * @param name
     *            the name of the table
     * 
     * @since 2.0.0
     */
    protected void setName(String name) {
        this.name = name;
    }
}