You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/07/16 14:10:37 UTC

[isis] 01/01: ISIS-2801: adds support for Blob persistence

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2801
in repository https://gitbox.apache.org/repos/asf/isis.git

commit f2a45a17b943b950fdc4eda8ab9ae053d53c0803
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Jul 16 15:09:14 2021 +0100

    ISIS-2801: adds support for Blob persistence
---
 .../dom/types/isis/blobs/jpa/IsisBlobJpa.java      | 113 ++++++++++-------
 .../jpa/adoc/modules/ROOT/pages/mapping-guide.adoc |  54 ++++++++
 .../jpa/applib/types/BlobJpaEmbeddable.java        | 139 +++++++++++++++++++++
 3 files changed, 264 insertions(+), 42 deletions(-)

diff --git a/examples/demo/domain/src/main/java/demoapp/dom/types/isis/blobs/jpa/IsisBlobJpa.java b/examples/demo/domain/src/main/java/demoapp/dom/types/isis/blobs/jpa/IsisBlobJpa.java
index b2c7e10..94b5e5e 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/types/isis/blobs/jpa/IsisBlobJpa.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/types/isis/blobs/jpa/IsisBlobJpa.java
@@ -18,14 +18,18 @@
  */
 package demoapp.dom.types.isis.blobs.jpa;
 
+import java.util.Optional;
+
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Embedded;
 import javax.persistence.Entity;
 import javax.persistence.EntityListeners;
 import javax.persistence.GeneratedValue;
 import javax.persistence.Id;
 import javax.persistence.Table;
 
-import org.springframework.context.annotation.Profile;
-
 import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.Editing;
 import org.apache.isis.applib.annotation.Optionality;
@@ -33,12 +37,12 @@ import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
 import org.apache.isis.applib.annotation.Title;
 import org.apache.isis.applib.value.Blob;
-import org.apache.isis.persistence.jpa.applib.integration.JpaEntityInjectionPointResolver;
+import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
+import org.apache.isis.persistence.jpa.integration.types.BlobJpaEmbeddable;
+import org.springframework.context.annotation.Profile;
 
 import demoapp.dom.types.isis.blobs.persistence.IsisBlobEntity;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
 
 @Profile("demo-jpa")
 //tag::class[]
@@ -47,7 +51,7 @@ import lombok.Setter;
       schema = "demo",
       name = "IsisBlobJpa"
 )
-@EntityListeners(JpaEntityInjectionPointResolver.class)
+@EntityListeners(IsisEntityListener.class)
 @DomainObject(
       logicalTypeName = "demo.IsisBlobEntity"
 )
@@ -57,8 +61,8 @@ public class IsisBlobJpa
 
 //end::class[]
     public IsisBlobJpa(Blob initialValue) {
-        this.readOnlyProperty = initialValue;
-        this.readWriteProperty = initialValue;
+        setReadOnlyProperty(initialValue);
+        setReadWriteProperty(initialValue);
     }
 
 //tag::class[]
@@ -66,51 +70,76 @@ public class IsisBlobJpa
     @GeneratedValue
     private Long id;
 
+    @AttributeOverrides({
+        @AttributeOverride(name="name",    column=@Column(name="readOnlyProperty_name")),
+        @AttributeOverride(name="mimeType",column=@Column(name="readOnlyProperty_mimeType")),
+        @AttributeOverride(name="bytes",   column=@Column(name="readOnlyProperty_bytes"))
+    })
+    @Embedded
+    private BlobJpaEmbeddable readOnlyProperty;
+
     @Title(prepend = "Blob JPA entity: ")
     @PropertyLayout(fieldSetId = "read-only-properties", sequence = "1")
-//    @Persistent(defaultFetchGroup="false", columns = {              // <.>
-//            @Column(name = "readOnlyProperty_name"),
-//            @Column(name = "readOnlyProperty_mimetype"),
-//            @Column(name = "readOnlyProperty_bytes")
-//    })
-    @Getter @Setter
-    private Blob readOnlyProperty;
+    public Blob getReadOnlyProperty() {
+        return readOnlyProperty.toBlob();
+    }
+    public void setReadOnlyProperty(final Blob readOnlyProperty) {
+        this.readOnlyProperty = BlobJpaEmbeddable.from(readOnlyProperty);
+    }
+
+    @AttributeOverrides({
+            @AttributeOverride(name="name",    column=@Column(name="readWriteProperty_name")),
+            @AttributeOverride(name="mimeType",column=@Column(name="readWriteProperty_mimeType")),
+            @AttributeOverride(name="bytes",   column=@Column(name="readWriteProperty_bytes"))
+    })
+    @Embedded
+    private BlobJpaEmbeddable readWriteProperty;
 
     @Property(editing = Editing.ENABLED)                            // <.>
     @PropertyLayout(fieldSetId = "editable-properties", sequence = "1")
-//    @Persistent(defaultFetchGroup="false", columns = {
-//            @Column(name = "readWriteProperty_name"),
-//            @Column(name = "readWriteProperty_mimetype"),
-//            @Column(name = "readWriteProperty_bytes")
-//    })
-    @Getter @Setter
-    private Blob readWriteProperty;
+    public Blob getReadWriteProperty() {
+        return readWriteProperty.toBlob();
+    }
+
+    public void setReadWriteProperty(final Blob readWriteProperty) {
+        this.readWriteProperty = BlobJpaEmbeddable.from(readWriteProperty);
+    }
+
+    @AttributeOverrides({
+            @AttributeOverride(name="name",    column=@Column(name="readOnlyOptionalProperty_name")),
+            @AttributeOverride(name="mimeType",column=@Column(name="readOnlyOptionalProperty_mimeType")),
+            @AttributeOverride(name="bytes",   column=@Column(name="readOnlyOptionalProperty_bytes"))
+    })
+    @Embedded
+    private BlobJpaEmbeddable readOnlyOptionalProperty;
 
     @Property(optionality = Optionality.OPTIONAL)                   // <.>
     @PropertyLayout(fieldSetId = "optional-properties", sequence = "1")
-//    @Persistent(defaultFetchGroup="false", columns = {
-//            @Column(name = "readOnlyOptionalProperty_name",
-//                    allowsNull = "true"),                           // <.>
-//            @Column(name = "readOnlyOptionalProperty_mimetype",
-//                    allowsNull = "true"),
-//            @Column(name = "readOnlyOptionalProperty_bytes",
-//                    allowsNull = "true")
-//    })
-    @Getter @Setter
-    private Blob readOnlyOptionalProperty;
+    public Blob getReadOnlyOptionalProperty() {
+        return Optional.ofNullable(readOnlyOptionalProperty).map(BlobJpaEmbeddable::toBlob).orElse(null);
+    }
+
+    public void setReadOnlyOptionalProperty(final Blob readOnlyOptionalProperty) {
+        this.readOnlyOptionalProperty = BlobJpaEmbeddable.from(readOnlyOptionalProperty);
+    }
+
+
+    @AttributeOverrides({
+            @AttributeOverride(name="name",    column=@Column(name="readWriteOptionalProperty_name")),
+            @AttributeOverride(name="mimeType",column=@Column(name="readWriteOptionalProperty_mimeType")),
+            @AttributeOverride(name="bytes",   column=@Column(name="readWriteOptionalProperty_bytes"))
+    })
+    @Embedded
+    private BlobJpaEmbeddable readWriteOptionalProperty;
 
     @Property(editing = Editing.ENABLED, optionality = Optionality.OPTIONAL)
     @PropertyLayout(fieldSetId = "optional-properties", sequence = "2")
-//    @Persistent(defaultFetchGroup="false", columns = {
-//            @Column(name = "readWriteOptionalProperty_name",
-//                    allowsNull = "true"),
-//            @Column(name = "readWriteOptionalProperty_mimetype",
-//                    allowsNull = "true"),
-//            @Column(name = "readWriteOptionalProperty_bytes",
-//                    allowsNull = "true")
-//    })
-    @Getter @Setter
-    private Blob readWriteOptionalProperty;
+    public Blob getReadWriteOptionalProperty() {
+        return Optional.ofNullable(readWriteOptionalProperty).map(BlobJpaEmbeddable::toBlob).orElse(null);
+    }
 
+    public void setReadWriteOptionalProperty(final Blob readWriteOptionalProperty) {
+        this.readWriteOptionalProperty = BlobJpaEmbeddable.from(readWriteOptionalProperty);
+    }
 }
 //end::class[]
diff --git a/persistence/jpa/adoc/modules/ROOT/pages/mapping-guide.adoc b/persistence/jpa/adoc/modules/ROOT/pages/mapping-guide.adoc
index b254255..73317e1 100644
--- a/persistence/jpa/adoc/modules/ROOT/pages/mapping-guide.adoc
+++ b/persistence/jpa/adoc/modules/ROOT/pages/mapping-guide.adoc
@@ -18,3 +18,57 @@ Take a look at:
 Although written for Hibernate, the JPA material should work fine.
 
 * link:https://www.baeldung.com/tag/jpa/[JPA Baeldung tags]
+
+== Custom Value Types
+
+The framework provides a number of custom value types.
+Some of these are wrappers around a single value (eg `AsciiDoc` or `Password`) while others map onto multiple values (eg `Blob`).
+
+This section shows how to map each (and can be adapted for your own custom types or `@Embedded` values).
+
+
+=== Mapping AsciiDoc
+
+TODO: not really sufficient to use `@Lob`; should provide a custom converter.
+
+
+=== Mapping Blobs and Clobs
+
+While the JPA standard does provide a standardised mechanism to marshall single valued types, but does not (currently) provide a standardised mechanism to marshall multiple valued types.
+
+There are two alternatives:
+
+* use EclipseLink-specific extensions, specifically its link:https://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_transformation.htm[@Transformation] annotation and the related link:https://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_writetransformer.htm#BGBGGAEA[@WriteTransformer] and link:https://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_readtransformer.htm#CHDGHHBA[@ReadTransformer].
+
+* manually map the class to an link:https://www.eclipse.org/eclipselink/documentation/3.0/concepts/entities005.htm#BABGBFDG[@Embeddable] class.
+
+The framework provides a number of off-the-shelf `@Embeddable` classes for this purpose; we think it is less boilerplate overall.
+
+==== Blobs
+
+To map a xref:refguide:applib:index/value/Blob.adoc[Blob], use the xref:refguide:persistence:index/jpa/applib/types/BlobJpaEmbedded.adoc[BlobJpaEmbedded] class:
+
+[source]
+.MyEntity.java
+----
+public class MyEntity ... {
+
+    @Embedded
+    private BlobJpaEmbeddable pdf;              // <.>
+
+   @Property()
+   @PropertyLayout()
+   public Blob getPdf() {                       // <.>
+       return BlobJpaEmbeddable.to(pod);
+   }
+   public void setPdf(final Blob pdf) {
+       this.pdf = BlobJpaEmbeddable.from(pdf);
+   }
+}
+----
+
+<.> the field as it is persisted by the ORM
+<.> the property as it is understood by Apache Isis.
+
+
+TIP: if you have multiple instancs of an `@Embedded` type, the `@javax.persistence.AttributeOverrides` and `@javax.persistence.AttributeOverride` provide a standardised way of fine-tuning the column definitions.
\ No newline at end of file
diff --git a/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/types/BlobJpaEmbeddable.java b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/types/BlobJpaEmbeddable.java
new file mode 100644
index 0000000..bca7d0e
--- /dev/null
+++ b/persistence/jpa/applib/src/main/java/org/apache/isis/persistence/jpa/applib/types/BlobJpaEmbeddable.java
@@ -0,0 +1,139 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.isis.persistence.jpa.applib.types;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+
+import org.apache.isis.applib.value.Blob;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.val;
+
+/**
+ * Although JPA supports custom value types, these can only be for simple values; see <a href="https://github.com/eclipse-ee4j/jpa-api/issues/105">eclipse-ee4j/jpa-api/issues/105</a>.
+ *
+ * <p>
+ *     EclipseLink <i>does</i> provide its <a href="https://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_transformation.htm>Transformation API</a>, but there's a lot of boilerplate involved even so.
+ * </p>
+ *
+ * <p>
+ *     This class provides support for an alternative approach, where the Isis {@link Blob} is marshalled in and out of this class.
+ * </p>
+ *
+ * <p>
+ *    Example usage:
+ *     <pre>
+ *     &#064;Embedded
+ *     private BlobJpaEmbeddable pdf;
+ *
+ *     &#064;Property()
+ *     &#064;PropertyLayout()
+ *     public Blob getPdf() {
+ *         return BlobJpaEmbeddable.to(pod);
+ *     }
+ *     public void setPdf(final Blob pdf) {
+ *         this.pdf = BlobJpaEmbeddable.from(pdf);
+ *     }
+ *    </pre>
+ * </p>
+ *
+ * <p>
+ *     Lastly; note that {@link javax.persistence.AttributeOverrides} and {@link javax.persistence.AttributeOverride}
+ *     provide a standardised way of fine-tuning the column definitions.
+ * </p>
+ * 
+ * @since 2.x {@index}
+ */
+@Embeddable
+@Getter @Setter
+public final class BlobJpaEmbeddable {
+
+    /**
+     * Factory method to marshall a {@link Blob} into a {@link BlobJpaEmbeddable}
+     * 
+     * @see #to(BlobJpaEmbeddable) 
+     */
+    public static BlobJpaEmbeddable from(Blob blob) {
+        if(blob == null) {
+            return null;
+        }
+        val blobJpaEmbeddable = new BlobJpaEmbeddable();
+        blobJpaEmbeddable.bytes = blob.getBytes();
+        blobJpaEmbeddable.mimeType = blob.getMimeType().toString();
+        blobJpaEmbeddable.name = blob.getName();
+        return blobJpaEmbeddable;
+    }
+
+    /**
+     * Reciprocal method to marshall a {@link BlobJpaEmbeddable} into a {@link Blob}
+     * 
+     * @see #from(Blob)
+     */
+    public static Blob to(final BlobJpaEmbeddable pod) {
+        return Optional.ofNullable(pod).map(BlobJpaEmbeddable::toBlob).orElse(null);
+    }
+
+    @Column(nullable = false, length = 255)
+    private String mimeType;
+
+    @Column(nullable = false)
+    private byte[] bytes;
+
+    @Column(nullable = false, length = 255)
+    private String name;
+
+    public Blob toBlob() {
+        return new Blob(name, mimeType, bytes);
+    }
+
+    
+    @Override
+    public String toString() {
+        return toBlob().toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        final BlobJpaEmbeddable that = (BlobJpaEmbeddable) o;
+
+        if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null)
+            return false;
+        if (!Arrays.equals(bytes, that.bytes))
+            return false;
+        return name != null ? name.equals(that.name) : that.name == null;
+    }
+
+    @Override public int hashCode() {
+        int result = mimeType != null ? mimeType.hashCode() : 0;
+        result = 31 * result + Arrays.hashCode(bytes);
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        return result;
+    }
+}