Skip to content

Commit

Permalink
Merge branch 'string-length-validation' of github.com:FOCONIS/ebean i…
Browse files Browse the repository at this point in the history
…nto FOCONIS-string-length-validation
  • Loading branch information
rbygrave committed Jun 23, 2023
2 parents 9a4b9d4 + d0020ab commit 4683877
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 9 deletions.
9 changes: 8 additions & 1 deletion ebean-api/src/main/java/io/ebean/DataIntegrityException.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import javax.persistence.PersistenceException;

/**
* Thrown when a foreign key constraint is enforced.
* Thrown when a foreign key constraint is enforced or a field is too large.
*/
public class DataIntegrityException extends PersistenceException {
private static final long serialVersionUID = -6740171949170180970L;
Expand All @@ -14,4 +14,11 @@ public class DataIntegrityException extends PersistenceException {
public DataIntegrityException(String message, Throwable cause) {
super(message, cause);
}

/**
* Create with message only.
*/
public DataIntegrityException(String message) {
super(message);
}
}
16 changes: 16 additions & 0 deletions ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ public class DatabaseConfig {

private String dumpMetricsOptions;

private LengthCheck lengthCheck = LengthCheck.OFF;
private Function<String, String> metricNaming = MetricNamingMatch.INSTANCE;

/**
Expand Down Expand Up @@ -2919,6 +2920,7 @@ protected void loadSettings(PropertiesWrapper p) {
jdbcFetchSizeFindEach = p.getInt("jdbcFetchSizeFindEach", jdbcFetchSizeFindEach);
jdbcFetchSizeFindList = p.getInt("jdbcFetchSizeFindList", jdbcFetchSizeFindList);
databasePlatformName = p.get("databasePlatformName", databasePlatformName);
lengthCheck = p.getEnum(LengthCheck.class, "lengthCheck", lengthCheck);

uuidVersion = p.getEnum(UuidVersion.class, "uuidVersion", uuidVersion);
uuidStateFile = p.get("uuidStateFile", uuidStateFile);
Expand Down Expand Up @@ -3419,6 +3421,20 @@ public void setMetricNaming(Function<String, String> metricNaming) {
this.metricNaming = metricNaming;
}

/**
* Returns the length check mode.
*/
public LengthCheck getLengthCheck() {
return lengthCheck;
}

/**
* Sets the length check mode.
*/
public void setLengthCheck(LengthCheck lengthCheck) {
this.lengthCheck = lengthCheck;
}

public enum UuidVersion {
VERSION4,
VERSION1,
Expand Down
22 changes: 22 additions & 0 deletions ebean-api/src/main/java/io/ebean/config/LengthCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.ebean.config;

/**
* Defines the length-check mode.
*
* @author Roland Praml, FOCONIS AG
*/
public enum LengthCheck {
/**
* By default, length checking is off. This means, strings/jsons and files are passed to the DB and the DB might or might not check the length.
* The DB has to check the data length. Note this is not possible for certain datatypes (e.g. clob without size)
*/
OFF,
/**
* When enabling length check, ebean validates strings/json strings and files before saving them to DB.
*/
ON,
/**
* Same as "ON", but take the UTF8-bytelength for validation. This may be useful, if you have an UTF8 based charset (default for DB2)
*/
UTF8
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,9 @@ public interface DataBinder {
*/
String popJson();

/**
* Returns the last bound object (e.g. for BindValidation). Note for InputStreams you'll get an InputStreamInfo.
*/
Object popLastObject();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.ebean.core.type;

import java.io.InputStream;

/**
* Helper to transports length info of DataBind.setBinaryStream(stream, length) to BindValidation
*
* @author Roland Praml, FOCONIS AG
*/
public class InputStreamInfo {
private final InputStream stream;

private final long length;

public InputStreamInfo(InputStream stream, long length) {
this.stream = stream;
this.length = length;
}

public InputStream stream() {
return stream;
}

public long length() {
return length;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.ebeaninternal.server.bind;

import io.ebean.core.type.DataBinder;
import io.ebean.core.type.InputStreamInfo;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.server.core.timezone.DataTimeZone;

import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
Expand All @@ -23,6 +27,8 @@ public class DataBind implements DataBinder {
protected int pos;
private String json;

private Object lastObject = null;

public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt, Connection connection) {
this.dataTimeZone = dataTimeZone;
this.pstmt = pstmt;
Expand Down Expand Up @@ -65,16 +71,19 @@ public final int currentPos() {
@Override
public void setObject(Object value) throws SQLException {
pstmt.setObject(++pos, value);
lastObject = value;
}

@Override
public final void setObject(Object value, int sqlType) throws SQLException {
pstmt.setObject(++pos, value, sqlType);
lastObject = value;
}

@Override
public void setNull(int jdbcType) throws SQLException {
pstmt.setNull(++pos, jdbcType);
lastObject = null;
}

@Override
Expand All @@ -96,7 +105,7 @@ public final int executeUpdate() throws SQLException {
}
}

private void closeInputStreams() {
public void closeInputStreams() {
if (inputStreams != null) {
for (InputStream inputStream : inputStreams) {
try {
Expand All @@ -117,36 +126,43 @@ public final PreparedStatement getPstmt() {
@Override
public void setString(String value) throws SQLException {
pstmt.setString(++pos, value);
lastObject = value;
}

@Override
public final void setInt(int value) throws SQLException {
pstmt.setInt(++pos, value);
lastObject = value;
}

@Override
public final void setLong(long value) throws SQLException {
pstmt.setLong(++pos, value);
lastObject = value;
}

@Override
public final void setShort(short value) throws SQLException {
pstmt.setShort(++pos, value);
lastObject = value;
}

@Override
public final void setFloat(float value) throws SQLException {
pstmt.setFloat(++pos, value);
lastObject = value;
}

@Override
public final void setDouble(double value) throws SQLException {
pstmt.setDouble(++pos, value);
lastObject = value;
}

@Override
public final void setBigDecimal(BigDecimal value) throws SQLException {
pstmt.setBigDecimal(++pos, value);
lastObject = value;
}

@Override
Expand All @@ -157,6 +173,7 @@ public final void setDate(java.sql.Date value) throws SQLException {
} else {
pstmt.setDate(++pos, value);
}
lastObject = value;
}

@Override
Expand All @@ -167,6 +184,7 @@ public final void setTimestamp(Timestamp value) throws SQLException {
} else {
pstmt.setTimestamp(++pos, value);
}
lastObject = value;
}

@Override
Expand All @@ -177,26 +195,31 @@ public final void setTime(Time value) throws SQLException {
} else {
pstmt.setTime(++pos, value);
}
lastObject = value;
}

@Override
public void setBoolean(boolean value) throws SQLException {
pstmt.setBoolean(++pos, value);
lastObject = value;
}

@Override
public void setBytes(byte[] value) throws SQLException {
pstmt.setBytes(++pos, value);
lastObject = value;
}

@Override
public void setByte(byte value) throws SQLException {
pstmt.setByte(++pos, value);
lastObject = value;
}

@Override
public void setChar(char value) throws SQLException {
pstmt.setString(++pos, String.valueOf(value));
lastObject = value;
}

@Override
Expand All @@ -211,21 +234,31 @@ public void setBinaryStream(InputStream inputStream, long length) throws SQLExce
}
inputStreams.add(inputStream);
pstmt.setBinaryStream(++pos, inputStream, length);
lastObject = new InputStreamInfo(inputStream, length);
}

@Override
public void setBlob(byte[] bytes) throws SQLException {
pstmt.setBinaryStream(++pos, new ByteArrayInputStream(bytes), bytes.length);
lastObject = bytes;
}

@Override
public void setClob(String content) throws SQLException {
pstmt.setCharacterStream(++pos, new StringReader(content), content.length());
lastObject = content;
}

@Override
public void setArray(String arrayType, Object[] elements) throws SQLException {
pstmt.setArray(++pos, connection.createArrayOf(arrayType, elements));
lastObject = elements;
}

@Override
public Object popLastObject() {
Object ret = lastObject;
lastObject = null;
return ret;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package io.ebeaninternal.server.deploy;

import com.fasterxml.jackson.core.JsonToken;
import io.ebean.DataIntegrityException;
import io.ebean.ValuePair;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.MutableValueInfo;
import io.ebean.bean.PersistenceContext;
import io.ebean.config.EncryptKey;
import io.ebean.config.LengthCheck;
import io.ebean.config.dbplatform.DbEncryptFunction;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.config.dbplatform.ExtraDbTypes;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.InputStreamInfo;
import io.ebean.core.type.ScalarType;
import io.ebean.plugin.Property;
import io.ebean.text.StringParser;
Expand Down Expand Up @@ -46,6 +50,7 @@
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
Expand Down Expand Up @@ -554,6 +559,22 @@ public Object readSet(DbReadContext ctx, EntityBean bean) throws SQLException {
@SuppressWarnings("unchecked")
public void bind(DataBind b, Object value) throws SQLException {
scalarType.bind(b, value);

if (needsLengthCheck()) {
LengthCheck lengthCheck = descriptor().config().getLengthCheck();
if (lengthCheck != LengthCheck.OFF) {
Object obj = b.popLastObject();
long l = getLength(obj, lengthCheck == LengthCheck.UTF8);
if (l > dbLength) {
b.closeInputStreams();
String s = String.valueOf(value); // take original bind value here.
if (s.length() > 100) {
s = s.substring(0, 97) + "...";
}
throw new DataIntegrityException("Cannot bind value '" + s + "' (effective length=" + l + ") to column '" + dbColumn + "' (length=" + dbLength + ")");
}
}
}
}

@SuppressWarnings(value = "unchecked")
Expand All @@ -565,6 +586,43 @@ public Object readData(DataInput dataInput) throws IOException {
return scalarType.readData(dataInput);
}

/**
* Returns, if this property needs a length check.
*/
boolean needsLengthCheck() {
if (dbLength == 0) {
return false;
}
switch (dbType) {
case Types.VARCHAR:
case Types.BLOB:
case ExtraDbTypes.JSON:
return true;
default:
return false;
}
}

/**
* Returns the length of <code>obj</code>. Note: for UTF8 strings -1 will be retuned, if the string length is lower than 1/4th of db length
*/
private long getLength(Object obj, boolean utf8) {
if (obj instanceof String) {
String s = (String) obj;
if (utf8) {
return s.length() * 4 <= dbLength ? -1 : s.getBytes(StandardCharsets.UTF_8).length;
} else {
return s.length();
}
} else if (obj instanceof byte[]) {
return ((byte[]) obj).length;
} else if (obj instanceof InputStreamInfo) {
return ((InputStreamInfo) obj).length();
} else {
return -1;
}
}

@Override
public BeanProperty beanProperty() {
return this;
Expand Down
Loading

0 comments on commit 4683877

Please sign in to comment.