From 2195d7ecbc763576a796fbd161e3056655ee660d Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 26 Nov 2014 22:45:33 +1300 Subject: [PATCH] #211 - ENH: Add support for File as a bean type ... for streaming into/from db --- .../ebeaninternal/server/type/DataBind.java | 11 +- .../ebeaninternal/server/type/DataReader.java | 2 + .../server/type/DefaultTypeManager.java | 20 +- .../server/type/ScalarTypeFile.java | 193 ++++++++++++++++++ .../avaje/tests/model/types/SomeFileBean.java | 51 +++++ .../com/avaje/tests/types/TestFileType.java | 111 ++++++++++ .../tests/types/TestFileTypeFetching.java | 65 ++++++ src/test/resources/java-64.png | Bin 0 -> 4871 bytes src/test/resources/profile-image.jpg | Bin 0 -> 1620 bytes 9 files changed, 446 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java create mode 100644 src/test/java/com/avaje/tests/model/types/SomeFileBean.java create mode 100644 src/test/java/com/avaje/tests/types/TestFileType.java create mode 100644 src/test/java/com/avaje/tests/types/TestFileTypeFetching.java create mode 100644 src/test/resources/java-64.png create mode 100644 src/test/resources/profile-image.jpg 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 0000000000000000000000000000000000000000..047153691eee504e330513efe4a48a1bdef1bbda GIT binary patch literal 4871 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/profile-image.jpg b/src/test/resources/profile-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e472a6bc2d25e0816d97b6fac7ed6983ebd6cf2 GIT binary patch literal 1620 zcmbW!c{tR090%~Zy$t*Ox|D_cFhmxuC2e@`zwMNLH&FBcNzbHxj9Mk3-#)}}-}MMqU$2_F#Rdf6v9 z9B+WvRfV&efC&HrgZGOQD5qdNFc=gHgL89p@gU(yBm#~=p!hHXC_XeF0wEwIfEL8! za5yCY0bwDmFb0dm?mq&8aPB~1yf7Fq7KK1z|8v;Q0GbDo1LPneNdSxnLC_#}8z8|! z!a!dG{236K(<2u*4=)_SIZz`2fI$!l7z)|%nsYjevkySgFw7w}BQ8N3S8mAwtok)- z9*>l9Wed)>Ygt;uEijfBE_6UxL{#Rmtem`prk3_m9bG+>6Q%^>Ni&k&sng^$_6}#= zJv=XXdHY-p3J#%!hJ{DO{d7J4#?6GplzXXZ=^6Jkv+@fb7d|N}E-8IlRb5m2tggP{ zRcjl)oze07O?OXkU;n^|!J)D7iOH$Y(=)Tbu~xpUuKm8gvAMPH0s)Y3EYA83_74}D z;{rpW5GeP)3j_}3Ob8kZJEX>iF|y%y4G@%6zs7?#rsh?)@JeaeF5}z+yWm37nqxAo zecD&qzk|j8FWFzPzg2R4mcP5Ulf?N9vGM+jrVSdcl{9MH0|<;j1M zx!iP3OWE=4wt{H`A!&s0W1lQslAu2ik^RMK%a;+cea37f+O1K2K)JgQET&CoO@UHY zMj^~ysxy(*Ltj6RYQ#nA_bF?)Xf51KYEU`ICG1qZ_8QUYp13)4+Af83j15p1NU7tr z$;hc9=N+T>SyHH5gnMc$)SMIDp7rO2S9q81ScW!=vDT9p*TSkReWixYlvI^`C6luV z)&)nZAWFKQLMt3LJePfTLXdj#&;1j^gST@pg;VB@1NjyK1J>5crQSw`9WFL--DP>P zvz?sP8<8U#HLR$wZk&G_qfK|m>%8gc$|YK?(?j0K@5uGNPtz%}D)};6SsZCz-O%*& zL-YCrl&8!VO`G_%xOY>V-blxG$9IOh`n7s-rctosp|?IPFZ$4B|1!Vc7rKE@RiXn? z3n?CxIg=Gu#s+dUqOICV#-K~y3{{fB5{jy<7xeL`b{>D8ZWz59I_ILh_dXhxeCK@4Nzy5vOh5Y_(U;jn3ZvD9~y0Zy8w&)b=^Te-JvwjCt|!m?W>gbK~i z=gOyusQ|Y`nIpToP?3qiB+T$cQ~%2#yWLz*)`_KZZ#ICHZR(<}x>Q?aAQxB8nZPsM zY!fS+v=wi>>yILJ@ICvUh%_aEx#XFbzd_Xw5l`43r1E+=yk-nB??V z0h;M9-gom2TRoDjftXa~4p3ctKo2u6=c6oHA+@+2lat+ZuG6r0Nz0BE*CyKA5L`P% zejKUYKNh>05+3n@=6EG2#)QAk#_4wFg5TKc-6J`AOgW-bA9`(6mdspitggZ5D&Sg@>9}wl= zxn#H}OE~+J)FKQzp|z~*vuWRUb4_~7#S+T6LAJr6A+-I2|o{PT? zFUupt4ziex?nK3!u}`J)c>AGIdQU`|{g;^5cOA*M!^k