QueryableDatatypeSyncer.java
/*
* Copyright 2013 Gregory Graham.
*
* 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 nz.co.gregs.dbvolution.datatypes;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import nz.co.gregs.dbvolution.expressions.DBExpression;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.internal.properties.SafeOneWaySimpleTypeAdaptor;
import nz.co.gregs.dbvolution.internal.properties.SafeOneWaySimpleTypeAdaptor.Direction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Allows synchronizations to be done between two QueryableDatatypes, based on a
* Type Adaptor.
*
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
*
* @author Malcolm Lett
*/
public class QueryableDatatypeSyncer implements Serializable {
private static final long serialVersionUID = 1l;
private static final Log LOG = LogFactory.getLog(QueryableDatatypeSyncer.class);
private final String propertyName;
// private final DBTypeAdaptor<Object, Object> typeAdaptor;
private final Class<? extends QueryableDatatype<?>> internalQdtType;
private QueryableDatatype<?> internalQdt;
private SafeOneWaySimpleTypeAdaptor toExternalSimpleTypeAdaptor;
private SafeOneWaySimpleTypeAdaptor toInternalSimpleTypeAdaptor;
/**
*
* @param propertyName used in error messages
* @param internalQdtType internalQdtType
* @param internalQdtLiteralType internalQdtLiteralType
* @param externalSimpleType externalSimpleType
* @param typeAdaptor typeAdaptor typeAdaptor
*/
public QueryableDatatypeSyncer(
String propertyName,
Class<? extends QueryableDatatype<?>> internalQdtType,
Class<?> internalQdtLiteralType,
Class<?> externalSimpleType,
DBTypeAdaptor<Object, Object> typeAdaptor) {
if (typeAdaptor == null) {
throw new DBRuntimeException("Null typeAdaptor was passed, this is an internal bug");
}
this.propertyName = propertyName;
// this.typeAdaptor = typeAdaptor;
this.internalQdtType = internalQdtType;
this.toExternalSimpleTypeAdaptor = new SafeOneWaySimpleTypeAdaptor(propertyName,
typeAdaptor, Direction.TO_EXTERNAL, internalQdtLiteralType, externalSimpleType);
this.toInternalSimpleTypeAdaptor = new SafeOneWaySimpleTypeAdaptor(propertyName,
typeAdaptor, Direction.TO_INTERNAL, externalSimpleType, internalQdtLiteralType);
try {
this.internalQdt = internalQdtType.getConstructor().newInstance();
} catch (InstantiationException e) {
throw new DBRuntimeException("Instantiation error creating internal "
+ internalQdtType.getSimpleName() + " QDT: " + e.getMessage(), e);
} catch (IllegalAccessException | NoSuchMethodException | SecurityException|IllegalArgumentException | InvocationTargetException e) {
throw new DBRuntimeException("Access error creating internal "
+ internalQdtType.getSimpleName() + " QDT: " + e.getMessage(), e);
}
}
/**
* supplies the QDT used internally, that is the QDT the represents the
* database's view of the data.
*
* @return the internal QDT.
*/
public QueryableDatatype<?> getInternalQueryableDatatype() {
return internalQdt;
}
/**
* Replaces the internal QDT with the one provided. Validates that the
* provided QDT is of the correct type.
*
* @param internalQdt internalQdt
*/
public void setInternalQueryableDatatype(QueryableDatatype<?> internalQdt) {
if (internalQdt != null && !internalQdt.getClass().equals(internalQdtType)) {
//throw new RuntimeException("Don't know what to do here: targetQdtType:"+internalQdt.getClass().getSimpleName()+" != "+internalQdtType+":"+internalQdtType.getSimpleName());
throw new ClassCastException("Cannot assign " + internalQdt.getClass().getSimpleName()
+ " to " + internalQdtType.getSimpleName() + " property " + propertyName);
}
this.internalQdt = internalQdt;
}
/**
* Sets the cached internal QDT after adapting the value from the provided
* QDT.
*
* @param externalQdt may be null
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
* @return the updated internal QDT
*/
public QueryableDatatype<?> setInternalQDTFromExternalQDT(QueryableDatatype<?> externalQdt) {
if (externalQdt == null) {
internalQdt = null;
} else {
DBSafeInternalQDTAdaptor qdtAdaptor = new DBSafeInternalQDTAdaptor(internalQdtType, getToInternalSimpleTypeAdaptor());
qdtAdaptor.setTargetQDTFromSourceQDT(getInternalQueryableDatatype(), externalQdt);
}
return getInternalQueryableDatatype();
}
/**
* Sets the provided external QDT from the internal QDT and returns the
* updated external QDT.
*
* @param externalQdt externalQdt
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
* @return the updated external QDT or null if the internal QDT is null
*/
public QueryableDatatype<?> setExternalFromInternalQDT(QueryableDatatype<?> externalQdt) {
if (getInternalQueryableDatatype() == null) {
return null;
} else {
@SuppressWarnings("unchecked")
DBSafeInternalQDTAdaptor qdtAdaptor = new DBSafeInternalQDTAdaptor((Class<? extends QueryableDatatype<?>>) externalQdt.getClass(), getToExternalSimpleTypeAdaptor());
qdtAdaptor.setTargetQDTFromSourceQDT(externalQdt, getInternalQueryableDatatype());
}
return externalQdt;
}
// for DEBUG purposes only
static String qdtToString(QueryableDatatype<?> qdt) {
String literalStr;
if (qdt == null) {
literalStr = null;
} else if (qdt.getLiteralValue() == null) {
literalStr = "null";
} else {
literalStr = qdt.getLiteralValue().getClass().getSimpleName() + "[" + qdt.getLiteralValue() + "]";
}
StringBuilder buf = new StringBuilder();
if (qdt == null) {
buf.append("null");
} else {
buf.append(qdt.getClass().getSimpleName());
buf.append("[");
buf.append(qdt);
buf.append(", ");
buf.append("literal=").append(literalStr);
if (qdt.getOperator() != null) {
buf.append(", ");
buf.append(qdt.getOperator().getClass().getSimpleName());
}
buf.append("]");
}
return buf.toString();
}
/**
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
*
* @return the toExternalSimpleTypeAdaptor
*/
protected SafeOneWaySimpleTypeAdaptor getToExternalSimpleTypeAdaptor() {
return toExternalSimpleTypeAdaptor;
}
/**
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
*
* @return the toInternalSimpleTypeAdaptor
*/
protected SafeOneWaySimpleTypeAdaptor getToInternalSimpleTypeAdaptor() {
return toInternalSimpleTypeAdaptor;
}
/**
*
* @param internalQDT the internal QDT
* @param internalValue the internal value
*/
protected static final void setQDTValueUsingDangerousReflection(QueryableDatatype<?> internalQDT, Object internalValue) {
try {
if (internalValue == null) {
internalQDT.setToNull();
} else {
Method method = internalQDT.getClass().getMethod("setValue", internalValue.getClass());
method.invoke(internalQDT, internalValue);
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new DBRuntimeException("Synchronisation Failed:" + ex.getMessage(), ex);
}
}
/**
* One-shot cycle-aware recursive QDT adaptor. Converts from existing QDT to
* brand new one, and copies from one QDT to another.
*
* <p>
* DBOperators can reference the same QDT that own the operator instance, such
* as:
* <code>QueryableDatatype.setLiteralValue{this.operator = new DBEqualsOperator(this)}</code>.
* Cycles are handled by tracking source QDTs observed and returning the
* previously mapped target QDT when re-observed.
*
* <p>
* Must be used only once for a given read or write of a field.
*/
public static class DBSafeInternalQDTAdaptor {
private final Class<? extends QueryableDatatype<?>> targetQdtType;
private final SafeOneWaySimpleTypeAdaptor simpleTypeAdaptor;
private final List<Map.Entry<QueryableDatatype<?>, QueryableDatatype<?>>> observedSourcesAndTargets
= new ArrayList<>();
/**
* Constructor
*
* @param targetQdtType targetQdtType
* @param typeAdaptor typeAdaptor
*/
public DBSafeInternalQDTAdaptor(
Class<? extends QueryableDatatype<?>> targetQdtType,
SafeOneWaySimpleTypeAdaptor typeAdaptor) {
this.targetQdtType = targetQdtType;
this.simpleTypeAdaptor = typeAdaptor;
}
/**
* Creates a brand new QDT of the configured target type, based on converted
* values from the given QDT. Recursively traverses the operators and inner
* QDT references within the given QDT.
*
* <p>
* If {@code source} is null, returns {@code null}.
*
* @param source the QDT to convert to the target type, may be null
* <p style="color: #F90;">Support DBvolution at
* <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
* @return the newly created QDT of the target type, or null if
* {@code source} was null
*/
public DBExpression convert(DBExpression source) {
if (!(source instanceof QueryableDatatype<?>)) {
return source;
} else {
QueryableDatatype<?> sourceQDT = (QueryableDatatype<?>) source;
try {
// cycle-detection
// (note: important that it uses reference equality, not object equality)
for (Map.Entry<QueryableDatatype<?>, QueryableDatatype<?>> sourceAndTarget : observedSourcesAndTargets) {
if (sourceAndTarget.getKey() == sourceQDT) {
// re-use existing value
return sourceAndTarget.getValue();
}
}
QueryableDatatype<?> targetQdt = newTargetQDT();
setTargetQDTFromSourceQDT(targetQdt, sourceQDT);
LOG.debug(simpleTypeAdaptor + " converting " + qdtToString(sourceQDT) + " ==> " + qdtToString(targetQdt));
return targetQdt;
} catch (RuntimeException e) {
LOG.debug(simpleTypeAdaptor + " converting " + qdtToString(sourceQDT) + " ==> " + e.getClass().getSimpleName());
throw e;
}
}
}
/**
* Updates the target QDT with converted values from the source QDT.
* Recursively traverses the operations and inner QDT references within the
* given source QTD.
*
* @param targetQdt the QDT to update (must not be null)
* @param sourceQdt the QDT with values to convert and copy to the target
* (must not be null)
*/
@SuppressWarnings("unchecked")
protected void setTargetQDTFromSourceQDT(QueryableDatatype<?> targetQdt, QueryableDatatype<?> sourceQdt) {
// sanity checks
if (!targetQdt.getClass().equals(targetQdtType)) {
throw new RuntimeException("Don't know what to do here: targetQdtType:"
+ targetQdt.getClass().getSimpleName() + " != " + targetQdtType + ":" + targetQdtType.getSimpleName());
}
// cycle-detection
// (note: important that it uses reference equality, not object equality)
for (Map.Entry<QueryableDatatype<?>, QueryableDatatype<?>> soFarEntry : observedSourcesAndTargets) {
if (soFarEntry.getKey() == sourceQdt) {
// already observed, so already done.
return;
}
}
observedSourcesAndTargets.add(new SimpleEntry<QueryableDatatype<?>, QueryableDatatype<?>>(sourceQdt, targetQdt));
// copy simple fields
targetQdt.setChanged(sourceQdt.hasChanged());
if (sourceQdt.isNull()) {
targetQdt.setToNull();
}
// targetQdt.isPrimaryKey = sourceQdt.isPrimaryKey;
targetQdt.setDefined(sourceQdt.isDefined());
if (sourceQdt.getSortOrder().equals(QueryableDatatype.SORT_ASCENDING)) {
targetQdt.setSortOrderAscending();
} else {
targetQdt.setSortOrderDescending();
}
targetQdt.setColumnExpression(sourceQdt.getColumnExpression());
// copy literal value with translation
//targetQdt.setLiteralValue(simpleTypeAdaptor.convert(sourceQdt.getLiteralValue()));
setQDTValueUsingDangerousReflection(targetQdt, simpleTypeAdaptor.convert(sourceQdt.getLiteralValue()));
// copy previous value with translation
targetQdt.setPreviousValue((QueryableDatatype) convert(sourceQdt.getPreviousValueAsQDT()));
// copy operator with translation
if (sourceQdt.getOperator() == null) {
targetQdt.setOperator(null);
} else {
targetQdt.setOperator(sourceQdt.getOperator().copyAndAdapt(this));
}
}
// factory method
private QueryableDatatype<?> newTargetQDT() {
try {
return targetQdtType.getConstructor().newInstance();
} catch (InstantiationException e) {
throw new DBRuntimeException("Instantiation error creating internal "
+ targetQdtType.getSimpleName() + " QDT: " + e.getMessage(), e);
} catch (IllegalAccessException|NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) {
throw new DBRuntimeException("Access error creating internal "
+ targetQdtType.getSimpleName() + " QDT: " + e.getMessage(), e);
}
}
}
}