blob: cb548b3a742fa5c71da4959654cddcbb59568771 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.polygene.entitystore.sql;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.polygene.api.association.AssociationDescriptor;
import org.apache.polygene.api.common.QualifiedName;
import org.apache.polygene.api.entity.EntityDescriptor;
import org.apache.polygene.api.entity.EntityReference;
import org.apache.polygene.api.property.PropertyDescriptor;
import org.apache.polygene.api.serialization.Serialization;
import org.apache.polygene.api.type.ValueType;
import org.apache.polygene.spi.entity.ManyAssociationState;
import org.apache.polygene.spi.entity.NamedAssociationState;
import org.apache.polygene.spi.entitystore.helpers.DefaultEntityState;
import org.jooq.Field;
import org.jooq.InsertSetMoreStep;
import org.jooq.InsertSetStep;
import org.jooq.Record;
import org.jooq.SQLDialect;
import org.jooq.Table;
import org.jooq.UpdateSetMoreStep;
import org.jooq.impl.DSL;
/**
* MixinTable is a class that handles the creation of the queries into the Mixin tables, both for insertions/updates
* as well as retrieval.
* <p>
* Note that these tables are only queried as part of a SQL {@code JOIN} statement and never directly.
* </p>
* <p>
* Creation of the actual Mixin tables happens in {@link TypesTable}.
* </p>
*/
class MixinTable
{
static final String NAME_COLUMN_NAME = "_name";
static final String INDEX_COLUMN_NAME = "_index"; // either index in ManyAssociation or name in NamedAssociation
static final String REFERENCE_COLUMN_NAME = "_reference";
private static final String ASSOCS_TABLE_POSTFIX = "_ASSOCS";
private final Table<Record> mixinTable;
private final Table<Record> mixinAssocsTable;
private Field<String> nameColumn;
private Field<String> referenceColumn;
private Field<String> indexColumn;
private final JooqDslContext dsl;
private final Map<QualifiedName, Field<Object>> properties = new ConcurrentHashMap<>();
private final Map<QualifiedName, Field<String>> associations = new ConcurrentHashMap<>();
private final List<QualifiedName> manyAssociations = new CopyOnWriteArrayList<>();
private final List<QualifiedName> namedAssociations = new CopyOnWriteArrayList<>();
private final Class<?> mixinType;
private final TypesTable types;
private Serialization serialization;
MixinTable( JooqDslContext dsl, SQLDialect dialect, TypesTable types, Class<?> mixinType,
EntityDescriptor descriptor, Serialization serialization )
{
this.types = types;
this.dsl = dsl;
this.mixinType = mixinType;
this.serialization = serialization;
nameColumn = SqlType.makeField( NAME_COLUMN_NAME, String.class, dialect);
referenceColumn = SqlType.makeField( REFERENCE_COLUMN_NAME, String.class, dialect );
indexColumn = SqlType.makeField( INDEX_COLUMN_NAME, String.class, dialect );
mixinTable = types.tableFor( mixinType, descriptor );
mixinAssocsTable = getAssocsTable( descriptor );
descriptor.valueType().properties()
.filter( this::isThisMixin )
.forEach( propDescriptor ->
{
QualifiedName propertyName = propDescriptor.qualifiedName();
Field<Object> propertyField = types.fieldOf( propDescriptor );
properties.put( propertyName, propertyField );
}
);
descriptor.valueType().associations()
.filter( this::isThisMixin )
.forEach( assocDescriptor ->
{
QualifiedName assocName = assocDescriptor.qualifiedName();
Field<String> assocField = types.fieldOf( assocDescriptor );
associations.put( assocName, assocField );
}
);
descriptor.valueType().manyAssociations()
.filter( this::isThisMixin )
.forEach( assocDescriptor -> manyAssociations.add( assocDescriptor.qualifiedName() ) );
descriptor.valueType().namedAssociations()
.filter( this::isThisMixin )
.forEach( assocDescriptor -> namedAssociations.add( assocDescriptor.qualifiedName() ) );
}
void insertMixinState( DefaultEntityState state, String valueIdentity )
{
InsertSetMoreStep<Record> primaryTable =
dsl.insertInto( mixinTable )
.set( types.identityColumn(), valueIdentity )
.set( types.createdColumn(), new Timestamp( System.currentTimeMillis() ) );
properties
.entrySet()
.stream()
.filter( entry -> !entry.getKey().name().equals( "identity" ) )
.forEach( entry -> primaryTable.set( entry.getValue(), getStateValue( entry.getValue(), state, entry.getKey() ) ) );
associations.forEach( ( assocName, assocField ) -> primaryTable.set( assocField, referenceToString( state, assocName ) )
);
int result = primaryTable.execute();
if( mixinAssocsTable != null )
{
insertManyAndNamedAssociations( state, valueIdentity );
}
}
private void insertManyAndNamedAssociations( DefaultEntityState state, String valueIdentity )
{
manyAssociations.forEach( assocName ->
{
InsertSetStep<Record> assocsTable = dsl.insertInto( mixinAssocsTable );
ManyAssociationState entityReferences = state.manyAssociationValueOf( assocName );
int endCount = entityReferences.count();
int counter = 0;
for( EntityReference ref : entityReferences )
{
InsertSetMoreStep<Record> set = assocsTable.set( types.identityColumn(), valueIdentity )
.set( types.createdColumn(), new Timestamp( System.currentTimeMillis() ) )
.set( nameColumn, assocName.name() )
.set( indexColumn, "" + counter++ )
.set( referenceColumn, ref == null ? null : ref.identity().toString() );
if( ++counter < endCount )
{
set.newRecord();
}
}
if( counter > 0 )
{
InsertSetMoreStep<Record> assocs = assocsTable.set( Collections.emptyMap() );
assocs.execute();
}
} );
namedAssociations.forEach( assocName ->
{
InsertSetStep<Record> assocsTable = dsl.insertInto( mixinAssocsTable );
NamedAssociationState entityReferences = state.namedAssociationValueOf( assocName );
int count = entityReferences.count();
if( count > 0 )
{
for( String name : entityReferences )
{
EntityReference ref = entityReferences.get( name );
InsertSetMoreStep<Record> set = assocsTable.set( types.identityColumn(), valueIdentity )
.set( types.createdColumn(), new Timestamp( System.currentTimeMillis() ) )
.set( nameColumn, assocName.name() )
.set( indexColumn, name )
.set( referenceColumn, ref.identity().toString() );
if( --count > 0 )
{
set.newRecord();
}
}
InsertSetMoreStep<Record> assocs = assocsTable.set( Collections.emptyMap() );
assocs.execute();
}
} );
}
Table<Record> associationsTable()
{
return mixinAssocsTable;
}
private boolean isThisMixin( PropertyDescriptor descriptor )
{
Class<?> declaringClass = declaredIn( descriptor );
return mixinType.equals( declaringClass );
}
private boolean isThisMixin( AssociationDescriptor descriptor )
{
Class<?> declaringClass = declaredIn( descriptor );
return mixinType.equals( declaringClass );
}
private Class<?> declaredIn( PropertyDescriptor descriptor )
{
AccessibleObject accessor = descriptor.accessor();
if( accessor instanceof Method )
{
return ( (Method) accessor ).getDeclaringClass();
}
throw new UnsupportedOperationException( "Property declared as " + accessor.getClass() + " is not supported in this Entity Store yet." );
}
private Class<?> declaredIn( AssociationDescriptor descriptor )
{
AccessibleObject accessor = descriptor.accessor();
if( accessor instanceof Method )
{
return ( (Method) accessor ).getDeclaringClass();
}
throw new UnsupportedOperationException( "Property declared as " + accessor.getClass() + " is not supported in this Entity Store yet." );
}
void modifyMixinState( DefaultEntityState state, String valueId )
{
UpdateSetMoreStep<Record> primaryTable =
dsl.update( mixinTable )
.set( Collections.emptyMap() ); // empty map is a hack to get the right type returned from JOOQ.
properties
.entrySet()
.stream()
.filter( entry -> !entry.getKey().name().equals( "identity" ) )
.forEach( entry -> primaryTable.set( entry.getValue(), getStateValue( entry.getValue(), state, entry.getKey() ) ) );
// Set the Association<?> fields
associations.forEach( ( assocName, assocField ) -> primaryTable.set( assocField, referenceToString( state, assocName ) ) );
int result = primaryTable.execute();
if( mixinAssocsTable != null )
{
// Need to remove existing records.
dsl.delete( mixinAssocsTable )
.where( types.identityColumn().eq( valueId ) )
.execute();
insertManyAndNamedAssociations( state, valueId );
}
}
private Table<Record> getAssocsTable( EntityDescriptor descriptor )
{
if( descriptor.state().manyAssociations().count() > 0
|| descriptor.state().namedAssociations().count() > 0 )
{
Table<Record> table = dsl.tableOf( mixinTable.getName() + ASSOCS_TABLE_POSTFIX );
int result = dsl.createTableIfNotExists( table )
.column( types.identityColumn() )
.column( types.createdColumn() )
.column( nameColumn )
.column( indexColumn )
.column( referenceColumn )
.execute();
dsl.createIndex( DSL.name( "IDX_" + table.getName() ) )
.on( table, types.identityColumn() )
.execute();
return table;
}
else
{
return null;
}
}
private Object getStateValue( Field<Object> field, DefaultEntityState state, QualifiedName name )
{
PropertyDescriptor property = state.entityDescriptor().state().findPropertyModelByQualifiedName( name );
ValueType type = property.valueType();
Object value = state.propertyValueOf( name );
Class<?> javaType = field.getDataType().getType();
int sqlType = field.getDataType().getSQLType();
if( value == null )
{
return null;
}
if( value.getClass().isPrimitive() )
{
return value;
}
if( type.equals( ValueType.STRING )
|| type.equals( ValueType.INTEGER )
|| type.equals( ValueType.BOOLEAN )
|| type.equals( ValueType.DOUBLE )
|| type.equals( ValueType.IDENTITY )
|| type.equals( ValueType.LONG )
|| type.equals( ValueType.FLOAT )
|| type.equals( ValueType.BYTE )
|| type.equals( ValueType.CHARACTER )
|| type.equals( ValueType.SHORT )
)
{
return value;
}
return serialization.serialize( value );
}
private String referenceToString( DefaultEntityState state, QualifiedName assocName )
{
EntityReference reference = state.associationValueOf( assocName );
if( reference == null )
{
return null;
}
return reference.identity().toString();
}
}