package org.molgenis.data.postgresql;
import org.molgenis.data.MolgenisDataException;
import org.molgenis.data.transaction.TransactionExceptionTranslator;
import org.molgenis.data.validation.ConstraintViolation;
import org.molgenis.data.validation.MolgenisValidationException;
import org.postgresql.util.PSQLException;
import org.postgresql.util.ServerErrorMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionException;
import javax.sql.DataSource;
import java.sql.BatchUpdateException;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.Collections.singleton;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static org.molgenis.data.meta.AttributeType.*;
/**
* Translates PostgreSQL exceptions to MOLGENIS data exceptions
*/
@Component
class PostgreSqlExceptionTranslator extends SQLErrorCodeSQLExceptionTranslator implements TransactionExceptionTranslator
{
@Autowired
PostgreSqlExceptionTranslator(DataSource dataSource)
{
super(requireNonNull(dataSource));
}
@Override
protected DataAccessException doTranslate(String task, String sql, SQLException ex)
{
DataAccessException dataAccessException = super.doTranslate(task, sql, ex);
if (dataAccessException == null)
{
return doTranslate(ex);
}
return doTranslate(dataAccessException);
}
private static MolgenisDataException doTranslate(DataAccessException dataAccessException)
{
Throwable cause = dataAccessException.getCause();
if (!(cause instanceof PSQLException))
{
throw new RuntimeException(format("Unexpected exception class [%s]", cause.getClass().getSimpleName()));
}
PSQLException pSqlException = (PSQLException) cause;
MolgenisDataException molgenisDataException = doTranslate(pSqlException);
if (molgenisDataException == null)
{
molgenisDataException = new MolgenisDataException(dataAccessException);
}
return molgenisDataException;
}
private static MolgenisDataException doTranslate(SQLException sqlException)
{
if (sqlException instanceof BatchUpdateException)
{
sqlException = sqlException.getNextException();
}
if (!(sqlException instanceof PSQLException))
{
throw new RuntimeException(
format("Unexpected exception class [%s]", sqlException.getClass().getSimpleName()));
}
PSQLException pSqlException = (PSQLException) sqlException;
MolgenisDataException molgenisDataException = doTranslate(pSqlException);
if (molgenisDataException == null)
{
molgenisDataException = new MolgenisDataException(sqlException);
}
return molgenisDataException;
}
private static MolgenisDataException doTranslate(PSQLException pSqlException)
{
switch (pSqlException.getSQLState())
{
case "22007": // invalid_datetime_format
case "22P02": // not an integer exception
return translateInvalidIntegerException(pSqlException);
case "23502": // not_null_violation
return translateNotNullViolation(pSqlException);
case "23503": // foreign_key_violation
return translateForeignKeyViolation(pSqlException);
case "23505": // unique_violation
return translateUniqueKeyViolation(pSqlException);
case "23514": // check_violation
return translateCheckConstraintViolation(pSqlException);
case "2BP01":
return translateDependentObjectsStillExist(pSqlException);
case "42703":
return translateUndefinedColumnException(pSqlException);
case PostgreSqlQueryGenerator.ERR_CODE_READONLY_VIOLATION:
return translateReadonlyViolation(pSqlException);
default:
return null;
}
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateReadonlyViolation(PSQLException pSqlException)
{
Matcher matcher = Pattern
.compile("Updating read-only column \"?(.*?)\"? of table \"?(.*?)\"? with id \\[(.*?)] is not allowed")
.matcher(pSqlException.getServerErrorMessage().getMessage());
boolean matches = matcher.matches();
if (!matches)
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String colName = matcher.group(1);
String tableName = matcher.group(2);
String id = matcher.group(3);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("Updating read-only attribute '%s' of entity '%s' with id '%s' is not allowed.", colName,
tableName, id));
return new MolgenisValidationException(singleton(constraintViolation));
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateDependentObjectsStillExist(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String detail = serverErrorMessage.getDetail();
Matcher matcher = Pattern.compile("constraint (.+) on table \"?([^\"]+)\"? depends on table \"?([^\"]+)\"?\n?")
.matcher(detail);
String table = null;
Set<String> dependentTables = new LinkedHashSet<>();
while (matcher.find())
{
table = matcher.group(2);
dependentTables.add(matcher.group(3));
}
if (table == null) // no matches
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String message;
if (dependentTables.size() == 1)
{
message = format("Cannot delete entity '%s' because entity '%s' depends on it.", table,
dependentTables.iterator().next());
}
else
{
message = format("Cannot delete entity '%s' because entities '%s' depend on it.", table,
dependentTables.stream().collect(joining(", ")));
}
ConstraintViolation constraintViolation = new ConstraintViolation(message, null);
return new MolgenisValidationException(singleton(constraintViolation));
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateInvalidIntegerException(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String message = serverErrorMessage.getMessage();
Matcher matcher = Pattern.compile("invalid input syntax for \\b(?:type )?\\b(.+?): \"(.*?)\"").matcher(message);
boolean matches = matcher.matches();
if (!matches)
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String postgreSqlType = matcher.group(1);
// convert PostgreSQL data type to attribute type:
String type;
switch (postgreSqlType)
{
case "boolean":
type = BOOL.toString();
break;
case "date":
type = DATE.toString();
break;
case "timestamp":
type = DATE_TIME.toString();
break;
case "double precision":
type = DECIMAL.toString();
break;
case "integer":
type = INT.toString() + " or " + LONG.toString();
break;
default:
type = postgreSqlType;
break;
}
String value = matcher.group(2);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("Value [%s] of this entity attribute is not of type [%s].", value, type), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateNotNullViolation(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String tableName = serverErrorMessage.getTable();
String message = serverErrorMessage.getMessage();
Matcher matcher = Pattern.compile("null value in column \"?(.*?)\"? violates not-null constraint")
.matcher(message);
boolean matches = matcher.matches();
if (matches)
{
// exception message when adding data that does not match constraint
String columnName = matcher.group(1);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("The attribute '%s' of entity '%s' can not be null.", columnName, tableName), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
else
{
// exception message when applying constraint on existing data
matcher = Pattern.compile("column \"(.*?)\" contains null values").matcher(message);
matches = matcher.matches();
if (!matches)
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String columnName = matcher.group(1);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("The attribute '%s' of entity '%s' contains null values.", columnName, tableName), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateForeignKeyViolation(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String tableName = serverErrorMessage.getTable();
String detailMessage = serverErrorMessage.getDetail();
Matcher m = Pattern.compile("\\((.*?)\\)").matcher(detailMessage);
if (!m.find())
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String colName = m.group(1);
if (!m.find())
{
throw new RuntimeException("Error translating exception", pSqlException);
}
String value = m.group(1);
String constraintViolationMessageTemplate;
if (detailMessage.contains("still referenced from"))
{
// ERROR: update or delete on table "x" violates foreign key constraint "y" on table "z"
// Detail: Key (k)=(v) is still referenced from table "x".
constraintViolationMessageTemplate = "Value '%s' for attribute '%s' is referenced by entity '%s'.";
}
else
{
// ERROR: insert or update on table "x" violates foreign key constraint "y"
// Detail: Key (k)=(v) is not present in table "z".
constraintViolationMessageTemplate = "Unknown xref value '%s' for attribute '%s' of entity '%s'.";
}
ConstraintViolation constraintViolation = new ConstraintViolation(
format(constraintViolationMessageTemplate, value, colName, tableName), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateUniqueKeyViolation(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String tableName = serverErrorMessage.getTable();
String detailMessage = serverErrorMessage.getDetail();
Matcher matcher = Pattern.compile("Key \\((.*?)\\)=\\((.*?)\\) already exists.").matcher(detailMessage);
boolean matches = matcher.matches();
if (matches)
{
// exception message when adding data that does not match constraint
String columnName = matcher.group(1);
String value = matcher.group(2);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("Duplicate value '%s' for unique attribute '%s' from entity '%s'.", value, columnName,
tableName), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
else
{
// exception message when applying constraint on existing data
matcher = Pattern.compile("Key \\((.*?)\\)=\\((.*?)\\) is duplicated.").matcher(detailMessage);
matches = matcher.matches();
if (matches)
{
String columnName = matcher.group(1);
String value = matcher.group(2);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("The attribute '%s' of entity '%s' contains duplicate value '%s'.", columnName,
tableName, value), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
else
{
throw new RuntimeException("Error translating exception", pSqlException);
}
}
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateCheckConstraintViolation(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String tableName = serverErrorMessage.getTable();
String constraintName = serverErrorMessage.getConstraint();
// constraint name: <tableName>_<columnName>_chk
String columnName = constraintName.substring(tableName.length() + 1, constraintName.length() - 4);
ConstraintViolation constraintViolation = new ConstraintViolation(
format("Unknown enum value for attribute '%s' of entity '%s'.", columnName, tableName), null);
return new MolgenisValidationException(singleton(constraintViolation));
}
/**
* Package private for testability
*
* @param pSqlException PostgreSQL exception
* @return translated validation exception
*/
static MolgenisValidationException translateUndefinedColumnException(PSQLException pSqlException)
{
ServerErrorMessage serverErrorMessage = pSqlException.getServerErrorMessage();
String message = serverErrorMessage.getMessage();
ConstraintViolation constraintViolation = new ConstraintViolation(message);
return new MolgenisValidationException(singleton(constraintViolation));
}
@Override
public MolgenisDataException doTranslate(TransactionException transactionException)
{
Throwable cause = transactionException.getCause();
if (!(cause instanceof PSQLException))
{
return null;
}
PSQLException pSqlException = (PSQLException) cause;
return doTranslate(pSqlException);
}
}