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

danhaywood pushed a commit to branch CAUSEWAY-3676
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit d5a6515397e5bdd0efea22338485a083e60ec5c9
Author: danhaywood <[email protected]>
AuthorDate: Wed Feb 14 21:07:15 2024 +0000

    CAUSEWAY-3676: adds support for retrieving blob bytes via rest controller;
    
    also support for obtaining multiple references from a choices or 
autoComplete etc
---
 .../graphql/model/domain/GqlvAbstractCustom.java   |  4 +
 .../viewer/graphql/model/domain/GqlvAction.java    | 17 +---
 .../graphql/model/domain/GqlvAssociationGet.java   |  4 +-
 .../graphql/model/domain/GqlvMetaSaveAs.java       | 14 +++-
 .../viewer/graphql/model/domain/GqlvProperty.java  | 33 +++++---
 .../graphql/model/domain/GqlvPropertyGetBlob.java  | 90 ++++++++++++++++++++++
 ...onGet.java => GqlvPropertyGetBlobAbstract.java} | 60 +++++++--------
 ...taSaveAs.java => GqlvPropertyGetBlobBytes.java} | 38 ++++-----
 ...aveAs.java => GqlvPropertyGetBlobMimeType.java} | 30 ++------
 ...etaSaveAs.java => GqlvPropertyGetBlobName.java} | 32 ++------
 .../viewer/graphql/model/domain/TypeNames.java     |  4 +
 ...d_department_and_add_staff_member_choices._.gql | 34 ++++++++
 ...ment_and_add_staff_member_choices.approved.json | 52 +++++++++++++
 ...department_and_add_staff_members.choices._.gql} |  0
 .../queryandmutations/Department_IntegTest.java    |  9 +++
 .../Staff_IntegTest.list_all_staff_members._.gql   |  6 +-
 ..._IntegTest.list_all_staff_members.approved.json | 30 ++++++--
 viewers/graphql/test/src/test/resources/schema.gql | 28 ++++++-
 viewers/graphql/viewer/pom.xml                     |  4 +
 .../graphql/viewer/src/main/java/module-info.java  |  2 +
 .../viewer/controller/BlobBytesController.java     | 80 +++++++++++++++++++
 21 files changed, 433 insertions(+), 138 deletions(-)

diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
index 9847453041..479b7cc62e 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
@@ -59,6 +59,10 @@ public abstract class GqlvAbstractCustom extends 
GqlvAbstract implements Parent
         return gqlObjectType != null;
     }
 
+    protected final void addChildFieldFor(GqlvAbstract hasField) {
+        addChildField(hasField.getField());
+    }
+
     protected final void addChildField(GraphQLFieldDefinition childField) {
         if (isBuilt()) {
             return;
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
index d4f037dcb7..fa8ef68e2c 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
@@ -72,22 +72,13 @@ public class GqlvAction
             final Context context) {
         super(holder, objectAction, 
TypeNames.actionTypeNameFor(holder.getObjectSpecification(), objectAction), 
context);
 
-        this.hidden = new GqlvMemberHidden<>(this, context);
-        addChildField(hidden.getField());
-
-        this.disabled = new GqlvMemberDisabled<>(this, context);
-        addChildField(disabled.getField());
-
-        this.validate = new GqlvActionValidity(this, context);
-        addChildField(validate.getField());
+        addChildFieldFor(this.hidden = new GqlvMemberHidden<>(this, context));
+        addChildFieldFor(this.disabled = new GqlvMemberDisabled<>(this, 
context));
+        addChildFieldFor(this.validate = new GqlvActionValidity(this, 
context));
 
         val variant = 
context.causewayConfiguration.getViewer().getGraphql().getApiVariant();
         if (objectAction.getSemantics().isSafeInNature() || variant == 
QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT) {
-            this.invoke = new GqlvActionInvoke(this, context);
-            GraphQLFieldDefinition invokeField = this.invoke.getField();
-            if (invokeField != null) {
-                addChildField(invokeField);
-            }
+            addChildFieldFor(this.invoke = new GqlvActionInvoke(this, 
context));
         } else {
             this.invoke = null;
         }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
index 2acd561b72..dbb63d0c3c 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
@@ -56,10 +56,10 @@ public abstract class GqlvAssociationGet<T extends 
ObjectAssociation> extends Gq
     abstract GraphQLOutputType outputTypeFor(Holder<T> holder);
 
     @Override
-    protected Object fetchData(final DataFetchingEnvironment 
dataFetchingEnvironment) {
+    protected Object fetchData(final DataFetchingEnvironment environment) {
 
         // TODO: introduce evaluator
-        val sourcePojo = BookmarkedPojo.sourceFrom(dataFetchingEnvironment);
+        val sourcePojo = BookmarkedPojo.sourceFrom(environment);
 
         val sourcePojoClass = sourcePojo.getClass();
         val objectSpecification = 
context.specificationLoader.loadSpecification(sourcePojoClass);
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
index 9f2e019396..62d54a1567 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
@@ -18,10 +18,13 @@
  */
 package org.apache.causeway.viewer.graphql.model.domain;
 
+import graphql.GraphQLContext;
 import graphql.Scalars;
 import graphql.schema.DataFetchingEnvironment;
 import graphql.schema.GraphQLArgument;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 import org.apache.causeway.viewer.graphql.model.context.Context;
 import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
 
@@ -46,7 +49,16 @@ public class GqlvMetaSaveAs extends GqlvAbstract {
     protected Object fetchData(DataFetchingEnvironment environment) {
         String ref = environment.getArgument("ref");
         GqlvMeta.Fetcher source = environment.getSource();
-        environment.getGraphQlContext().put(keyFor(ref), new 
BookmarkedPojo(source.bookmark(), context.bookmarkService));
+        String originalKey = keyFor(ref);
+        GraphQLContext graphQlContext = environment.getGraphQlContext();
+
+        // we ensure the key hasn't been used already
+        int i = 2; // we start at 2 deliberately, so save "cust", "cust-2", 
"cust-3" ... etc if there is a clash
+        String key = originalKey;
+        while (graphQlContext.hasKey(key)) {
+            key = originalKey + "-" + (i++);
+        }
+        graphQlContext.put(key, new BookmarkedPojo(source.bookmark(), 
context.bookmarkService));
         return ref;
     }
 
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
index 3cd351d1b8..7e82bd35fc 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
@@ -21,6 +21,8 @@ package org.apache.causeway.viewer.graphql.model.domain;
 import graphql.schema.GraphQLArgument;
 import graphql.schema.GraphQLFieldDefinition;
 
+import org.apache.causeway.applib.value.Blob;
+import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
@@ -32,17 +34,18 @@ import lombok.val;
 public class GqlvProperty
         extends GqlvAssociation<OneToOneAssociation, GqlvMember.Holder>
         implements GqlvMemberHidden.Holder<OneToOneAssociation>,
-                   GqlvMemberDisabled.Holder<OneToOneAssociation>,
-                   GqlvPropertyGet.Holder,
-                   GqlvPropertyChoices.Holder,
-                   GqlvPropertyAutoComplete.Holder,
-                   GqlvPropertyValidate.Holder,
-                   GqlvPropertySet.Holder,
-                   GqlvAssociationDatatype.Holder<OneToOneAssociation> {
+        GqlvMemberDisabled.Holder<OneToOneAssociation>,
+        GqlvPropertyGet.Holder,
+        GqlvPropertyChoices.Holder,
+        GqlvPropertyAutoComplete.Holder,
+        GqlvPropertyValidate.Holder,
+        GqlvPropertySet.Holder,
+        GqlvAssociationDatatype.Holder<OneToOneAssociation>,
+        GqlvPropertyGetBlob.Holder {
 
     private final GqlvMemberHidden<OneToOneAssociation> hidden;
     private final GqlvMemberDisabled<OneToOneAssociation> disabled;
-    private final GqlvPropertyGet get;
+    private final GqlvAbstract get;
     /**
      * Populated iff there are choices
      */
@@ -71,7 +74,11 @@ public class GqlvProperty
         this.disabled = new GqlvMemberDisabled<>(this, context);
         addChildField(disabled.getField());
 
-        this.get = new GqlvPropertyGet(this, context);
+        if (isBlob()) {
+            this.get = new GqlvPropertyGetBlob(this, context);
+        } else {
+            this.get = new GqlvPropertyGet(this, context);
+        }
         addChildField(get.getField());
 
         this.validate = new GqlvPropertyValidate(this, context);
@@ -107,6 +114,14 @@ public class GqlvProperty
         buildObjectTypeAndField(oneToOneAssociation.getId());
     }
 
+    private boolean isBlob() {
+        return 
getOneToOneAssociation().getElementType().getCorrespondingClass() == Blob.class;
+    }
+
+    private boolean isClob() {
+        return 
getOneToOneAssociation().getElementType().getCorrespondingClass() == Clob.class;
+    }
+
     public void addGqlArgument(
             final OneToOneAssociation oneToOneAssociation,
             final GraphQLFieldDefinition.Builder builder,
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
new file mode 100644
index 0000000000..bb9e3d830d
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlob.java
@@ -0,0 +1,90 @@
+/*
+ *  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.causeway.viewer.graphql.model.domain;
+
+import graphql.schema.DataFetchingEnvironment;
+
+import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.viewer.graphql.model.context.Context;
+import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
+import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectAssociationProvider;
+import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectSpecificationProvider;
+
+public class GqlvPropertyGetBlob
+        extends GqlvAbstractCustom
+        implements GqlvPropertyGetBlobBytes.Holder
+{
+
+    final Holder holder;
+    final GqlvPropertyGetBlobBytes blobName;
+    final GqlvPropertyGetBlobMimeType blobMimeType;
+    final GqlvPropertyGetBlobName blobBytes;
+
+    public GqlvPropertyGetBlob(
+            final Holder holder,
+            final Context context) {
+        
super(TypeNames.propertyBlobTypeNameFor(holder.getObjectSpecification(), 
holder.getObjectMember()), context);
+        this.holder = holder;
+
+        addChildFieldFor(blobName = new GqlvPropertyGetBlobBytes(this, 
context));
+        addChildFieldFor(blobMimeType = new GqlvPropertyGetBlobMimeType(this, 
context));
+        addChildFieldFor(blobBytes = new GqlvPropertyGetBlobName(this, 
context));
+
+        setField(newFieldDefinition()
+                    .name("get")
+                    .type(buildObjectType())
+                    .build());
+    }
+
+    @Override
+    protected Object fetchData(final DataFetchingEnvironment 
dataFetchingEnvironment) {
+        return BookmarkedPojo.sourceFrom(dataFetchingEnvironment, context);
+    }
+
+    @Override
+    protected void addDataFetchersForChildren() {
+        blobName.addDataFetcher(this);
+        blobMimeType.addDataFetcher(this);
+        blobBytes.addDataFetcher(this);
+    }
+
+    @Override
+    public OneToOneAssociation getObjectAssociation() {
+        return holder.getObjectAssociation();
+    }
+
+    @Override
+    public OneToOneAssociation getObjectMember() {
+        return holder.getObjectMember();
+    }
+
+    @Override
+    public ObjectSpecification getObjectSpecification() {
+        return holder.getObjectSpecification();
+    }
+
+    public interface Holder
+            extends ObjectSpecificationProvider,
+                    ObjectAssociationProvider<OneToOneAssociation> {
+
+    }
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobAbstract.java
similarity index 55%
copy from 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
copy to 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobAbstract.java
index 2acd561b72..b2a9d79136 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAssociationGet.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobAbstract.java
@@ -18,48 +18,39 @@
  */
 package org.apache.causeway.viewer.graphql.model.domain;
 
-import graphql.schema.DataFetchingEnvironment;
-import graphql.schema.GraphQLOutputType;
+import java.util.Optional;
+import java.util.function.Function;
 
-import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+import graphql.Scalars;
+import graphql.schema.DataFetchingEnvironment;
 
+import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
 import org.apache.causeway.viewer.graphql.model.context.Context;
 import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
-import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectAssociationProvider;
-import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectSpecificationProvider;
+
+import graphql.schema.GraphQLFieldDefinition;
 
 import lombok.val;
 
-public abstract class GqlvAssociationGet<T extends ObjectAssociation> extends 
GqlvAbstract {
+public abstract class GqlvPropertyGetBlobAbstract extends GqlvAbstract {
 
-    final Holder<T> holder;
+    final Holder holder;
 
-    public GqlvAssociationGet(
-            final Holder<T> holder,
-            final Context context) {
+    public GqlvPropertyGetBlobAbstract(
+            final Holder holder,
+            final Context context, String name) {
         super(context);
         this.holder = holder;
 
-        GraphQLOutputType type = outputTypeFor(holder);
-        if (type != null) {
-            val fieldBuilder = newFieldDefinition()
-                    .name("get")
-                    .type(type);
-            setField(fieldBuilder.build());
-        } else {
-            setField(null);
-        }
+        setField(GraphQLFieldDefinition.newFieldDefinition()
+                    .name(name)
+                    .type(Scalars.GraphQLString)
+                    .build());
     }
 
-    abstract GraphQLOutputType outputTypeFor(Holder<T> holder);
-
-    @Override
-    protected Object fetchData(final DataFetchingEnvironment 
dataFetchingEnvironment) {
-
-        // TODO: introduce evaluator
-        val sourcePojo = BookmarkedPojo.sourceFrom(dataFetchingEnvironment);
+    protected Object fetchDataFromBlob(DataFetchingEnvironment environment, 
Function<Blob, ?> mapper) {
+        val sourcePojo = BookmarkedPojo.sourceFrom(environment);
 
         val sourcePojoClass = sourcePojo.getClass();
         val objectSpecification = 
context.specificationLoader.loadSpecification(sourcePojoClass);
@@ -72,14 +63,15 @@ public abstract class GqlvAssociationGet<T extends 
ObjectAssociation> extends Gq
         val managedObject = ManagedObject.adaptSingular(objectSpecification, 
sourcePojo);
         val resultManagedObject = association.get(managedObject);
 
-        return resultManagedObject != null
-                ? resultManagedObject.getPojo()
-                : null;
+        return Optional.ofNullable(resultManagedObject)
+                .map(ManagedObject::getPojo)
+                .filter(Blob.class::isInstance)
+                .map(Blob.class::cast)
+                .map(mapper)
+                .orElse(null);
     }
 
-    public interface Holder<T extends ObjectAssociation>
-            extends ObjectSpecificationProvider,
-                    ObjectAssociationProvider<T> {
-
+    public interface Holder extends GqlvPropertyGet.Holder {
     }
+
 }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
similarity index 55%
copy from 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
copy to 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
index 9f2e019396..5e2246d39c 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobBytes.java
@@ -18,40 +18,32 @@
  */
 package org.apache.causeway.viewer.graphql.model.domain;
 
-import graphql.Scalars;
 import graphql.schema.DataFetchingEnvironment;
-import graphql.schema.GraphQLArgument;
 
-import org.apache.causeway.viewer.graphql.model.context.Context;
-import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
+import lombok.val;
 
-import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+import java.util.Optional;
 
-public class GqlvMetaSaveAs extends GqlvAbstract {
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.viewer.graphql.model.context.Context;
+import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
 
-    public GqlvMetaSaveAs(final Context context) {
-        super(context);
+public class GqlvPropertyGetBlobBytes extends GqlvPropertyGetBlobAbstract {
 
-        setField(newFieldDefinition()
-                    .name("saveAs")
-                    .type(Scalars.GraphQLString)
-                    .argument(new GraphQLArgument.Builder()
-                            .name("ref")
-                            .type(Scalars.GraphQLString)
-                    )
-                    .build());
+    public GqlvPropertyGetBlobBytes(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "bytes");
     }
 
     @Override
     protected Object fetchData(DataFetchingEnvironment environment) {
-        String ref = environment.getArgument("ref");
-        GqlvMeta.Fetcher source = environment.getSource();
-        environment.getGraphQlContext().put(keyFor(ref), new 
BookmarkedPojo(source.bookmark(), context.bookmarkService));
-        return ref;
-    }
+        val sourcePojo = BookmarkedPojo.sourceFrom(environment);
+
+        Optional<Bookmark> bookmarkIfAny = 
context.bookmarkService.bookmarkFor(sourcePojo);
+        return bookmarkIfAny.map(x -> String.format(
+                "///%s/object/%s:%s/%s/blobBytes", "graphql", 
x.getLogicalTypeName(), x.getIdentifier(), 
holder.getObjectAssociation().getId())).orElse(null);
 
-    public static String keyFor(String ref) {
-        return GqlvMetaSaveAs.class.getName() + "#" + ref;
     }
 
 }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobMimeType.java
similarity index 51%
copy from 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
copy to 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobMimeType.java
index 9f2e019396..f80bdf3284 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobMimeType.java
@@ -18,40 +18,22 @@
  */
 package org.apache.causeway.viewer.graphql.model.domain;
 
-import graphql.Scalars;
 import graphql.schema.DataFetchingEnvironment;
-import graphql.schema.GraphQLArgument;
 
 import org.apache.causeway.viewer.graphql.model.context.Context;
-import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
 
-import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+public class GqlvPropertyGetBlobMimeType extends GqlvPropertyGetBlobAbstract {
 
-public class GqlvMetaSaveAs extends GqlvAbstract {
+    public GqlvPropertyGetBlobMimeType(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "mimeType");
 
-    public GqlvMetaSaveAs(final Context context) {
-        super(context);
-
-        setField(newFieldDefinition()
-                    .name("saveAs")
-                    .type(Scalars.GraphQLString)
-                    .argument(new GraphQLArgument.Builder()
-                            .name("ref")
-                            .type(Scalars.GraphQLString)
-                    )
-                    .build());
     }
 
     @Override
     protected Object fetchData(DataFetchingEnvironment environment) {
-        String ref = environment.getArgument("ref");
-        GqlvMeta.Fetcher source = environment.getSource();
-        environment.getGraphQlContext().put(keyFor(ref), new 
BookmarkedPojo(source.bookmark(), context.bookmarkService));
-        return ref;
-    }
-
-    public static String keyFor(String ref) {
-        return GqlvMetaSaveAs.class.getName() + "#" + ref;
+        return fetchDataFromBlob(environment, blob -> 
blob.getMimeType().toString());
     }
 
 }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobName.java
similarity index 51%
copy from 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
copy to 
viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobName.java
index 9f2e019396..63180d7c17 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMetaSaveAs.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetBlobName.java
@@ -18,40 +18,22 @@
  */
 package org.apache.causeway.viewer.graphql.model.domain;
 
-import graphql.Scalars;
 import graphql.schema.DataFetchingEnvironment;
-import graphql.schema.GraphQLArgument;
 
+import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.viewer.graphql.model.context.Context;
-import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
 
-import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+public class GqlvPropertyGetBlobName extends GqlvPropertyGetBlobAbstract {
 
-public class GqlvMetaSaveAs extends GqlvAbstract {
-
-    public GqlvMetaSaveAs(final Context context) {
-        super(context);
-
-        setField(newFieldDefinition()
-                    .name("saveAs")
-                    .type(Scalars.GraphQLString)
-                    .argument(new GraphQLArgument.Builder()
-                            .name("ref")
-                            .type(Scalars.GraphQLString)
-                    )
-                    .build());
+    public GqlvPropertyGetBlobName(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "name");
     }
 
     @Override
     protected Object fetchData(DataFetchingEnvironment environment) {
-        String ref = environment.getArgument("ref");
-        GqlvMeta.Fetcher source = environment.getSource();
-        environment.getGraphQlContext().put(keyFor(ref), new 
BookmarkedPojo(source.bookmark(), context.bookmarkService));
-        return ref;
-    }
-
-    public static String keyFor(String ref) {
-        return GqlvMetaSaveAs.class.getName() + "#" + ref;
+        return fetchDataFromBlob(environment, Blob::getName);
     }
 
 }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/TypeNames.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/TypeNames.java
index ca90a3f5b2..abe4d837f0 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/TypeNames.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/TypeNames.java
@@ -61,6 +61,10 @@ public final class TypeNames {
         return objectTypeNameFor(owningType) + "__" + 
oneToOneAssociation.getId() + "__gqlv_property";
     }
 
+    public static String propertyBlobTypeNameFor(ObjectSpecification 
owningType, OneToOneAssociation oneToOneAssociation) {
+        return objectTypeNameFor(owningType) + "__" + 
oneToOneAssociation.getId() + "__gqlv_property_blob";
+    }
+
     public static String collectionTypeNameFor(ObjectSpecification owningType, 
OneToManyAssociation objectMember) {
         return objectTypeNameFor(owningType) + "__" + objectMember.getId() + 
"__gqlv_collection";
     }
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices._.gql
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices._.gql
new file mode 100644
index 0000000000..3f8207e675
--- /dev/null
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices._.gql
@@ -0,0 +1,34 @@
+{
+  Scenario(name: "checks choice 'saveAs' reference numbering") {
+    Given {
+      university_dept_Departments {
+        findDepartmentByName {
+          invoke(name: "Classics") {
+            addStaffMembers {
+              params {
+                staffMembers {
+                  choices {
+                    _gqlv_meta {
+                      id
+                      saveAs(ref: "staff-member-choices")
+                    }
+                    name {
+                      get
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    Then {
+      university_dept_StaffMember(object: {ref: "staff-member-choices-2"}) {
+        name {
+          get
+        }
+      }
+    }
+  }
+}
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices.approved.json
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices.approved.json
new file mode 100644
index 0000000000..5d9cf1af12
--- /dev/null
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_member_choices.approved.json
@@ -0,0 +1,52 @@
+{
+  "data" : {
+    "Scenario" : {
+      "Given" : {
+        "university_dept_Departments" : {
+          "findDepartmentByName" : {
+            "invoke" : {
+              "addStaffMembers" : {
+                "params" : {
+                  "staffMembers" : {
+                    "choices" : [ {
+                      "_gqlv_meta" : {
+                        "id" : "15",
+                        "saveAs" : "staff-member-choices"
+                      },
+                      "name" : {
+                        "get" : "John Gartner"
+                      }
+                    }, {
+                      "_gqlv_meta" : {
+                        "id" : "16",
+                        "saveAs" : "staff-member-choices"
+                      },
+                      "name" : {
+                        "get" : "Margaret Randall"
+                      }
+                    }, {
+                      "_gqlv_meta" : {
+                        "id" : "14",
+                        "saveAs" : "staff-member-choices"
+                      },
+                      "name" : {
+                        "get" : "Mervin Hughes"
+                      }
+                    } ]
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      "Then" : {
+        "university_dept_StaffMember" : {
+          "name" : {
+            "get" : "Margaret Randall"
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_members._.choices.gql
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_members.choices._.gql
similarity index 100%
rename from 
viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_members._.choices.gql
rename to 
viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.find_department_and_add_staff_members.choices._.gql
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.java
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.java
index 44778f12b7..5db64cac19 100644
--- 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.java
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Department_IntegTest.java
@@ -113,6 +113,15 @@ public class Department_IntegTest extends 
Abstract_IntegTest {
         Approvals.verify(submit(), jsonOptions());
     }
 
+    @Test
+    @UseReporter(DiffReporter.class)
+    void find_department_and_add_staff_member_choices() throws Exception {
+
+        // when, then
+        Approvals.verify(submit(), jsonOptions());
+
+    }
+
     @Test
     @UseReporter(DiffReporter.class)
     void find_department_and_add_staff_members() throws Exception {
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members._.gql
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members._.gql
index f4b574714d..5d3c3da6c5 100644
--- 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members._.gql
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members._.gql
@@ -9,7 +9,11 @@
           get
         }
         photo {
-          get
+          get {
+            name
+            mimeType
+            bytes
+          }
         }
       }
     }
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members.approved.json
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members.approved.json
index a78da165d1..58c2e97788 100644
--- 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members.approved.json
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.list_all_staff_members.approved.json
@@ -10,7 +10,11 @@
             "get" : "LECTURER"
           },
           "photo" : {
-            "get" : "StaffMember-photo-Bar.pdf [application/pdf]: 47488 bytes"
+            "get" : {
+              "name" : "StaffMember-photo-Bar.pdf",
+              "mimeType" : "application/pdf",
+              "bytes" : 
"///graphql/object/university.dept.StaffMember:658/photo/blobBytes"
+            }
           }
         }, {
           "name" : {
@@ -20,7 +24,11 @@
             "get" : "LECTURER"
           },
           "photo" : {
-            "get" : null
+            "get" : {
+              "name" : null,
+              "mimeType" : null,
+              "bytes" : 
"///graphql/object/university.dept.StaffMember:660/photo/blobBytes"
+            }
           }
         }, {
           "name" : {
@@ -30,7 +38,11 @@
             "get" : "LECTURER"
           },
           "photo" : {
-            "get" : "StaffMember-photo-Foo.pdf [application/pdf]: 47185 bytes"
+            "get" : {
+              "name" : "StaffMember-photo-Foo.pdf",
+              "mimeType" : "application/pdf",
+              "bytes" : 
"///graphql/object/university.dept.StaffMember:657/photo/blobBytes"
+            }
           }
         }, {
           "name" : {
@@ -40,7 +52,11 @@
             "get" : "LECTURER"
           },
           "photo" : {
-            "get" : null
+            "get" : {
+              "name" : null,
+              "mimeType" : null,
+              "bytes" : 
"///graphql/object/university.dept.StaffMember:661/photo/blobBytes"
+            }
           }
         }, {
           "name" : {
@@ -50,7 +66,11 @@
             "get" : "LECTURER"
           },
           "photo" : {
-            "get" : "StaffMember-photo-Fizz.pdf [application/pdf]: 46833 bytes"
+            "get" : {
+              "name" : "StaffMember-photo-Fizz.pdf",
+              "mimeType" : "application/pdf",
+              "bytes" : 
"///graphql/object/university.dept.StaffMember:659/photo/blobBytes"
+            }
           }
         } ]
       }
diff --git a/viewers/graphql/test/src/test/resources/schema.gql 
b/viewers/graphql/test/src/test/resources/schema.gql
index 5a059d85a2..4aca251bf4 100644
--- a/viewers/graphql/test/src/test/resources/schema.gql
+++ b/viewers/graphql/test/src/test/resources/schema.gql
@@ -228,6 +228,7 @@ type causeway_applib_DomainObjectList__gqlv_meta {
 }
 
 type causeway_applib_DomainObjectList__objects__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -249,6 +250,7 @@ type causeway_applib_FacetGroupNode {
 }
 
 type causeway_applib_FacetGroupNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -289,6 +291,7 @@ type causeway_applib_ParameterNode {
 }
 
 type causeway_applib_ParameterNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -331,6 +334,7 @@ type causeway_applib_PropertyNode {
 }
 
 type causeway_applib_PropertyNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -416,6 +420,7 @@ type causeway_applib_TypeNode {
 }
 
 type causeway_applib_TypeNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -557,6 +562,7 @@ type causeway_applib_UserMemento__realName__gqlv_property {
 }
 
 type causeway_applib_UserMemento__roles__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_applib_RoleMemento]
   hidden: Boolean
@@ -601,6 +607,7 @@ type causeway_applib_node_ActionNode__action__gqlv_property 
{
 }
 
 type causeway_applib_node_ActionNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -643,6 +650,7 @@ type causeway_applib_node_CollectionNode {
 }
 
 type causeway_applib_node_CollectionNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -693,6 +701,7 @@ type causeway_applib_node_FacetAttrNode {
 }
 
 type causeway_applib_node_FacetAttrNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -735,6 +744,7 @@ type causeway_applib_node_FacetNode {
 }
 
 type causeway_applib_node_FacetNode__childNodes__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -830,6 +840,7 @@ type causeway_conf_ConfigurationViewmodel {
 }
 
 type causeway_conf_ConfigurationViewmodel__environment__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_conf_ConfigurationProperty]
   hidden: Boolean
@@ -847,12 +858,14 @@ type causeway_conf_ConfigurationViewmodel__gqlv_meta {
 }
 
 type causeway_conf_ConfigurationViewmodel__primary__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_conf_ConfigurationProperty]
   hidden: Boolean
 }
 
 type causeway_conf_ConfigurationViewmodel__secondary__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_conf_ConfigurationProperty]
   hidden: Boolean
@@ -922,6 +935,7 @@ type causeway_feat_ApplicationNamespace {
 }
 
 type causeway_feat_ApplicationNamespace__contents__gqlv_collection {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -1281,12 +1295,14 @@ type 
causeway_feat_ApplicationTypeProperty__typicalLength__gqlv_property {
 }
 
 type causeway_feat_ApplicationType__actions__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_feat_ApplicationTypeAction]
   hidden: Boolean
 }
 
 type causeway_feat_ApplicationType__collections__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_feat_ApplicationTypeCollection]
   hidden: Boolean
@@ -1330,6 +1346,7 @@ type causeway_feat_ApplicationType__parent__gqlv_property 
{
 }
 
 type causeway_feat_ApplicationType__properties__gqlv_collection {
+  datatype: String
   disabled: String
   get: [causeway_feat_ApplicationTypeProperty]
   hidden: Boolean
@@ -1656,6 +1673,7 @@ type 
org_apache_causeway_core_metamodel_inspect_model_MMNode {
 }
 
 type 
org_apache_causeway_core_metamodel_inspect_model_MMNode__childNodes__gqlv_collection
 {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -1687,6 +1705,7 @@ type 
org_apache_causeway_core_metamodel_inspect_model_MemberNode {
 }
 
 type 
org_apache_causeway_core_metamodel_inspect_model_MemberNode__childNodes__gqlv_collection
 {
+  datatype: String
   disabled: String
   hidden: Boolean
 }
@@ -2511,6 +2530,7 @@ type 
university_dept_Department__removeStaffMember__staffMember__gqlv_action_par
 
 "Staff member of a university department, responsible for delivering lectures, 
tutorials, exam invigilation and candidate interviews"
 type university_dept_Department__staffMembers__gqlv_collection {
+  datatype: String
   disabled: String
   get: [university_dept_StaffMember]
   hidden: Boolean
@@ -2755,12 +2775,18 @@ type university_dept_StaffMember__name__gqlv_property {
 type university_dept_StaffMember__photo__gqlv_property {
   datatype: String
   disabled: String
-  get: String
+  get: university_dept_StaffMember__photo__gqlv_property_blob
   hidden: Boolean
   set(photo: String): university_dept_StaffMember
   validate(photo: String): String
 }
 
+type university_dept_StaffMember__photo__gqlv_property_blob {
+  bytes: String
+  mimeType: String
+  name: String
+}
+
 type 
university_dept_Staff__createStaffMember__department__gqlv_action_parameter {
   choices(name: String): [university_dept_Department]
   datatype: String
diff --git a/viewers/graphql/viewer/pom.xml b/viewers/graphql/viewer/pom.xml
index f83bdf9a1c..50ffa9b7e9 100644
--- a/viewers/graphql/viewer/pom.xml
+++ b/viewers/graphql/viewer/pom.xml
@@ -47,6 +47,10 @@
        </build>
        <dependencies>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot</artifactId>
+        </dependency>
                <dependency>
                        <groupId>org.apache.causeway.viewer</groupId>
                        <artifactId>causeway-viewer-graphql-model</artifactId>
diff --git a/viewers/graphql/viewer/src/main/java/module-info.java 
b/viewers/graphql/viewer/src/main/java/module-info.java
index d1d8a7bb90..4a9a36c67a 100644
--- a/viewers/graphql/viewer/src/main/java/module-info.java
+++ b/viewers/graphql/viewer/src/main/java/module-info.java
@@ -1,6 +1,7 @@
 module org.apache.causeway.incubator.viewer.graphql.viewer {
     exports org.apache.causeway.viewer.graphql.viewer;
     exports org.apache.causeway.viewer.graphql.viewer.integration;
+    exports org.apache.causeway.viewer.graphql.viewer.controller;
 
     requires com.fasterxml.jackson.core;
     requires com.fasterxml.jackson.databind;
@@ -25,4 +26,5 @@ module org.apache.causeway.incubator.viewer.graphql.viewer {
     requires spring.graphql;
     requires spring.tx;
     requires org.apache.causeway.incubator.viewer.graphql.applib;
+    requires spring.web;
 }
\ No newline at end of file
diff --git 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
new file mode 100644
index 0000000000..fc5ae3e523
--- /dev/null
+++ 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
@@ -0,0 +1,80 @@
+package org.apache.causeway.viewer.graphql.viewer.controller;
+
+import javax.inject.Inject;
+
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.value.Blob;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
+
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
+
+import java.util.Optional;
+
+@RestController(value = "/graphql/object")
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class BlobBytesController {
+
+    private final BookmarkService bookmarkService;
+    private final ObjectManager objectManager;
+
+    @Value(staticConstructor = "of")
+    private static class ManagedObjectAndPropertyIfAny {
+        ManagedObject owningObject;
+        Optional<OneToOneAssociation> propertyIfAny;
+        boolean isPropertyPresent() {
+            return propertyIfAny.isPresent();
+        }
+    }
+
+    private static class ManagedObjectAndProperty {
+        private static ManagedObjectAndProperty 
of(ManagedObjectAndPropertyIfAny tuple) {
+            return new ManagedObjectAndProperty(tuple);
+        }
+        private ManagedObjectAndProperty(ManagedObjectAndPropertyIfAny tuple) {
+            this.owningObject = tuple.owningObject;
+            this.property = tuple.propertyIfAny.orElse(null);
+        }
+        ManagedObject owningObject;
+        OneToOneAssociation property;
+
+        public ManagedObject value() {
+            return property.get(owningObject);
+        }
+    }
+
+    @GetMapping(value = "/{logicalTypeName}:{id}/{propertyId}/blobBytes")
+    public ResponseEntity<byte[]> propertyBlobBytes(
+            @PathVariable final String logicalTypeName,
+            @PathVariable final String id,
+            @PathVariable final String propertyId) {
+
+        return 
bookmarkService.lookup(Bookmark.forLogicalTypeNameAndIdentifier(logicalTypeName,
 id))
+                .map(objectManager::adapt)
+                .map(managedObject -> 
ManagedObjectAndPropertyIfAny.of(managedObject, 
managedObject.getSpecification().getProperty(propertyId)))
+                .filter(ManagedObjectAndPropertyIfAny::isPropertyPresent)
+                .map(ManagedObjectAndProperty::of)
+                .map(ManagedObjectAndProperty::value)
+                .map(ManagedObject::getPojo)
+                .filter(Blob.class::isInstance)
+                .map(Blob.class::cast)
+                .map(blob -> ResponseEntity.ok()
+                        .contentType(MediaType.APPLICATION_PDF)
+                        .header(HttpHeaders.CONTENT_DISPOSITION, 
ContentDisposition.attachment().filename(blob.getName()).build().toString())
+                        .contentLength(blob.getBytes().length)
+                        .body(blob.getBytes()))
+                .orElse(ResponseEntity.notFound().build());
+    }
+}


Reply via email to