You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/05/17 05:32:48 UTC

(causeway) 01/01: CAUSEWAY-4000: PoC for advanced StandaloneValuePanel rendering

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

andi-huber pushed a commit to branch 4000-wkt.standalone.val
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 7fa86cf4bea7560919fe963cff05c85ed7a11795
Author: andi-huber <ah...@apache.org>
AuthorDate: Sun May 17 07:32:35 2026 +0200

    CAUSEWAY-4000: PoC for advanced StandaloneValuePanel rendering
---
 .../causeway/core/metamodel/util/Facets.java       |   7 +-
 .../valuesemantics/URLValueSemantics.java          |  11 +-
 .../wicket/model/models/PseudoAttributeModel.java  | 172 +++++++++++++++++++++
 .../viewer/wicket/model/models/UiAttributeWkt.java |  12 +-
 .../model/models/coll/CollectionModelAbstract.java |  15 +-
 .../ui/components/value/StandaloneValuePanel.java  |  14 +-
 .../StandaloneCollectionPage.java                  |  29 +---
 7 files changed, 217 insertions(+), 43 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
index d9dc855057d..d79bc43d309 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
@@ -317,12 +317,13 @@ public ManagedObject projected(final ManagedObject objectAdapter) {
             .orElse(objectAdapter);
     }
 
-    public Optional<PromptStyle> promptStyle(final ObjectFeature feature) {
-        return feature.lookupFacet(PromptStyleFacet.class)
+    public Optional<PromptStyle> promptStyle(final @Nullable ObjectFeature feature) {
+        return Optional.ofNullable(feature)
+    		.flatMap(f->f.lookupFacet(PromptStyleFacet.class))
             .map(PromptStyleFacet::value);
     }
 
-    public PromptStyle promptStyleOrElse(final ObjectFeature feature, final PromptStyle fallback) {
+    public PromptStyle promptStyleOrElse(final @Nullable ObjectFeature feature, final PromptStyle fallback) {
         return Facets.promptStyle(feature)
             .map(promptStyle->
                 promptStyle == PromptStyle.AS_CONFIGURED
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/URLValueSemantics.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/URLValueSemantics.java
index 5fe9eda063c..e28c3054d10 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/URLValueSemantics.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/valuesemantics/URLValueSemantics.java
@@ -81,19 +81,18 @@ private java.net.URL parseUrlElseNull(final String url) {
 
     @Override
     public String titlePresentation(final ValueSemanticsProvider.Context context, final java.net.URL value) {
-        return renderTitle(value, v->v.toString());
+        return renderTitle(value, URL::toString);
     }
 
     @Override
     public String htmlPresentation(final ValueSemanticsProvider.Context context, final java.net.URL value) {
-        return renderHtml(value, v->toHtmlLink(v));
+        return renderHtml(value, this::toHtmlLink);
     }
 
     private String toHtmlLink(final java.net.URL url) {
-        return String.format("<a "
-                + "target=\"_blank\" "
-                + "class=\"no-click-bubbling\" "
-                + "href=\"%s\">%s</a>", url.toString(), url.toString());
+        return """
+			<a target="_blank" class="no-click-bubbling" href="%s">%s</a>"""
+        		.formatted(url.toString(), url.toString());
     }
 
     // -- PARSER
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/PseudoAttributeModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/PseudoAttributeModel.java
new file mode 100644
index 00000000000..254c8ffe813
--- /dev/null
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/PseudoAttributeModel.java
@@ -0,0 +1,172 @@
+/*
+ *  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.wicket.model.models;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.commons.binding.Bindable;
+import org.apache.causeway.commons.binding.Observable;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.binding._Bindables;
+import org.apache.causeway.commons.internal.binding._Observables;
+import org.apache.causeway.core.metamodel.commons.ViewOrEditMode;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FeatureType;
+import org.apache.causeway.core.metamodel.interactions.managed.InteractionVeto;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedValue;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
+import org.apache.causeway.viewer.commons.model.hints.RenderingHint;
+
+/**
+ * Wraps a {@link ValueModel}. Used for rendering standalone values.
+ */
+class PseudoAttributeModel extends UiAttributeWkt {
+
+	private static final long serialVersionUID = 1L;
+
+	final ValueModel valueModel;
+
+	//TODO don't serialize the value object twice
+	protected PseudoAttributeModel(final ValueModel valueModel) {
+		super(UiObjectWkt.ofAdapter(valueModel.getObject()), ViewOrEditMode.VIEWING, RenderingHint.STANDALONE_PROPERTY_COLUMN);
+		this.valueModel = valueModel;
+	}
+
+	record PseudoFeature(ValueModel valueModel) implements ObjectFeature {
+		@Override public FeatureType getFeatureType() {
+			return FeatureType.OBJECT;
+		}
+		@Override public FacetHolder getFacetHolder() {
+			return valueModel.elementType();
+		}
+		@Override public String getId() {
+			return "$value";
+		}
+		@Override public String getFriendlyName(final Supplier<ManagedObject> domainObjectProvider) {
+			return "value";
+		}
+		@Override public Optional<String> getStaticFriendlyName() {
+			return Optional.of("value");
+		}
+		@Override public String getCanonicalFriendlyName() {
+			return "value";
+		}
+		@Override public Optional<String> getDescription(final Supplier<ManagedObject> domainObjectProvider) {
+			return Optional.empty();
+		}
+		@Override public Optional<String> getStaticDescription() {
+			return Optional.empty();
+		}
+		@Override public Optional<String> getCanonicalDescription() {
+			return Optional.empty();
+		}
+		@Override public ObjectSpecification getElementType() {
+			return valueModel.elementType();
+		}
+		@Override public String asciiId() {
+			return "$value";
+		}
+	}
+
+	record PseudoManagedValue(ValueModel valueModel) implements ManagedValue {
+
+		@Override public ObjectSpecification getElementType() {
+			return valueModel.elementType();
+		}
+		@Override public Bindable<ManagedObject> getValue() {
+			return _Bindables.forValue(valueModel.getObject());
+		}
+		@Override public Observable<String> getValueAsTitle() {
+			// TODO Auto-generated method stub
+			return null;
+		}
+		@Override public Observable<String> getValueAsHtml() {
+			// TODO Auto-generated method stub
+			return null;
+		}
+		@Override public boolean isValueAsParsableTextSupported() {
+			return false;
+		}
+		@Override
+		public Bindable<String> getValueAsParsableText() {
+			// TODO Auto-generated method stub
+			return null;
+		}
+		@Override public Observable<String> getValidationMessage() {
+			// TODO Auto-generated method stub
+			return null;
+		}
+		@Override public Bindable<String> getSearchArgument() {
+			// TODO Auto-generated method stub
+			return null;
+		}
+		@Override public Observable<Can<ManagedObject>> getChoices() {
+			return _Observables.lazy(Can::empty);
+		}
+	}
+
+	@Override
+	public ObjectFeature getMetaModel() {
+		return new PseudoFeature(valueModel);
+	}
+
+	@Override
+	public ManagedObject getOwner() {
+		return valueModel.getObject();
+	}
+
+	@Override
+	public String getIdentifier() {
+		return "$value";
+	}
+
+	@Override
+	public ManagedValue proposedValue() {
+		return new PseudoManagedValue(valueModel);
+	}
+
+	@Override
+	protected String toStringOf() {
+		return "%s[%s]".formatted(getClass().getName(), getIdentifier());
+	}
+
+	@Override public boolean isSingular() { return true; }
+	@Override public boolean isPlural() { return false; }
+	@Override public boolean isProperty() { return false; }
+	@Override public boolean isParameter() { return false; }
+
+	@Override public String getCssClass() { return null; }
+	@Override public boolean whetherHidden() { return false; }
+	@Override public Optional<InteractionVeto> disabledReason() { return Optional.empty(); }
+	@Override public int getAutoCompleteMinLength() { return 0; }
+	@Override public ManagedObject getDefault() { return valueModel.getObject(); }
+	@Override public boolean hasChoices() {  return false; }
+	@Override public boolean hasAutoComplete() { return false; }
+	@Override public Can<ManagedObject> getChoices() { return Can.empty(); }
+	@Override public Can<ManagedObject> getAutoComplete(final String searchArg) { return Can.empty(); }
+	@Override public String validate(@NonNull final ManagedObject proposedAdapter) {  return null; }
+	@Override protected Can<ObjectAction> calcAssociatedActions() { return Can.empty(); }
+
+}
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiAttributeWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiAttributeWkt.java
index 90c1d8319e4..56857af14bc 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiAttributeWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/UiAttributeWkt.java
@@ -67,6 +67,13 @@ public abstract class UiAttributeWkt
 
     private static final long serialVersionUID = 1L;
 
+    /**
+     * Returns a readonly variant, for standalone value rendering.
+     */
+    public static UiAttributeWkt pseudo(final ValueModel valueModel) {
+		return new PseudoAttributeModel(valueModel);
+    }
+
     private final UiObjectWkt parentEntityModel;
 
     @Getter(onMethod_={@Override})
@@ -215,11 +222,10 @@ public InlinePromptContext getInlinePromptContext() {
     }
 
     public void setInlinePromptContext(final InlinePromptContext inlinePromptContext) {
-        if (this.inlinePromptContext != null) {
-            // otherwise the components created for an property edit inline prompt will overwrite the original
+        if (this.inlinePromptContext != null)
+			// otherwise the components created for an property edit inline prompt will overwrite the original
             // components on the underlying page (which we go back to if the prompt is cancelled).
             return;
-        }
         this.inlinePromptContext = inlinePromptContext;
     }
 
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/coll/CollectionModelAbstract.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/coll/CollectionModelAbstract.java
index 6222bb9cb42..8287d651d40 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/coll/CollectionModelAbstract.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/coll/CollectionModelAbstract.java
@@ -19,10 +19,13 @@
 package org.apache.causeway.viewer.wicket.model.models.coll;
 
 import java.util.List;
+import java.util.Optional;
 
 import org.apache.wicket.model.LoadableDetachableModel;
+import org.jspecify.annotations.NonNull;
 
 import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
@@ -32,8 +35,6 @@
 import org.apache.causeway.viewer.wicket.model.models.ActionModel;
 import org.apache.causeway.viewer.wicket.model.models.interaction.BookmarkedObjectWkt;
 
-import org.jspecify.annotations.NonNull;
-
 /**
  * Represents a collection (a member) of a domain object.
  *
@@ -118,6 +119,16 @@ public final Variant getVariant() {
         return variant;
     }
 
+    // -- BOOKMARKS
+
+    public List<Bookmark> bookmarks() {
+        return getDataTableModel().dataElementsObservable().getValue().stream()
+            .map(ManagedObject::getBookmark)
+            .filter(Optional::isPresent)
+            .map(Optional::get)
+            .toList();
+    }
+
     // -- LINKS PROVIDER
 
     /**
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/value/StandaloneValuePanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/value/StandaloneValuePanel.java
index 77febf2f2c2..eddf7dd6c1b 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/value/StandaloneValuePanel.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/value/StandaloneValuePanel.java
@@ -23,18 +23,19 @@
 import java.util.UUID;
 
 import org.apache.causeway.applib.services.bookmark.idstringifiers.PredefinedSerializables;
+import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.LocalResourcePath;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.MmValueUtils;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
 import org.apache.causeway.viewer.wicket.model.models.ValueModel;
-import org.apache.causeway.viewer.wicket.ui.components.attributes.AttributePanel;
+import org.apache.causeway.viewer.wicket.ui.components.attributes.blobclob.BlobAttributePanelFactory;
 import org.apache.causeway.viewer.wicket.ui.panels.PanelAbstract;
 import org.apache.causeway.viewer.wicket.ui.util.Wkt;
 
 /**
- * Panel for rendering any value types that do not have their own custom
- * {@link AttributePanel panel} to render them.
+ * Panel for rendering value types.
  */
 class StandaloneValuePanel
 extends PanelAbstract<ManagedObject, ValueModel> {
@@ -45,6 +46,13 @@ class StandaloneValuePanel
     public StandaloneValuePanel(final String id, final ValueModel valueModel) {
         super(id, valueModel);
 
+        //TODO do this properly using ComponentFactory lookup
+        if(valueModel.elementType().getCorrespondingClass().equals(Blob.class)) {
+        	Wkt.add(this, new BlobAttributePanelFactory()
+        			.createComponent(ID_STANDALONE_VALUE, UiAttributeWkt.pseudo(valueModel)));
+			return;
+        }
+
         //XXX StandaloneValuePanel has its limitations compared to the ScalarPanel infrastructure,
         // which has better rendering support
         // (we probably need to remove StandaloneValuePanel and utilize the AttributePanel for standalone values instead)
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/pages/standalonecollection/StandaloneCollectionPage.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/pages/standalonecollection/StandaloneCollectionPage.java
index 3df0ec9a864..0d875255ceb 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/pages/standalonecollection/StandaloneCollectionPage.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/pages/standalonecollection/StandaloneCollectionPage.java
@@ -19,10 +19,8 @@
 package org.apache.causeway.viewer.wicket.ui.pages.standalonecollection;
 
 import java.util.List;
-import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 import org.apache.wicket.Component;
 import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeInstantiation;
@@ -30,9 +28,8 @@
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.publishing.spi.PageRenderSubscriber;
 import org.apache.causeway.applib.services.user.UserMemento;
-import org.apache.causeway.commons.binding.Observable;
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.viewer.commons.model.components.UiComponentType;
 import org.apache.causeway.viewer.wicket.model.models.coll.CollectionModelStandalone;
 import org.apache.causeway.viewer.wicket.model.util.PageParameterUtils;
@@ -81,29 +78,9 @@ private void onRenderingOrRendered(
             final Can<PageRenderSubscriber> objectRenderSubscribers,
             final BiFunction<PageRenderSubscriber, Supplier<List<Bookmark>>, Void> handler) {
 
-        if(objectRenderSubscribers.isEmpty()) {
-            return;
-        }
-
-        Supplier<List<Bookmark>> listSupplier = () -> {
-
-            final var dataTableInteractive = collectionModel.getObject();
-            Observable<Can<ManagedObject>> dataElements = dataTableInteractive.dataElementsObservable();
-            Can<ManagedObject> managedObjects = dataElements.getValue();
-
-            final var bookmarks =
-                managedObjects.stream()
-                .map(ManagedObject::getBookmark)
-                .filter(Optional::isPresent)
-                .map(Optional::get)
-                .collect(Collectors.toUnmodifiableList());
-            return bookmarks;
-        };
-
+    	var bookmarksLazy = _Lazy.threadSafe(collectionModel::bookmarks);
         objectRenderSubscribers
-            .forEach(subscriber -> {
-                handler.apply(subscriber, listSupplier);
-            });
+            .forEach(subscriber -> handler.apply(subscriber, bookmarksLazy::get));
     }
 
 }