/*
* (c) Copyright 2011 by Volker Bergmann. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, is permitted under the terms of the
* GNU General Public License (GPL).
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* WITHOUT A WARRANTY OF ANY KIND. ALL EXPRESS OR IMPLIED CONDITIONS,
* REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE
* HEREBY EXCLUDED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.databene.benerator.engine.statement;
import java.io.IOException;
import java.io.StreamTokenizer;
import static java.io.StreamTokenizer.*;
import java.io.StringReader;
import java.util.List;
import org.databene.benerator.composite.ComponentAndVariableSupport;
import org.databene.benerator.composite.GeneratorComponent;
import org.databene.benerator.engine.BeneratorContext;
import org.databene.benerator.factory.ComplexTypeGeneratorFactory;
import org.databene.commons.ArrayBuilder;
import org.databene.commons.ArrayFormat;
import org.databene.commons.ConfigurationError;
import org.databene.commons.Context;
import org.databene.commons.IOUtil;
import org.databene.commons.SyntaxError;
import org.databene.jdbacl.SQLUtil;
import org.databene.jdbacl.identity.IdentityModel;
import org.databene.jdbacl.identity.IdentityProvider;
import org.databene.jdbacl.identity.KeyMapper;
import org.databene.jdbacl.identity.NoIdentity;
import org.databene.jdbacl.model.DBForeignKeyConstraint;
import org.databene.jdbacl.model.DBTable;
import org.databene.jdbacl.model.Database;
import org.databene.model.data.ComplexTypeDescriptor;
import org.databene.model.data.Entity;
import org.databene.model.data.InstanceDescriptor;
import org.databene.model.data.ReferenceDescriptor;
import org.databene.model.data.Uniqueness;
import org.databene.platform.db.DBSystem;
import org.databene.webdecs.DataContainer;
import org.databene.webdecs.DataIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Cascades the 'transcode' operation to all entities configured to be related
* to the currently transcoded entity.<br/><br/>
* Created: 18.04.2011 07:14:34
* @since 0.6.6
* @author Volker Bergmann
*/
public class CascadeStatement extends SequentialStatement implements CascadeParent {
private static final Logger LOGGER = LoggerFactory.getLogger(CascadeStatement.class);
private static final String REF_SYNTAX_MESSAGE = "Expected Syntax: table(column1, column2, ...)";
private CascadeParent parent;
private Reference ref;
private Entity currentEntity;
MutatingTypeExpression typeExpression;
ComplexTypeDescriptor type;
public CascadeStatement(String ref, MutatingTypeExpression typeExpression, CascadeParent parent) {
this.typeExpression = typeExpression;
this.ref = Reference.parse(ref);
this.parent = parent;
this.currentEntity = null;
}
@Override
public boolean execute(BeneratorContext context) {
DBSystem source = getSource(context);
getType(source, context);
IdentityModel identity = parent.getIdentityProvider().getIdentity(type.getName(), false);
String tableName = type.getName();
LOGGER.debug("Cascading transcode from " + parent.currentEntity().type() + " to " + tableName);
// iterate rows
List<GeneratorComponent<Entity>> generatorComponents =
ComplexTypeGeneratorFactory.createMutatingGeneratorComponents(type, Uniqueness.NONE, context);
ComponentAndVariableSupport<Entity> cavs = new ComponentAndVariableSupport<Entity>(tableName,
generatorComponents, context);
cavs.init(context);
DataIterator<Entity> iterator = ref.resolveReferences(parent.currentEntity(), source, context);
DataContainer<Entity> container = new DataContainer<Entity>();
while ((container = iterator.next(container)) != null)
mutateAndTranscodeEntity(container.getData(), identity, cavs, context);
IOUtil.close(iterator);
return true;
}
public DBSystem getSource(BeneratorContext context) {
return parent.getSource(context);
}
public Entity currentEntity() {
return currentEntity;
}
public KeyMapper getKeyMapper() {
return parent.getKeyMapper();
}
public IdentityProvider getIdentityProvider() {
return parent.getIdentityProvider();
}
public boolean needsNkMapping(String type) {
return parent.needsNkMapping(type);
}
public DBSystem getTarget(BeneratorContext context) {
return parent.getTarget(context);
}
public ComplexTypeDescriptor getType(DBSystem db, BeneratorContext context) {
if (type == null) {
String parentType = parent.getType(db, context).getName();
typeExpression.setTypeName(ref.getTargetTableName(parentType, db, context));
type = typeExpression.evaluate(context);
}
return type;
}
// implementation --------------------------------------------------------------------------------------------------
private void mutateAndTranscodeEntity(Entity sourceEntity, IdentityModel identity, ComponentAndVariableSupport<Entity> cavs, BeneratorContext context) {
Object sourcePK = sourceEntity.idComponentValues();
boolean mapNk = parent.needsNkMapping(sourceEntity.type());
String nk = null;
KeyMapper mapper = getKeyMapper();
DBSystem source = getSource(context);
if (mapNk)
nk = mapper.getNaturalKey(source.getId(), identity, sourcePK);
Entity targetEntity = new Entity(sourceEntity);
cavs.apply(targetEntity, context);
Object targetPK = targetEntity.idComponentValues();
transcodeForeignKeys(targetEntity, source, context);
mapper.store(source.getId(), identity, nk, sourcePK, targetPK);
getTarget(context).store(targetEntity);
LOGGER.debug("transcoded {} to {}", sourceEntity, targetEntity);
cascade(sourceEntity, context);
}
private void transcodeForeignKeys(Entity entity, DBSystem source, Context context) {
ComplexTypeDescriptor tableDescriptor = entity.descriptor();
for (InstanceDescriptor component : tableDescriptor.getParts()) {
if (component instanceof ReferenceDescriptor) {
ReferenceDescriptor fk = (ReferenceDescriptor) component;
String refereeTableName = fk.getTargetType();
Object sourceRef = entity.get(fk.getName());
if (sourceRef != null) {
IdentityProvider identityProvider = parent.getIdentityProvider();
IdentityModel sourceIdentity = identityProvider.getIdentity(refereeTableName, false);
if (sourceIdentity == null) {
DBTable refereeTable = source.getDbMetaData().getTable(refereeTableName);
sourceIdentity = new NoIdentity(refereeTable.getName());
identityProvider.registerIdentity(sourceIdentity, refereeTableName);
}
boolean needsNkMapping = parent.needsNkMapping(refereeTableName);
if (sourceIdentity instanceof NoIdentity && needsNkMapping)
throw new ConfigurationError("No identity defined for table " + refereeTableName);
KeyMapper mapper = parent.getKeyMapper();
Object targetRef;
if (needsNkMapping) {
String sourceRefNK = mapper.getNaturalKey(source.getId(), sourceIdentity, sourceRef);
targetRef = mapper.getTargetPK(sourceIdentity, sourceRefNK);
} else {
targetRef = mapper.getTargetPK(source.getId(), sourceIdentity, sourceRef);
}
if (targetRef == null) {
String message = "No mapping found for " + source.getId() + '.' + refereeTableName + "#" + sourceRef +
" referred in " + entity.type() + "(" + fk.getName() + "). " +
"Probably has not been in the result set of the former '" + refereeTableName + "' nk query.";
getErrorHandler(context).handleError(message);
}
entity.set(fk.getName(), targetRef);
}
}
}
}
private void cascade(Entity sourceEntity, BeneratorContext context) {
this.currentEntity = sourceEntity;
executeSubStatements(context);
this.currentEntity = null;
}
public static class Reference {
private String refererTableName;
private String[] columnNames;
private DBForeignKeyConstraint fk;
private Database database;
private DBTable refererTable;
private DBTable refereeTable;
private DBTable targetTable;
public Reference(String refererTableName, String[] columnNames) {
this.refererTableName = refererTableName;
this.columnNames = columnNames;
}
public String getTargetTableName(String parentTable, DBSystem db, BeneratorContext context) {
if (!parentTable.equals(refererTableName))
return refererTableName;
else {
initIfNecessary(parentTable, db, context);
return targetTable.getName();
}
}
public DataIterator<Entity> resolveReferences(Entity currentEntity, DBSystem db, BeneratorContext context) {
initIfNecessary(currentEntity.type(), db, context);
DBTable parentTable = database.getTable(currentEntity.type());
if (parentTable.equals(refereeTable))
return resolveToManyReference(currentEntity, fk, db, context); // including self-recursion
else if (parentTable.equals(refererTable))
return resolveToOneReference(currentEntity, fk, db, context);
else
throw new ConfigurationError("Table '" + parentTable + "' does not relate to the foreign key " +
refererTableName + '(' + ArrayFormat.format(columnNames) + ')');
}
private void initIfNecessary(String parentTable, DBSystem db, BeneratorContext context) {
if (this.database != null)
return;
this.database = db.getDbMetaData();
this.refererTable = this.database.getTable(refererTableName);
this.fk = refererTable.getForeignKeyConstraint(columnNames);
this.refereeTable = fk.getRefereeTable();
this.targetTable = (parentTable.equalsIgnoreCase(refereeTable.getName()) ? refererTable : refereeTable);
}
DataIterator<Entity> resolveToManyReference(
Entity fromEntity, DBForeignKeyConstraint fk, DBSystem db, BeneratorContext context) {
StringBuilder selector = new StringBuilder();
String[] refererColumnNames = fk.getColumnNames();
String[] refereeColumnNames = fk.getRefereeColumnNames();
for (int i = 0; i < refererColumnNames.length; i++) {
if (selector.length() > 0)
selector.append(" and ");
Object refereeColumnValue = fromEntity.get(refereeColumnNames[i]);
selector.append(refererColumnNames[i]).append('=').append(SQLUtil.renderValue(refereeColumnValue));
}
return db.queryEntities(fk.getTable().getName(), selector.toString(), context).iterator();
}
DataIterator<Entity> resolveToOneReference(
Entity fromEntity, DBForeignKeyConstraint fk, DBSystem db, BeneratorContext context) {
StringBuilder selector = new StringBuilder();
String[] refererColumnNames = fk.getColumnNames();
String[] refereeColumnNames = fk.getRefereeColumnNames();
for (int i = 0; i < refererColumnNames.length; i++) {
if (selector.length() > 0)
selector.append(" and ");
Object refererColumnValue = fromEntity.get(refererColumnNames[i]);
selector.append(refereeColumnNames[i]).append('=').append(SQLUtil.renderValue(refererColumnValue));
}
return db.queryEntities(fk.getRefereeTable().getName(), selector.toString(), context).iterator();
}
static Reference parse(String refSpec) {
StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(refSpec));
tokenizer.wordChars('_', '_');
try {
// parse table name
int token = tokenizer.nextToken();
if (token != TT_WORD)
throw new SyntaxError(REF_SYNTAX_MESSAGE, refSpec);
String tableName = tokenizer.sval;
// parse column names
if ((token = tokenizer.nextToken()) != '(')
throw new SyntaxError(REF_SYNTAX_MESSAGE, refSpec);
ArrayBuilder<String> columnNames = new ArrayBuilder<String>(String.class);
do {
if ((token = tokenizer.nextToken()) != TT_WORD)
throw new SyntaxError(REF_SYNTAX_MESSAGE, refSpec);
columnNames.add(tokenizer.sval);
token = tokenizer.nextToken();
if (token != ',' && token != ')')
throw new SyntaxError(REF_SYNTAX_MESSAGE, refSpec);
} while (token == ',');
if (token != ')')
throw new SyntaxError("reference definition must end with ')'", refSpec);
return new Reference(tableName, columnNames.toArray());
} catch (IOException e) {
throw new SyntaxError(REF_SYNTAX_MESSAGE, refSpec);
}
}
}
}