org.hibernate.mapping.SimpleValue.java Source code

Java tutorial

Introduction

Here is the source code for org.hibernate.mapping.SimpleValue.java

Source

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

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Objects;
import javax.persistence.AttributeConverter;

import org.hibernate.FetchMode;
import org.hibernate.MappingException;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.Mapping;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.IdentityGenerator;
import org.hibernate.id.PersistentIdentifierGenerator;
import org.hibernate.id.factory.IdentifierGeneratorFactory;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.metamodel.model.convert.spi.JpaAttributeConverter;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.BinaryType;
import org.hibernate.type.RowVersionType;
import org.hibernate.type.Type;
import org.hibernate.type.descriptor.JdbcTypeNameMapper;
import org.hibernate.type.descriptor.converter.AttributeConverterSqlTypeDescriptorAdapter;
import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter;
import org.hibernate.type.descriptor.java.BasicJavaDescriptor;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext;
import org.hibernate.type.descriptor.sql.JdbcTypeJavaClassMappings;
import org.hibernate.type.descriptor.sql.LobTypeMappings;
import org.hibernate.type.descriptor.sql.NationalizedTypeMappings;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.usertype.DynamicParameterizedType;

/**
 * Any value that maps to columns.
 * @author Gavin King
 */
public class SimpleValue implements KeyValue {
    private static final CoreMessageLogger log = CoreLogging.messageLogger(SimpleValue.class);

    public static final String DEFAULT_ID_GEN_STRATEGY = "assigned";

    private final MetadataImplementor metadata;

    private final List<Selectable> columns = new ArrayList<>();
    private final List<Boolean> insertability = new ArrayList<>();
    private final List<Boolean> updatability = new ArrayList<>();

    private String typeName;
    private Properties typeParameters;
    private boolean isVersion;
    private boolean isNationalized;
    private boolean isLob;

    private Properties identifierGeneratorProperties;
    private String identifierGeneratorStrategy = DEFAULT_ID_GEN_STRATEGY;
    private String nullValue;
    private Table table;
    private String foreignKeyName;
    private String foreignKeyDefinition;
    private boolean alternateUniqueKey;
    private boolean cascadeDeleteEnabled;

    private ConverterDescriptor attributeConverterDescriptor;
    private Type type;

    /**
     * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext)} instead.
     */
    @Deprecated
    public SimpleValue(MetadataImplementor metadata) {
        this.metadata = metadata;
    }

    /**
     * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext, Table)} instead.
     */
    @Deprecated
    public SimpleValue(MetadataImplementor metadata, Table table) {
        this(metadata);
        this.table = table;
    }

    /**
     * @deprecated Use {@link SimpleValue#SimpleValue(MetadataBuildingContext, Table)} instead.
     */
    @Deprecated
    public SimpleValue(MetadataBuildingContext buildingContext) {
        this(buildingContext.getMetadataCollector());
    }

    public SimpleValue(MetadataBuildingContext buildingContext, Table table) {
        this.metadata = buildingContext.getMetadataCollector();
        this.table = table;
    }

    public MetadataImplementor getMetadata() {
        return metadata;
    }

    @Override
    public ServiceRegistry getServiceRegistry() {
        return getMetadata().getMetadataBuildingOptions().getServiceRegistry();
    }

    @Override
    public boolean isCascadeDeleteEnabled() {
        return cascadeDeleteEnabled;
    }

    public void setCascadeDeleteEnabled(boolean cascadeDeleteEnabled) {
        this.cascadeDeleteEnabled = cascadeDeleteEnabled;
    }

    public void addColumn(Column column) {
        addColumn(column, true, true);
    }

    public void addColumn(Column column, boolean isInsertable, boolean isUpdatable) {
        int index = columns.indexOf(column);
        if (index == -1) {
            columns.add(column);
            insertability.add(isInsertable);
            updatability.add(isUpdatable);
        } else {
            if (insertability.get(index) != isInsertable) {
                throw new IllegalStateException(
                        "Same column is added more than once with different values for isInsertable");
            }
            if (updatability.get(index) != isUpdatable) {
                throw new IllegalStateException(
                        "Same column is added more than once with different values for isUpdatable");
            }
        }
        column.setValue(this);
        column.setTypeIndex(columns.size() - 1);
    }

    public void addFormula(Formula formula) {
        columns.add(formula);
        insertability.add(false);
        updatability.add(false);
    }

    @Override
    public boolean hasFormula() {
        Iterator iter = getColumnIterator();
        while (iter.hasNext()) {
            Object o = iter.next();
            if (o instanceof Formula) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int getColumnSpan() {
        return columns.size();
    }

    @Override
    public Iterator<Selectable> getColumnIterator() {
        return columns.iterator();
    }

    public List getConstraintColumns() {
        return columns;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        if (typeName != null && typeName.startsWith(AttributeConverterTypeAdapter.NAME_PREFIX)) {
            final String converterClassName = typeName
                    .substring(AttributeConverterTypeAdapter.NAME_PREFIX.length());
            final ClassLoaderService cls = getMetadata().getMetadataBuildingOptions().getServiceRegistry()
                    .getService(ClassLoaderService.class);
            try {
                final Class<? extends AttributeConverter> converterClass = cls.classForName(converterClassName);
                this.attributeConverterDescriptor = new ClassBasedConverterDescriptor(converterClass, false,
                        ((InFlightMetadataCollector) getMetadata()).getClassmateContext());
                return;
            } catch (Exception e) {
                log.logBadHbmAttributeConverterType(typeName, e.getMessage());
            }
        }

        this.typeName = typeName;
    }

    public void makeVersion() {
        this.isVersion = true;
    }

    public boolean isVersion() {
        return isVersion;
    }

    public void makeNationalized() {
        this.isNationalized = true;
    }

    public boolean isNationalized() {
        return isNationalized;
    }

    public void makeLob() {
        this.isLob = true;
    }

    public boolean isLob() {
        return isLob;
    }

    public void setTable(Table table) {
        this.table = table;
    }

    @Override
    public void createForeignKey() throws MappingException {
    }

    @Override
    public void createForeignKeyOfEntity(String entityName) {
        if (!hasFormula() && !"none".equals(getForeignKeyName())) {
            ForeignKey fk = table.createForeignKey(getForeignKeyName(), getConstraintColumns(), entityName,
                    getForeignKeyDefinition());
            fk.setCascadeDeleteEnabled(cascadeDeleteEnabled);
        }
    }

    private IdentifierGenerator identifierGenerator;

    @Override
    public IdentifierGenerator createIdentifierGenerator(IdentifierGeneratorFactory identifierGeneratorFactory,
            Dialect dialect, String defaultCatalog, String defaultSchema, RootClass rootClass)
            throws MappingException {

        if (identifierGenerator != null) {
            return identifierGenerator;
        }

        Properties params = new Properties();

        //if the hibernate-mapping did not specify a schema/catalog, use the defaults
        //specified by properties - but note that if the schema/catalog were specified
        //in hibernate-mapping, or as params, they will already be initialized and
        //will override the values set here (they are in identifierGeneratorProperties)
        if (defaultSchema != null) {
            params.setProperty(PersistentIdentifierGenerator.SCHEMA, defaultSchema);
        }
        if (defaultCatalog != null) {
            params.setProperty(PersistentIdentifierGenerator.CATALOG, defaultCatalog);
        }

        //pass the entity-name, if not a collection-id
        if (rootClass != null) {
            params.setProperty(IdentifierGenerator.ENTITY_NAME, rootClass.getEntityName());
            params.setProperty(IdentifierGenerator.JPA_ENTITY_NAME, rootClass.getJpaEntityName());
        }

        //init the table here instead of earlier, so that we can get a quoted table name
        //TODO: would it be better to simply pass the qualified table name, instead of
        //      splitting it up into schema/catalog/table names
        String tableName = getTable().getQuotedName(dialect);
        params.setProperty(PersistentIdentifierGenerator.TABLE, tableName);

        //pass the column name (a generated id almost always has a single column)
        String columnName = ((Column) getColumnIterator().next()).getQuotedName(dialect);
        params.setProperty(PersistentIdentifierGenerator.PK, columnName);

        if (rootClass != null) {
            StringBuilder tables = new StringBuilder();
            Iterator iter = rootClass.getIdentityTables().iterator();
            while (iter.hasNext()) {
                Table table = (Table) iter.next();
                tables.append(table.getQuotedName(dialect));
                if (iter.hasNext()) {
                    tables.append(", ");
                }
            }
            params.setProperty(PersistentIdentifierGenerator.TABLES, tables.toString());
        } else {
            params.setProperty(PersistentIdentifierGenerator.TABLES, tableName);
        }

        if (identifierGeneratorProperties != null) {
            params.putAll(identifierGeneratorProperties);
        }

        // TODO : we should pass along all settings once "config lifecycle" is hashed out...
        final ConfigurationService cs = metadata.getMetadataBuildingOptions().getServiceRegistry()
                .getService(ConfigurationService.class);

        params.put(AvailableSettings.PREFER_POOLED_VALUES_LO,
                cs.getSetting(AvailableSettings.PREFER_POOLED_VALUES_LO, StandardConverters.BOOLEAN, false));
        if (cs.getSettings().get(AvailableSettings.PREFERRED_POOLED_OPTIMIZER) != null) {
            params.put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER,
                    cs.getSettings().get(AvailableSettings.PREFERRED_POOLED_OPTIMIZER));
        }

        identifierGeneratorFactory.setDialect(dialect);
        identifierGenerator = identifierGeneratorFactory.createIdentifierGenerator(identifierGeneratorStrategy,
                getType(), params);

        return identifierGenerator;
    }

    public boolean isUpdateable() {
        //needed to satisfy KeyValue
        return true;
    }

    public FetchMode getFetchMode() {
        return FetchMode.SELECT;
    }

    public Properties getIdentifierGeneratorProperties() {
        return identifierGeneratorProperties;
    }

    public String getNullValue() {
        return nullValue;
    }

    public Table getTable() {
        return table;
    }

    /**
     * Returns the identifierGeneratorStrategy.
     * @return String
     */
    public String getIdentifierGeneratorStrategy() {
        return identifierGeneratorStrategy;
    }

    public boolean isIdentityColumn(IdentifierGeneratorFactory identifierGeneratorFactory, Dialect dialect) {
        identifierGeneratorFactory.setDialect(dialect);
        return IdentityGenerator.class.isAssignableFrom(
                identifierGeneratorFactory.getIdentifierGeneratorClass(identifierGeneratorStrategy));
    }

    /**
     * Sets the identifierGeneratorProperties.
     * @param identifierGeneratorProperties The identifierGeneratorProperties to set
     */
    public void setIdentifierGeneratorProperties(Properties identifierGeneratorProperties) {
        this.identifierGeneratorProperties = identifierGeneratorProperties;
    }

    /**
     * Sets the identifierGeneratorStrategy.
     * @param identifierGeneratorStrategy The identifierGeneratorStrategy to set
     */
    public void setIdentifierGeneratorStrategy(String identifierGeneratorStrategy) {
        this.identifierGeneratorStrategy = identifierGeneratorStrategy;
    }

    /**
     * Sets the nullValue.
     * @param nullValue The nullValue to set
     */
    public void setNullValue(String nullValue) {
        this.nullValue = nullValue;
    }

    public String getForeignKeyName() {
        return foreignKeyName;
    }

    public void setForeignKeyName(String foreignKeyName) {
        this.foreignKeyName = foreignKeyName;
    }

    public String getForeignKeyDefinition() {
        return foreignKeyDefinition;
    }

    public void setForeignKeyDefinition(String foreignKeyDefinition) {
        this.foreignKeyDefinition = foreignKeyDefinition;
    }

    public boolean isAlternateUniqueKey() {
        return alternateUniqueKey;
    }

    public void setAlternateUniqueKey(boolean unique) {
        this.alternateUniqueKey = unique;
    }

    public boolean isNullable() {
        Iterator itr = getColumnIterator();
        while (itr.hasNext()) {
            final Object selectable = itr.next();
            if (selectable instanceof Formula) {
                // if there are *any* formulas, then the Value overall is
                // considered nullable
                return true;
            } else if (!((Column) selectable).isNullable()) {
                // if there is a single non-nullable column, the Value
                // overall is considered non-nullable.
                return false;
            }
        }
        // nullable by default
        return true;
    }

    public boolean isSimpleValue() {
        return true;
    }

    public boolean isValid(Mapping mapping) throws MappingException {
        return getColumnSpan() == getType().getColumnSpan(mapping);
    }

    public Type getType() throws MappingException {
        if (type != null) {
            return type;
        }

        if (typeName == null) {
            throw new MappingException("No type name");
        }

        if (typeParameters != null
                && Boolean.valueOf(typeParameters.getProperty(DynamicParameterizedType.IS_DYNAMIC))
                && typeParameters.get(DynamicParameterizedType.PARAMETER_TYPE) == null) {
            createParameterImpl();
        }

        Type result = getMetadata().getTypeConfiguration().getTypeResolver().heuristicType(typeName,
                typeParameters);
        // if this is a byte[] version/timestamp, then we need to use RowVersionType
        // instead of BinaryType (HHH-10413)
        if (isVersion && BinaryType.class.isInstance(result)) {
            log.debug("version is BinaryType; changing to RowVersionType");
            result = RowVersionType.INSTANCE;
        }
        if (result == null) {
            String msg = "Could not determine type for: " + typeName;
            if (table != null) {
                msg += ", at table: " + table.getName();
            }
            if (columns != null && columns.size() > 0) {
                msg += ", for columns: " + columns;
            }
            throw new MappingException(msg);
        }

        return result;
    }

    @Override
    public void setTypeUsingReflection(String className, String propertyName) throws MappingException {
        // NOTE : this is called as the last piece in setting SimpleValue type information, and implementations
        // rely on that fact, using it as a signal that all information it is going to get is defined at this point...

        if (typeName != null) {
            // assume either (a) explicit type was specified or (b) determine was already performed
            return;
        }

        if (type != null) {
            return;
        }

        if (attributeConverterDescriptor == null) {
            // this is here to work like legacy.  This should change when we integrate with metamodel to
            // look for SqlTypeDescriptor and JavaTypeDescriptor individually and create the BasicType (well, really
            // keep a registry of [SqlTypeDescriptor,JavaTypeDescriptor] -> BasicType...)
            if (className == null) {
                throw new MappingException(
                        "Attribute types for a dynamic entity must be explicitly specified: " + propertyName);
            }
            typeName = ReflectHelper.reflectedPropertyClass(className, propertyName, getMetadata()
                    .getMetadataBuildingOptions().getServiceRegistry().getService(ClassLoaderService.class))
                    .getName();
            // todo : to fully support isNationalized here we need do the process hinted at above
            //       essentially, much of the logic from #buildAttributeConverterTypeAdapter wrt resolving
            //      a (1) SqlTypeDescriptor, a (2) JavaTypeDescriptor and dynamically building a BasicType
            //       combining them.
            return;
        }

        // we had an AttributeConverter...
        type = buildAttributeConverterTypeAdapter();
    }

    /**
     * Build a Hibernate Type that incorporates the JPA AttributeConverter.  AttributeConverter works totally in
     * memory, meaning it converts between one Java representation (the entity attribute representation) and another
     * (the value bound into JDBC statements or extracted from results).  However, the Hibernate Type system operates
     * at the lower level of actually dealing directly with those JDBC objects.  So even though we have an
     * AttributeConverter, we still need to "fill out" the rest of the BasicType data and bridge calls
     * to bind/extract through the converter.
     * <p/>
     * Essentially the idea here is that an intermediate Java type needs to be used.  Let's use an example as a means
     * to illustrate...  Consider an {@code AttributeConverter<Integer,String>}.  This tells Hibernate that the domain
     * model defines this attribute as an Integer value (the 'entityAttributeJavaType'), but that we need to treat the
     * value as a String (the 'databaseColumnJavaType') when dealing with JDBC (aka, the database type is a
     * VARCHAR/CHAR):<ul>
     *     <li>
     *         When binding values to PreparedStatements we need to convert the Integer value from the entity
     *         into a String and pass that String to setString.  The conversion is handled by calling
     *         {@link AttributeConverter#convertToDatabaseColumn(Object)}
     *     </li>
     *     <li>
     *         When extracting values from ResultSets (or CallableStatement parameters) we need to handle the
     *         value via getString, and convert that returned String to an Integer.  That conversion is handled
     *         by calling {@link AttributeConverter#convertToEntityAttribute(Object)}
     *     </li>
     * </ul>
     *
     * @return The built AttributeConverter -> Type adapter
     *
     * @todo : ultimately I want to see attributeConverterJavaType and attributeConverterJdbcTypeCode specify-able separately
     * then we can "play them against each other" in terms of determining proper typing
     *
     * @todo : see if we already have previously built a custom on-the-fly BasicType for this AttributeConverter; see note below about caching
     */
    @SuppressWarnings("unchecked")
    private Type buildAttributeConverterTypeAdapter() {
        // todo : validate the number of columns present here?

        final JpaAttributeConverter jpaAttributeConverter = attributeConverterDescriptor
                .createJpaAttributeConverter(new JpaAttributeConverterCreationContext() {
                    @Override
                    public ManagedBeanRegistry getManagedBeanRegistry() {
                        return getMetadata().getMetadataBuildingOptions().getServiceRegistry()
                                .getService(ManagedBeanRegistry.class);
                    }

                    @Override
                    public org.hibernate.type.descriptor.java.spi.JavaTypeDescriptorRegistry getJavaTypeDescriptorRegistry() {
                        return metadata.getTypeConfiguration().getJavaTypeDescriptorRegistry();
                    }
                });

        final BasicJavaDescriptor entityAttributeJavaTypeDescriptor = jpaAttributeConverter
                .getDomainJavaTypeDescriptor();

        // build the SqlTypeDescriptor adapter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Going back to the illustration, this should be a SqlTypeDescriptor that handles the Integer <-> String
        //      conversions.  This is the more complicated piece.  First we need to determine the JDBC type code
        //      corresponding to the AttributeConverter's declared "databaseColumnJavaType" (how we read that value out
        //       of ResultSets).  See JdbcTypeJavaClassMappings for details.  Again, given example, this should return
        //       VARCHAR/CHAR
        final SqlTypeDescriptor recommendedSqlType = jpaAttributeConverter.getRelationalJavaTypeDescriptor()
                .getJdbcRecommendedSqlType(
                        // todo (6.0) : handle the other JdbcRecommendedSqlTypeMappingContext methods
                        metadata::getTypeConfiguration);
        int jdbcTypeCode = recommendedSqlType.getSqlType();
        if (isLob()) {
            if (LobTypeMappings.isMappedToKnownLobCode(jdbcTypeCode)) {
                jdbcTypeCode = LobTypeMappings.getLobCodeTypeMapping(jdbcTypeCode);
            } else {
                if (Serializable.class.isAssignableFrom(entityAttributeJavaTypeDescriptor.getJavaType())) {
                    jdbcTypeCode = Types.BLOB;
                } else {
                    throw new IllegalArgumentException(String.format(Locale.ROOT,
                            "JDBC type-code [%s (%s)] not known to have a corresponding LOB equivalent, and Java type is not Serializable (to use BLOB)",
                            jdbcTypeCode, JdbcTypeNameMapper.getTypeName(jdbcTypeCode)));
                }
            }
        }
        if (isNationalized()) {
            jdbcTypeCode = NationalizedTypeMappings.toNationalizedTypeCode(jdbcTypeCode);
        }

        // find the standard SqlTypeDescriptor for that JDBC type code (allow itr to be remapped if needed!)
        final SqlTypeDescriptor sqlTypeDescriptor = getMetadata().getMetadataBuildingOptions().getServiceRegistry()
                .getService(JdbcServices.class).getJdbcEnvironment().getDialect().remapSqlTypeDescriptor(
                        metadata.getTypeConfiguration().getSqlTypeDescriptorRegistry().getDescriptor(jdbcTypeCode));

        // and finally construct the adapter, which injects the AttributeConverter calls into the binding/extraction
        //       process...
        final SqlTypeDescriptor sqlTypeDescriptorAdapter = new AttributeConverterSqlTypeDescriptorAdapter(
                jpaAttributeConverter, sqlTypeDescriptor, jpaAttributeConverter.getRelationalJavaTypeDescriptor());

        // todo : cache the AttributeConverterTypeAdapter in case that AttributeConverter is applied multiple times.

        final String name = AttributeConverterTypeAdapter.NAME_PREFIX
                + jpaAttributeConverter.getConverterJavaTypeDescriptor().getJavaType().getName();
        final String description = String.format("BasicType adapter for AttributeConverter<%s,%s>",
                jpaAttributeConverter.getDomainJavaTypeDescriptor().getJavaType().getSimpleName(),
                jpaAttributeConverter.getRelationalJavaTypeDescriptor().getJavaType().getSimpleName());
        return new AttributeConverterTypeAdapter(name, description, jpaAttributeConverter, sqlTypeDescriptorAdapter,
                jpaAttributeConverter.getDomainJavaTypeDescriptor().getJavaType(),
                jpaAttributeConverter.getRelationalJavaTypeDescriptor().getJavaType(),
                entityAttributeJavaTypeDescriptor);
    }

    public boolean isTypeSpecified() {
        return typeName != null;
    }

    public void setTypeParameters(Properties parameterMap) {
        this.typeParameters = parameterMap;
    }

    public Properties getTypeParameters() {
        return typeParameters;
    }

    public void copyTypeFrom(SimpleValue sourceValue) {
        setTypeName(sourceValue.getTypeName());
        setTypeParameters(sourceValue.getTypeParameters());

        type = sourceValue.type;
        attributeConverterDescriptor = sourceValue.attributeConverterDescriptor;
    }

    @Override
    public boolean isSame(Value other) {
        return this == other || other instanceof SimpleValue && isSame((SimpleValue) other);
    }

    protected static boolean isSame(Value v1, Value v2) {
        return v1 == v2 || v1 != null && v2 != null && v1.isSame(v2);
    }

    public boolean isSame(SimpleValue other) {
        return Objects.equals(columns, other.columns) && Objects.equals(typeName, other.typeName)
                && Objects.equals(typeParameters, other.typeParameters) && Objects.equals(table, other.table)
                && Objects.equals(foreignKeyName, other.foreignKeyName)
                && Objects.equals(foreignKeyDefinition, other.foreignKeyDefinition);
    }

    @Override
    public String toString() {
        return getClass().getName() + '(' + columns.toString() + ')';
    }

    public Object accept(ValueVisitor visitor) {
        return visitor.accept(this);
    }

    public boolean[] getColumnInsertability() {
        return extractBooleansFromList(insertability);
    }

    public boolean[] getColumnUpdateability() {
        return extractBooleansFromList(updatability);
    }

    private static boolean[] extractBooleansFromList(List<Boolean> list) {
        final boolean[] array = new boolean[list.size()];
        int i = 0;
        for (Boolean value : list) {
            array[i++] = value;
        }
        return array;
    }

    public void setJpaAttributeConverterDescriptor(ConverterDescriptor descriptor) {
        this.attributeConverterDescriptor = descriptor;
    }

    private void createParameterImpl() {
        try {
            String[] columnsNames = new String[columns.size()];
            for (int i = 0; i < columns.size(); i++) {
                Selectable column = columns.get(i);
                if (column instanceof Column) {
                    columnsNames[i] = ((Column) column).getName();
                }
            }

            final XProperty xProperty = (XProperty) typeParameters.get(DynamicParameterizedType.XPROPERTY);
            // todo : not sure this works for handling @MapKeyEnumerated
            final Annotation[] annotations = xProperty == null ? null : xProperty.getAnnotations();

            final ClassLoaderService classLoaderService = getMetadata().getMetadataBuildingOptions()
                    .getServiceRegistry().getService(ClassLoaderService.class);
            typeParameters.put(DynamicParameterizedType.PARAMETER_TYPE,
                    new ParameterTypeImpl(
                            classLoaderService.classForName(
                                    typeParameters.getProperty(DynamicParameterizedType.RETURNED_CLASS)),
                            annotations, table.getCatalog(), table.getSchema(), table.getName(),
                            Boolean.valueOf(typeParameters.getProperty(DynamicParameterizedType.IS_PRIMARY_KEY)),
                            columnsNames));
        } catch (ClassLoadingException e) {
            throw new MappingException("Could not create DynamicParameterizedType for type: " + typeName, e);
        }
    }

    private static final class ParameterTypeImpl implements DynamicParameterizedType.ParameterType {

        private final Class returnedClass;
        private final Annotation[] annotationsMethod;
        private final String catalog;
        private final String schema;
        private final String table;
        private final boolean primaryKey;
        private final String[] columns;

        private ParameterTypeImpl(Class returnedClass, Annotation[] annotationsMethod, String catalog,
                String schema, String table, boolean primaryKey, String[] columns) {
            this.returnedClass = returnedClass;
            this.annotationsMethod = annotationsMethod;
            this.catalog = catalog;
            this.schema = schema;
            this.table = table;
            this.primaryKey = primaryKey;
            this.columns = columns;
        }

        @Override
        public Class getReturnedClass() {
            return returnedClass;
        }

        @Override
        public Annotation[] getAnnotationsMethod() {
            return annotationsMethod;
        }

        @Override
        public String getCatalog() {
            return catalog;
        }

        @Override
        public String getSchema() {
            return schema;
        }

        @Override
        public String getTable() {
            return table;
        }

        @Override
        public boolean isPrimaryKey() {
            return primaryKey;
        }

        @Override
        public String[] getColumns() {
            return columns;
        }
    }
}