package org.molgenis.data.postgresql;
import org.molgenis.data.Entity;
import org.molgenis.data.EntityManager;
import org.molgenis.data.Fetch;
import org.molgenis.data.meta.AttributeType;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
@Component
class PostgreSqlEntityFactory
{
@SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(PostgreSqlEntityFactory.class);
private final EntityManager entityManager;
@Autowired
public PostgreSqlEntityFactory(EntityManager entityManager)
{
this.entityManager = requireNonNull(entityManager);
}
RowMapper<Entity> createRowMapper(EntityType entityType, Fetch fetch)
{
return new EntityMapper(entityManager, entityType, fetch);
}
Iterable<Entity> getReferences(EntityType refEntityType, Iterable<?> ids)
{
return entityManager.getReferences(refEntityType, ids);
}
private static class EntityMapper implements RowMapper<Entity>
{
private final EntityManager entityManager;
private final EntityType entityType;
private final Fetch fetch;
private EntityMapper(EntityManager entityManager, EntityType entityType, Fetch fetch)
{
this.entityManager = requireNonNull(entityManager);
this.entityType = requireNonNull(entityType);
this.fetch = fetch; // can be null
}
@Override
public Entity mapRow(ResultSet resultSet, int i) throws SQLException
{
Entity e = entityManager.createFetch(entityType, fetch);
// TODO performance, iterate over fetch if available
for (Attribute attr : entityType.getAtomicAttributes())
{
if (fetch == null || fetch.hasField(attr.getName()))
{
if (attr.getExpression() != null)
{
continue;
}
e.set(attr.getName(), mapValue(resultSet, attr));
}
}
return e;
}
/**
* Maps a single results set value to an entity value.
* See the JDBC 4.0 specification appendix B titled "Data Type Conversion Tables" for conversion rules.
*
* @param resultSet result set
* @param attr attribute
* @return value for the given attribute in the type defined by the attribute type
* @throws SQLException if an error occurs reading from the result set
*/
private Object mapValue(ResultSet resultSet, Attribute attr) throws SQLException
{
return mapValue(resultSet, attr, attr.getName());
}
/**
* Maps a single results set value to an entity value.
* See the JDBC 4.0 specification appendix B titled "Data Type Conversion Tables" for conversion rules.
*
* @param resultSet result set
* @param attr attribute
* @param colName column name in the result set
* @return value for the given attribute in the type defined by the attribute type
* @throws SQLException if an error occurs reading from the result set
*/
private Object mapValue(ResultSet resultSet, Attribute attr, String colName) throws SQLException
{
Object value;
switch (attr.getDataType())
{
case BOOL:
boolean boolValue = resultSet.getBoolean(colName);
value = resultSet.wasNull() ? null : boolValue;
break;
case CATEGORICAL:
case FILE:
case XREF:
EntityType xrefEntityType = attr.getRefEntity();
Object refIdValue = mapValue(resultSet, xrefEntityType.getIdAttribute(), colName);
value = refIdValue != null ? entityManager.getReference(xrefEntityType, refIdValue) : null;
break;
case CATEGORICAL_MREF:
case MREF:
EntityType mrefEntityMeta = attr.getRefEntity();
Array mrefArrayValue = resultSet.getArray(colName);
value = resultSet.wasNull() ? null : mapValueMref(mrefArrayValue, mrefEntityMeta);
break;
case ONE_TO_MANY:
Array oneToManyArrayValue = resultSet.getArray(colName);
value = resultSet.wasNull() ? null : mapValueOneToMany(oneToManyArrayValue, attr);
break;
case COMPOUND:
throw new RuntimeException(
format("Value mapping not allowed for attribute type [%s]", attr.getDataType().toString()));
case DATE:
LocalDate localDate = resultSet.getObject(colName, LocalDate.class);
value = localDate != null ? Date.from(localDate.atStartOfDay(ZoneId.of("UTC")).toInstant()) : null;
break;
case DATE_TIME:
// valid, because java.sql.Timestamp extends required type java.util.Date
value = resultSet.getTimestamp(colName);
break;
case DECIMAL:
BigDecimal bigDecimalValue = resultSet.getBigDecimal(colName);
value = bigDecimalValue != null ? bigDecimalValue.doubleValue() : null;
break;
case EMAIL:
case ENUM:
case HTML:
case HYPERLINK:
case SCRIPT:
case STRING:
case TEXT:
value = resultSet.getString(colName);
break;
case INT:
int intValue = resultSet.getInt(colName);
value = resultSet.wasNull() ? null : intValue;
break;
case LONG:
long longValue = resultSet.getLong(colName);
value = resultSet.wasNull() ? null : longValue;
break;
default:
throw new RuntimeException(format("Unknown attribute type [%s]", attr.getDataType().toString()));
}
return value;
}
/**
* Maps a single results set array value to an entity value for one-to-many attributes.
*
* @param arrayValue result set array value
* @param attr attribute meta data
* @return mapped value
* @throws SQLException if an error occurs while attempting to access the array
*/
private Object mapValueOneToMany(Array arrayValue, Attribute attr) throws SQLException
{
EntityType entityType = attr.getRefEntity();
Object value;
String[] postgreSqlMrefIds = (String[]) arrayValue.getArray();
if (postgreSqlMrefIds.length > 0 && postgreSqlMrefIds[0] != null)
{
Attribute idAttr = entityType.getIdAttribute();
Object[] mrefIds = new Object[postgreSqlMrefIds.length];
for (int i = 0; i < postgreSqlMrefIds.length; ++i)
{
String mrefIdStr = postgreSqlMrefIds[i];
Object mrefId = mrefIdStr != null ? convertMrefIdValue(mrefIdStr, idAttr) : null;
mrefIds[i] = mrefId;
}
// convert ids to (lazy) entities
value = entityManager.getReferences(entityType, asList(mrefIds));
}
else
{
value = null;
}
return value;
}
/**
* Maps a single results set array value to an entity value for mref attributes.
*
* @param arrayValue result set array value
* @param entityType entity meta data
* @return mapped value
* @throws SQLException if an error occurs while attempting to access the array
*/
private Object mapValueMref(Array arrayValue, EntityType entityType) throws SQLException
{
// ResultSet contains a two dimensional array for MREF attribute values:
// [[<order_nr_as_string>,<mref_id_as_string>],[<order_nr_as_string>,<mref_id_as_string>], ...]
// In case there are no MREF attribute values the ResulSet is:
// [[null,null]]
Object value;
String[][] mrefIdsAndOrder = (String[][]) arrayValue.getArray();
if (mrefIdsAndOrder.length > 0 && mrefIdsAndOrder[0][0] != null)
{
Arrays.sort(mrefIdsAndOrder, comparing(o -> Integer.valueOf(o[0])));
Attribute idAttr = entityType.getIdAttribute();
Object[] mrefIds = new Object[mrefIdsAndOrder.length];
for (int i = 0; i < mrefIdsAndOrder.length; ++i)
{
String[] mrefIdAndOrder = mrefIdsAndOrder[i];
String mrefIdStr = mrefIdAndOrder[1];
Object mrefId = mrefIdStr != null ? convertMrefIdValue(mrefIdStr, idAttr) : null;
mrefIds[i] = mrefId;
}
// convert ids to (lazy) entities
value = entityManager.getReferences(entityType, asList(mrefIds));
}
else
{
value = null;
}
return value;
}
/**
* Converts a mref id value string to an entity value.
*
* @param idValueStr id value string
* @param idAttr id attribute
* @return entity value
*/
private static Object convertMrefIdValue(String idValueStr, Attribute idAttr)
{
// use iteration instead of tail recursion
while (true)
{
AttributeType attrType = idAttr.getDataType();
switch (attrType)
{
case BOOL:
return Boolean.valueOf(idValueStr);
case CATEGORICAL:
case FILE:
case XREF:
idAttr = idAttr.getRefEntity().getIdAttribute();
continue;
case DATE:
case DATE_TIME:
return Date.valueOf(idValueStr);
case DECIMAL:
return Double.valueOf(idValueStr);
case EMAIL:
case ENUM:
case HTML:
case HYPERLINK:
case SCRIPT:
case STRING:
case TEXT:
return idValueStr;
case INT:
return Integer.valueOf(idValueStr);
case LONG:
return Long.valueOf(idValueStr);
case CATEGORICAL_MREF:
case COMPOUND:
case MREF:
case ONE_TO_MANY:
throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString()));
default:
throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString()));
}
}
}
}
}