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>
+ * @Embedded
+ * private BlobJpaEmbeddable pdf;
+ *
+ * @Property()
+ * @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;
+ }
+}