diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java b/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java index 8897be957..acc20626e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DataBind.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.type; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.math.BigDecimal; @@ -114,7 +115,15 @@ public class DataBind { public void setChar(char v) throws SQLException { pstmt.setString(++pos, String.valueOf(v)); } - + + public void setBlob(InputStream inputStream, long length) throws SQLException { + pstmt.setBlob(++pos, inputStream, length); + } + + public void setBlob(InputStream inputStream) throws SQLException { + pstmt.setBlob(++pos, inputStream); + } + public void setBlob(byte[] bytes) throws SQLException { ByteArrayInputStream is = new ByteArrayInputStream(bytes); pstmt.setBinaryStream(++pos, is, bytes.length); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java b/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java index 65fa162fb..2b92e13cc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.type; +import java.io.InputStream; import java.math.BigDecimal; import java.sql.Array; import java.sql.SQLException; @@ -52,4 +53,5 @@ public interface DataReader { public Object getObject() throws SQLException; + public InputStream getBinaryStream() throws SQLException; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java index 8ef3780b5..edaca8754 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.type; +import java.io.File; import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.BigInteger; @@ -66,6 +67,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final DefaultTypeFactory extraTypeFactory; + private final ScalarTypeFile fileType = new ScalarTypeFile(); + private final ScalarType charType = new ScalarTypeChar(); private final ScalarType charArrayType = new ScalarTypeCharArray(); @@ -127,8 +130,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { */ public DefaultTypeManager(ServerConfig config, BootupClasses bootupClasses) { - int clobType = config == null ? Types.CLOB : config.getDatabasePlatform().getClobDbType(); - int blobType = config == null ? Types.BLOB : config.getDatabasePlatform().getBlobDbType(); + int clobType = config.getDatabasePlatform().getClobDbType(); + int blobType = config.getDatabasePlatform().getBlobDbType(); this.jsonDateTime = config.getJsonDateTime(); this.checkImmutable = new CheckImmutable(this); @@ -296,7 +299,12 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { @SuppressWarnings("unchecked") public ScalarType getScalarType(Class type, int jdbcType) { - // check for Clob, LongVarchar etc first... + // File is a special Lob so check for that first + if (File.class.equals(type)) { + return (ScalarType) fileType; + } + + // check for Clob, LongVarchar etc ... // the reason being that String maps to multiple jdbc types // varchar, clob, longVarchar. ScalarType scalarType = getLobTypes(jdbcType); @@ -321,8 +329,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return (ScalarType) extraTypeFactory.createCalendar(jsonDateTime, jdbcType); } - String msg = "Unmatched ScalarType for " + type + " jdbcType:" + jdbcType; - throw new RuntimeException(msg); + throw new RuntimeException("Unmatched ScalarType for " + type + " jdbcType:" + jdbcType); } /** @@ -650,7 +657,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { * plus some other common types such as java.util.Date and java.util.Calendar. */ protected void initialiseStandard(JsonConfig.DateTime mode, int platformClobType, int platformBlobType, boolean binaryUUID) { - + ScalarType utilDateType = extraTypeFactory.createUtilDate(mode); typeMap.put(java.util.Date.class, utilDateType); @@ -675,6 +682,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { ScalarType uuidType = (binaryUUID) ? new ScalarTypeUUIDBinary() : new ScalarTypeUUIDVarchar(); typeMap.put(UUID.class, uuidType); + typeMap.put(File.class, fileType); typeMap.put(InetAddress.class, inetAddressType); typeMap.put(Locale.class, localeType); typeMap.put(Currency.class, currencyType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java new file mode 100644 index 000000000..074f4ece1 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java @@ -0,0 +1,193 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.text.TextException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.sql.SQLException; +import java.sql.Types; + +/** + * ScalarType for streaming between a File and the database. + */ +public class ScalarTypeFile extends ScalarTypeBase { + + private static Logger logger = LoggerFactory.getLogger(ScalarTypeFile.class); + + private final String prefix; + + private final String suffix; + + private final File directory; + + private final int bufferSize; + + /** + * Construct with reasonable defaults of Blob and 8096 buffer size. + */ + public ScalarTypeFile() { + this(Types.BLOB, "db-", null, null, 8096); + } + + /** + * Create the ScalarTypeFile. + */ + public ScalarTypeFile(int jdbcType, String prefix, String suffix, File directory, int bufferSize) { + super(File.class, false, jdbcType); + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + this.bufferSize = bufferSize; + } + + private InputStream getInputStream(File value) throws IOException { + FileInputStream fi = new FileInputStream(value); + return new BufferedInputStream(fi, bufferSize); + } + + private OutputStream getOutputStream(File value) throws IOException { + FileOutputStream fi = new FileOutputStream(value); + return new BufferedOutputStream(fi, bufferSize); + } + + @Override + public File read(DataReader dataReader) throws SQLException { + + InputStream is = dataReader.getBinaryStream(); + if (is == null) { + return null; + } + + try { + // stream from db into our temp file + File tempFile = File.createTempFile(prefix, suffix, directory); + OutputStream os = getOutputStream(tempFile); + pump(is, os); + + return tempFile; + + } catch (IOException e) { + throw new SQLException("Error reading db file inputStream", e); + } + } + + + @Override + public void bind(DataBind b, File value) throws SQLException { + if (value == null) { + b.setNull(jdbcType); + } else { + try { + // stream from our file to the db + InputStream fi = getInputStream(value); + b.setBlob(fi, value.length()); + } catch (IOException e) { + throw new SQLException("Error trying to set file inputStream", e); + } + } + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public File toBeanType(Object value) { + return (File) value; + } + + @Override + public void jsonWrite(JsonGenerator ctx, String name, File value) throws IOException { + ctx.writeFieldName(name); + InputStream is = getInputStream(value); + ctx.writeBinary(is, (int) value.length()); + } + + @Override + public File jsonRead(JsonParser ctx, JsonToken event) throws IOException { + File tempFile = File.createTempFile(prefix, suffix, directory); + OutputStream os = getOutputStream(tempFile); + ctx.readBinaryValue(os); + os.flush(); + os.close(); + return tempFile; + } + + @Override + public String formatValue(File file) { + throw new TextException("Not supported"); + } + + @Override + public File parse(String value) { + throw new TextException("Not supported"); + } + + @Override + public File convertFromMillis(long systemTimeMillis) { + throw new TextException("Not supported"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public File readData(DataInput dataInput) throws IOException { + // skip reading large file + return null; + } + + public void writeData(DataOutput dataOutput, File file) throws IOException { + // skip writing large file + } + + /** + * Helper method to pump bytes from input to output. + */ + public long pump(InputStream is, OutputStream out) throws IOException { + + long totalBytes = 0; + InputStream input = null; + OutputStream output = null; + + try { + input = new BufferedInputStream(is, bufferSize); + output = new BufferedOutputStream(out, bufferSize); + + byte[] buffer = new byte[bufferSize]; + int length; + while (((length = input.read(buffer)) > 0)) { + output.write(buffer, 0, length); + totalBytes += length; + } + + output.flush(); + + return totalBytes; + + } finally { + if (output != null) { + try { + output.close(); + } catch (IOException e) { + logger.error("Error when closing outputstream", e); + } + } + if (input != null) { + try { + input.close(); + } catch (IOException e) { + logger.error("Error when closing inputstream ", e); + } + } + } + } + +} diff --git a/src/test/java/com/avaje/tests/model/types/SomeFileBean.java b/src/test/java/com/avaje/tests/model/types/SomeFileBean.java new file mode 100644 index 000000000..eec3115d8 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/types/SomeFileBean.java @@ -0,0 +1,51 @@ +package com.avaje.tests.model.types; + +import javax.persistence.*; +import java.io.File; + +@Entity +public class SomeFileBean { + + @Id + Long id; + + @Version + Long version; + + String name; + + @Lob + File file; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } +} diff --git a/src/test/java/com/avaje/tests/types/TestFileType.java b/src/test/java/com/avaje/tests/types/TestFileType.java new file mode 100644 index 000000000..f054fbef9 --- /dev/null +++ b/src/test/java/com/avaje/tests/types/TestFileType.java @@ -0,0 +1,111 @@ +package com.avaje.tests.types; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.types.SomeFileBean; +import org.junit.Test; + +import java.io.File; +import java.net.URL; + +import static org.junit.Assert.*; + +public class TestFileType extends BaseTestCase { + + File file = getFile("/profile-image.jpg"); + File file2 = getFile("/java-64.png"); + + @Test + public void test_insertNullFile() { + + assertTrue(file.exists()); + assertTrue(file2.exists()); + + SomeFileBean bean0 = new SomeFileBean(); + bean0.setName("afile"); + Ebean.save(bean0); + + SomeFileBean bean1 = Ebean.find(SomeFileBean.class) + .select("name, file") + .setId(bean0.getId()) + .findUnique(); + + assertEquals("afile", bean1.getName()); + assertNull(bean1.getFile()); + + bean1.setFile(file); + Ebean.save(bean1); + + SomeFileBean bean2 = Ebean.find(SomeFileBean.class) + .select("name, file") + .setId(bean0.getId()) + .findUnique(); + + assertEquals("afile", bean2.getName()); + assertNotNull(bean2.getFile()); + System.out.println("test_insertNullFile: bean2: "+bean2.getFile()); + + Ebean.delete(bean1); + } + + @Test + public void test_insertUpdateDelete() { + + assertTrue(file.exists()); + assertTrue(file2.exists()); + + SomeFileBean bean0 = new SomeFileBean(); + bean0.setName("afile"); + bean0.setFile(file); + + Ebean.save(bean0); + + SomeFileBean bean1 = Ebean.find(SomeFileBean.class) + .select("name, file") + .setId(bean0.getId()) + .findUnique(); + + assertEquals("afile", bean1.getName()); + assertNotNull(bean1.getFile()); + assertEquals(file.length(), bean1.getFile().length()); + System.out.println("t2 bean1: " + bean1.getFile().getAbsoluteFile()); + + bean1.setName("mod-file"); + bean1.setFile(file2); + // update to file2 + Ebean.save(bean1); + + + SomeFileBean bean2 = Ebean.find(SomeFileBean.class) + .select("name, file") + .setId(bean0.getId()) + .findUnique(); + + assertEquals(file2.length(), bean2.getFile().length()); + System.out.println("t2 bean3: " + bean2.getFile().getAbsoluteFile()); + + // update to null + bean2.setFile(null); + bean2.setName("setNull"); + Ebean.save(bean2); + + SomeFileBean bean3 = Ebean.find(SomeFileBean.class) + .select("name, file") + .setId(bean0.getId()) + .findUnique(); + + assertNull(bean3.getFile()); + System.out.println("t2 bean3: " + bean3.getFile()); + + bean3.setName("changeOnlyName"); + Ebean.save(bean3); + + Ebean.delete(bean3); + } + + private File getFile(String resource) { + URL url = getClass().getResource(resource); + return new File(url.getFile()); + } + +} diff --git a/src/test/java/com/avaje/tests/types/TestFileTypeFetching.java b/src/test/java/com/avaje/tests/types/TestFileTypeFetching.java new file mode 100644 index 000000000..172a646e6 --- /dev/null +++ b/src/test/java/com/avaje/tests/types/TestFileTypeFetching.java @@ -0,0 +1,65 @@ +package com.avaje.tests.types; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.BeanState; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.types.SomeFileBean; +import org.junit.Test; + +import java.io.File; +import java.net.URL; +import java.util.Set; + +import static org.junit.Assert.*; + +public class TestFileTypeFetching extends BaseTestCase { + + File file = getFile("/profile-image.jpg"); + File file2 = getFile("/java-64.png"); + + @Test + public void test_lazyFetch_statelessUpdate() { + + assertTrue(file.exists()); + + SomeFileBean bean0 = new SomeFileBean(); + bean0.setName("one"); + bean0.setFile(file); + Ebean.save(bean0); + + SomeFileBean bean1 = Ebean.find(SomeFileBean.class) + .setId(bean0.getId()) + .findUnique(); + + BeanState beanState = Ebean.getBeanState(bean1); + Set loadedProps = beanState.getLoadedProps(); + assertTrue(loadedProps.contains("name")); + assertFalse(loadedProps.contains("file")); + + File file1 = bean1.getFile(); + assertEquals(file.length(), file1.length()); + + + SomeFileBean statelessUpdateBean = new SomeFileBean(); + statelessUpdateBean.setId(bean0.getId()); + statelessUpdateBean.setFile(file2); + + // perform stateless update (handy) + Ebean.update(statelessUpdateBean); + + SomeFileBean bean2 = Ebean.find(SomeFileBean.class) + .select("file") + .setId(bean0.getId()) + .findUnique(); + + assertEquals(file2.length(), bean2.getFile().length()); + + Ebean.delete(bean1); + } + + private File getFile(String resource) { + URL url = getClass().getResource(resource); + return new File(url.getFile()); + } + +} diff --git a/src/test/resources/java-64.png b/src/test/resources/java-64.png new file mode 100644 index 000000000..047153691 Binary files /dev/null and b/src/test/resources/java-64.png differ diff --git a/src/test/resources/profile-image.jpg b/src/test/resources/profile-image.jpg new file mode 100644 index 000000000..3e472a6bc Binary files /dev/null and b/src/test/resources/profile-image.jpg differ