package io.cattle.platform.object.impl;
import io.cattle.platform.engine.idempotent.Idempotent;
import io.cattle.platform.engine.idempotent.IdempotentExecution;
import io.cattle.platform.engine.idempotent.IdempotentExecutionNoReturn;
import io.cattle.platform.object.jooq.utils.JooqUtils;
import io.cattle.platform.object.meta.MapRelationship;
import io.cattle.platform.object.meta.ObjectMetaDataManager;
import io.cattle.platform.object.meta.Relationship;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.object.util.ObjectUtils;
import io.cattle.platform.util.type.CollectionUtils;
import io.cattle.platform.util.type.UnmodifiableMap;
import io.github.ibuildthecloud.gdapi.model.Schema;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.inject.Inject;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.ForeignKey;
import org.jooq.ResultQuery;
import org.jooq.SelectQuery;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.UpdatableRecord;
import org.jooq.impl.DSL;
import org.jooq.impl.DefaultDSLContext;
public class JooqObjectManager extends AbstractObjectManager {
// private static final Logger log =
// LoggerFactory.getLogger(JooqObjectManager.class);
Map<ChildReferenceCacheKey, ForeignKey<?, ?>> childReferenceCache = Collections
.synchronizedMap(new WeakHashMap<ChildReferenceCacheKey, ForeignKey<?, ?>>());
Configuration configuration;
Configuration lockingConfiguration;
TransactionDelegate transactionDelegate;
@SuppressWarnings("unchecked")
@Override
public <T> T instantiate(Class<T> clz, Map<String, Object> properties) {
Class<UpdatableRecord<?>> recordClass = JooqUtils.getRecordClass(schemaFactory, clz);
UpdatableRecord<?> record = JooqUtils.getRecord(recordClass);
record.attach(getConfiguration());
return (T) record;
}
@SuppressWarnings("unchecked")
@Override
public <T> T newRecord(Class<T> type) {
Class<?> clz = JooqUtils.getRecordClass(schemaFactory, type);
try {
return (T) clz.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override
public <T> T insert(T instance, Class<T> clz, Map<String, Object> properties) {
final UpdatableRecord<?> record = JooqUtils.getRecordObject(instance);
record.attach(configuration);
Idempotent.change(new IdempotentExecutionNoReturn() {
@Override
protected void executeNoResult() {
record.insert();
}
});
return instance;
}
@Override
public <T> T reload(T obj) {
if (obj == null) {
return null;
}
UpdatableRecord<?> record = JooqUtils.getRecordObject(obj);
record.attach(getConfiguration());
record.refresh();
return obj;
}
@Override
public <T> T persist(T obj) {
final UpdatableRecord<?> record = JooqUtils.getRecordObject(obj);
record.attach(getConfiguration());
record.update();
Idempotent.change(new IdempotentExecutionNoReturn() {
@Override
protected void executeNoResult() {
persistRecord(record);
}
});
return obj;
}
@Override
public <T> T setFields(final Object obj, final Map<String, Object> values) {
return setFields(null, obj, values);
}
@Override
public <T> T setFields(final Schema schema, final Object obj, final Map<String, Object> values) {
return Idempotent.change(new IdempotentExecution<T>() {
@Override
public T execute() {
return setFieldsInternal(schema, obj, values);
}
});
}
@SuppressWarnings("unchecked")
protected <T> T setFieldsInternal(final Schema schema, final Object obj, final Map<String, Object> values) {
final List<UpdatableRecord<?>> pending = new ArrayList<UpdatableRecord<?>>();
Map<Object, Object> toWrite = toObjectsToWrite(obj, values);
setFields(schema, obj, toWrite, pending);
if (pending.size() == 1) {
persistRecord(pending.get(0));
} else if (pending.size() > 1) {
transactionDelegate.doInTransaction(new Runnable() {
@Override
public void run() {
for (UpdatableRecord<?> record : pending) {
persistRecord(record);
}
}
});
}
return (T) obj;
}
@SuppressWarnings("unchecked")
protected void setFields(Schema schema, Object obj, Map<Object, Object> toWrite, List<UpdatableRecord<?>> result) {
String type = getPossibleSubType(obj);
if (schema == null) {
schema = schemaFactory.getSchema(type);
}
UpdatableRecord<?> record = JooqUtils.getRecordObject(obj);
for (Map.Entry<Object, Object> entry : toWrite.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (key instanceof String) {
String name = (String) key;
if (name.startsWith(ObjectMetaDataManager.APPEND) && value instanceof Map<?, ?>) {
name = name.substring(ObjectMetaDataManager.APPEND.length());
Object mapObj = ObjectUtils.getPropertyIgnoreErrors(obj, name);
if (mapObj instanceof Map<?, ?>) {
mapObj = mergeMap((Map<Object, Object>) value, (Map<Object, Object>) mapObj);
}
setField(schema, record, name, mapObj);
} else {
setField(schema, record, (String) key, value);
}
} else if (key instanceof Relationship && value instanceof Map<?, ?>) {
Relationship rel = (Relationship) key;
Object id = ObjectUtils.getPropertyIgnoreErrors(obj, rel.getPropertyName());
if (id == null) {
continue;
}
Object refObj = loadResource(rel.getObjectType(), id);
setFields(schema, refObj, (Map<Object, Object>) value, result);
}
}
if (record.changed()) {
result.add(record);
}
}
@SuppressWarnings("unchecked")
protected Map<Object, Object> mergeMap(Map<Object, Object> src, Map<Object, Object> dest) {
if (dest instanceof UnmodifiableMap<?, ?>) {
dest = ((UnmodifiableMap<Object, Object>) dest).getModifiableCopy();
}
for (Map.Entry<Object, Object> entry : src.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (key instanceof String) {
String name = (String) key;
if (name.startsWith(ObjectMetaDataManager.APPEND) && value instanceof Map<?, ?>) {
name = name.substring(ObjectMetaDataManager.APPEND.length());
Object mapObj = dest.get(name);
if (mapObj instanceof Map<?, ?>) {
mergeMap((Map<Object, Object>) value, (Map<Object, Object>) mapObj);
} else {
dest.put(name, value);
}
} else {
dest.put(name, value);
}
}
}
return dest;
}
protected void persistRecord(final UpdatableRecord<?> record) {
if (record.field(ObjectMetaDataManager.DATA_FIELD) != null && record.changed(ObjectMetaDataManager.DATA_FIELD)) {
record.attach(getLockingConfiguration());
} else if (record.field(ObjectMetaDataManager.STATE_FIELD) != null && record.changed(ObjectMetaDataManager.STATE_FIELD)) {
record.attach(getLockingConfiguration());
} else {
record.attach(getConfiguration());
}
if (record.changed()) {
if (record.update() == 0) {
throw new IllegalStateException("Failed to update [" + record + "]");
}
}
}
@Override
public <T> List<T> children(Object obj, Class<T> type) {
return children(obj, type, null);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public <T> List<T> children(Object obj, Class<T> type, String propertyName) {
if (obj == null) {
return Collections.emptyList();
}
UpdatableRecord<?> recordObject = JooqUtils.getRecordObject(obj);
Class<UpdatableRecord<?>> parent = JooqUtils.getRecordClass(schemaFactory, obj.getClass());
Class<UpdatableRecord<?>> child = JooqUtils.getRecordClass(schemaFactory, type);
ChildReferenceCacheKey key = new ChildReferenceCacheKey(parent, child, propertyName);
TableField<?, ?> propertyField = (TableField<?, ?>) (propertyName == null ? null : metaDataManager.convertFieldNameFor(getType(type), propertyName));
ForeignKey<?, ?> foreignKey = childReferenceCache.get(key);
if (foreignKey == null) {
Table<?> childTable = JooqUtils.getTableFromRecordClass(child);
for (ForeignKey<?, ?> foreignKeyTest : childTable.getReferences()) {
if (foreignKeyTest.getKey().getTable().getRecordType() == parent) {
if (propertyField != null) {
if (foreignKeyTest.getFields().get(0).getName().equals(propertyField.getName())) {
if (foreignKey != null) {
throw new IllegalStateException("Found more that one foreign key from [" + child + "] to [" + parent + "]");
}
foreignKey = foreignKeyTest;
}
} else if (foreignKey == null) {
foreignKey = foreignKeyTest;
} else {
throw new IllegalStateException("Found more that one foreign key from [" + child + "] to [" + parent + "]");
}
}
}
if (foreignKey == null) {
throw new IllegalStateException("Failed to find a foreign key from [" + child + "] to [" + parent + "]");
}
childReferenceCache.put(key, foreignKey);
}
recordObject.attach(getConfiguration());
return (List<T>) recordObject.fetchChildren((ForeignKey) foreignKey);
}
@Override
public <T> List<T> mappedChildren(Object obj, Class<T> type) {
if (obj == null) {
return Collections.emptyList();
}
Schema schema = schemaFactory.getSchema(type);
if (schema != null) {
String typeName = schemaFactory.getSchemaName(obj.getClass());
String linkName = schema.getPluralName();
Relationship rel = getMetaDataManager().getRelationship(typeName, linkName);
if (rel != null) {
return getListByRelationship(obj, rel);
}
}
throw new IllegalStateException("Failed to find a path from [" + obj.getClass() + "] to [" + type + "]");
}
@SuppressWarnings("unchecked")
@Override
protected <T> List<T> getListByRelationshipMap(Object obj, MapRelationship rel) {
Class<UpdatableRecord<?>> typeClass = JooqUtils.getRecordClass(schemaFactory, rel.getObjectType());
String mappingType = schemaFactory.getSchemaName(rel.getMappingType());
String fromType = schemaFactory.getSchemaName(rel.getObjectType());
TableField<?, Object> fieldFrom = JooqUtils.getTableField(getMetaDataManager(), fromType, ObjectMetaDataManager.ID_FIELD);
TableField<?, Object> mappingTo = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getOtherRelationship().getPropertyName());
TableField<?, Object> mappingOther = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getPropertyName());
TableField<?, Object> mappingRemoved = JooqUtils.getTableField(getMetaDataManager(), mappingType, ObjectMetaDataManager.REMOVED_FIELD);
Table<?> table = JooqUtils.getTable(schemaFactory, typeClass);
Table<?> mapTable = JooqUtils.getTable(schemaFactory, rel.getMappingType());
SelectQuery<?> query = create().selectQuery();
query.addFrom(table);
query.addSelect(table.fields());
query.addJoin(mapTable, fieldFrom.eq(mappingTo).and(mappingRemoved == null ? DSL.trueCondition() : mappingRemoved.isNull()).and(
mappingOther.eq(ObjectUtils.getId(obj))));
return (List<T>) query.fetchInto(typeClass);
}
@Override
public Map<String, Object> convertToPropertiesFor(Object obj, Map<Object, Object> values) {
Map<String, Object> result = new LinkedHashMap<String, Object>();
Class<?> recordClass = null;
if (obj instanceof Class<?>) {
recordClass = JooqUtils.getRecordClass(schemaFactory, (Class<?>) obj);
} else {
UpdatableRecord<?> record = JooqUtils.getRecordObject(obj);
recordClass = JooqUtils.getRecordClass(schemaFactory, record.getClass());
}
for (Map.Entry<Object, Object> entry : values.entrySet()) {
String name = metaDataManager.convertToPropertyNameString(recordClass, entry.getKey());
if (name != null) {
result.put(name, entry.getValue());
}
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public <T> T findOne(Class<T> clz, Map<Object, Object> values) {
return (T) toQuery(clz, values).fetchOne();
}
@Override
public <T> T findOne(Class<T> clz, Object key, Object... valueKeyValue) {
Map<Object, Object> map = CollectionUtils.asMap(key, valueKeyValue);
return findOne(clz, map);
}
@SuppressWarnings("unchecked")
@Override
public <T> T findAny(Class<T> clz, Map<Object, Object> values) {
return (T) toQuery(clz, values).fetchAny();
}
@Override
public <T> T findAny(Class<T> clz, Object key, Object... valueKeyValue) {
Map<Object, Object> map = CollectionUtils.asMap(key, valueKeyValue);
return findAny(clz, map);
}
@SuppressWarnings("unchecked")
@Override
public <T> List<T> find(Class<T> clz, Map<Object, Object> values) {
return (List<T>) toQuery(clz, values).fetch();
}
@Override
public <T> List<T> find(Class<T> clz, Object key, Object... valueKeyValue) {
Map<Object, Object> map = CollectionUtils.asMap(key, valueKeyValue);
return find(clz, map);
}
protected ResultQuery<?> toQuery(Class<?> clz, Map<Object, Object> values) {
String type = schemaFactory.getSchemaName(clz);
if (type == null) {
throw new IllegalArgumentException("Failed to find type of class [" + clz + "]");
}
Class<UpdatableRecord<?>> recordClass = JooqUtils.getRecordClass(schemaFactory, clz);
Table<?> table = JooqUtils.getTableFromRecordClass(recordClass);
return create().selectFrom(table).where(JooqUtils.toConditions(metaDataManager, type, values));
}
protected DSLContext create() {
return new DefaultDSLContext(configuration);
}
protected void setField(Schema schema, Object obj, String name, Object value) {
try {
if (PropertyUtils.getPropertyDescriptor(obj, name) == null) {
if (schema != null && schema.getResourceFields().containsKey(name)) {
DataAccessor.fields(obj).withKey(name).set(value);
}
} else {
/* BeanUtils doesn't always like setting null values */
if (value == null) {
PropertyUtils.setProperty(obj, name, value);
} else {
BeanUtils.setProperty(obj, name, value);
}
}
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Failed to set [" + name + "] to value [" + value + "] on [" + obj + "]");
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Failed to set [" + name + "] to value [" + value + "] on [" + obj + "]");
} catch (NoSuchMethodException e) {
// Ignore if it doesn't exists
}
}
@Override
public void delete(Object obj) {
if (obj == null) {
return;
}
Object id = ObjectUtils.getId(obj);
String type = getType(obj);
Table<?> table = JooqUtils.getTableFromRecordClass(JooqUtils.getRecordClass(getSchemaFactory(), obj.getClass()));
TableField<?, Object> idField = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.ID_FIELD);
int result = create().delete(table).where(idField.eq(id)).execute();
if (result != 1) {
throw new IllegalStateException("Failed to delete [" + type + "] id [" + id + "]");
}
}
@Override
public <T> T loadResource(String resourceType, String resourceId) {
return loadResource(resourceType, (Object) resourceId);
}
@Override
public <T> T loadResource(String resourceType, Long resourceId) {
return loadResource(resourceType, (Object) resourceId);
}
@SuppressWarnings("unchecked")
protected <T> T loadResource(String resourceType, Object resourceId) {
Class<?> clz = schemaFactory.getSchemaClass(resourceType);
return (T) loadResource(clz, resourceId);
}
@Override
public <T> T loadResource(Class<T> type, Long resourceId) {
return loadResource(type, (Object) resourceId);
}
@Override
public <T> T loadResource(Class<T> type, String resourceId) {
return loadResource(type, (Object) resourceId);
}
@SuppressWarnings("unchecked")
protected <T> T loadResource(Class<T> type, Object resourceId) {
if (resourceId == null || type == null) {
return null;
}
Class<UpdatableRecord<?>> clz = JooqUtils.getRecordClass(schemaFactory, type);
return (T) JooqUtils.findById(DSL.using(getConfiguration()), clz, resourceId);
}
public Configuration getConfiguration() {
return configuration;
}
@Inject
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
public Configuration getLockingConfiguration() {
return lockingConfiguration;
}
public void setLockingConfiguration(Configuration lockingConfiguration) {
this.lockingConfiguration = lockingConfiguration;
}
public TransactionDelegate getTransactionDelegate() {
return transactionDelegate;
}
@Inject
public void setTransactionDelegate(TransactionDelegate transactionDelegate) {
this.transactionDelegate = transactionDelegate;
}
}