This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new dbbcf24368 Complex alias warning fix (#1391)
dbbcf24368 is described below
commit dbbcf24368d7967a50346b97497b067151c447e2
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Tue Jun 30 16:51:38 2026 +0530
Complex alias warning fix (#1391)
PROBLEM STATEMENT - Fixing the console warning on every startup.
-----------------
ModelViewEntity.populateReverseLinks() builds ModelConversion mappings
that drive the entity cache invalidation system. When a member entity
row changes (e.g. OrderItem), OFBiz uses these mappings to build a probe
key — a partial map of field→value pairs — and evicts any cached
view-entity entries that match it.
For simple aliases, the mapping is straightforward: memberField →
viewAliasName. For complex aliases (<complex-alias> in XML — expressions
like arithmetic, COALESCE, UPPER, aggregate functions like SUM/COUNT),
no mapping was registered. The alias stayed in the wildcards set, which
produced EntityOperator.WILDCARD in the probe key, meaning "match any
cached entry regardless of this field's value." This was safe (never
missed an eviction) but overly broad (evicted entries that did not
actually change).
A Debug.logWarning() fired for every such alias on every server startup
— noisy and not actionable.
2026-06-30 15:51:23,151 |main |ContainerLoader :151|I| Starting
container delegator-container
Admin socket configured on - /127.0.0.1:10523
2026-06-30 15:51:23,153 |delegator-startup-1 |DelegatorFactoryImpl
:33|I| Creating new delegator [default] (delegator-startup-1)
2026-06-30 15:51:23,405 |delegator-startup-1 |ModelViewEntity :674|W|
[TestingCryptoRawView]: Conversion for complex-alias needs to be
implemented for cache and in-memory eval stuff to work correctly, will
not work for alias: rawEncryptedValue
2026-06-30 15:51:23,405 |delegator-startup-1 |ModelViewEntity :674|W|
[TestingCryptoRawView]: Conversion for complex-alias needs to be
implemented for cache and in-memory eval stuff to work correctly, will
not work for alias: rawSaltedEncryptedValue
2026-06-30 15:51:23,437 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemQuantityReportGroupByItem]: Conversion for complex-alias needs
to be implemented for cache and in-memory eval stuff to work correctly,
will not work for alias: quantityOrdered
2026-06-30 15:51:23,437 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemQuantityReportGroupByItem]: Conversion for complex-alias needs
to be implemented for cache and in-memory eval stuff to work correctly,
will not work for alias: quantityOpen
2026-06-30 15:51:23,437 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemQuantityReportGroupByProduct]: Conversion for complex-alias
needs to be implemented for cache and in-memory eval stuff to work
correctly, will not work for alias: quantityOrdered
2026-06-30 15:51:23,437 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemQuantityReportGroupByProduct]: Conversion for complex-alias
needs to be implemented for cache and in-memory eval stuff to work
correctly, will not work for alias: quantityOpen
2026-06-30 15:51:23,438 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderReportSalesGroupByProduct]: Conversion for complex-alias needs to
be implemented for cache and in-memory eval stuff to work correctly,
will not work for alias: quantityOrdered
2026-06-30 15:51:23,438 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderReportSalesGroupByProduct]: Conversion for complex-alias needs to
be implemented for cache and in-memory eval stuff to work correctly,
will not work for alias: amount
2026-06-30 15:51:23,439 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemAndShipGrpInvResAndItemSum]: Conversion for complex-alias
needs to be implemented for cache and in-memory eval stuff to work
correctly, will not work for alias: quantityOrdered
2026-06-30 15:51:23,440 |delegator-startup-1 |ModelViewEntity :674|W|
[OrderItemAndShipGrpInvResAndItemSum]: Conversion for complex-alias
needs to be implemented for cache and in-memory eval stuff to work
correctly, will not work for alias: totQuantityAvailable
2026-06-30 15:51:23,442 |delegator-startup-1 |ModelViewEntity :674|W|
[ExampleStatusDetail]: Conversion for complex-alias needs to be
implemented for cache and in-memory eval stuff to work correctly, will
not work for alias: statusDelay
2026-06-30 15:51:23,444 |delegator-startup-1 |ModelViewEntity :674|W|
[ProjectPhaseTaskActualRatedHoursView]: Conversion for complex-alias
needs to be implemented for cache and in-memory eval stuff to work
correctly, will not work for alias: totalRatedHours
2026-06-30 15:51:23,458 |delegator-startup-1 |ModelReader :445|I|
Finished loading entities; #Entities=866 #ViewEntities=322 #Fields=8990
#Relationships=2993 #AutoRelationships=2192
2026-06-30 15:51:23,462 |delegator-startup-1 |GenericDelegator :239|I|
Doing entity definition check...
A patch was proposed that called addConversion(rawField, aliasName) for
every ComplexAliasField. The problem with that patch is that
ModelConversion.convert() copies the raw source field value directly
into the probe key, but the cache stores the computed alias value. These
two values differ whenever the expression is non-trivial:
- UPPER(P.firstName) → cache stores "JOHN", patch probes for "john" →
cache entry never evicted → stale data
- COALESCE(OI.qty, 0) → when qty is null, cache stores 0, patch probes
for null → stale data
- OI.unitPrice + OI.quantity → cache stores the computed sum, patch
probes for just the unit price → stale data
- SUM(OI.amount) → aggregate functions cannot be evaluated per row →
completely wrong probe value
The original WILDCARD behavior is safe: it evicts more than needed. The
proposed patch behavior is unsafe: it evicts less than needed and can
leave wrong data in the cache with no error.
WHAT WAS FIXED
---------------------------------------
File:
framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelViewEntity.java
The fix registers a conversion only for the one case where it is
provably correct: a ComplexAlias with exactly one ComplexAliasField
member that has no function and no defaultValue applied. In this case
the raw field value equals the computed alias value by identity, so the
conversion is safe. All other cases — multiple members (arithmetic),
transforming functions (UPPER, LOWER, SUM, etc.), COALESCE with a
default — remain in the wildcards set, giving the existing
broad-but-safe invalidation behavior.
Change 1 — populateReverseLinks(): Replaced the TODO comment +
Debug.logVerbose call with a dispatch to
alias.getComplexAliasMember().bindAliasToConversions(alias.getName(),
this). The log noise is eliminated and the safe subset now participates
in cache invalidation correctly.
Change 2 — ModelAlias.getComplexAliasMember(): New accessor added to
expose the ComplexAliasMember so populateReverseLinks() can dispatch to
it.
Change 3 — ComplexAliasMember interface: Added void
bindAliasToConversions(String aliasName, ModelViewEntity
modelViewEntity) as a new interface method, implemented by both concrete
classes.
Change 4 — ComplexAlias.bindAliasToConversions(): Contains the safety
gate. Delegates to the sole ComplexAliasField only when
complexAliasMembers.size() == 1, the sole member is a ComplexAliasField
instance, and sole.isPassThrough() returns true. All other cases are
silent no-ops, leaving the alias in wildcards.
Change 5 — ComplexAliasField.isPassThrough() and
ComplexAliasField.bindAliasToConversions(): isPassThrough() returns true
only when entityAlias is non-empty, field is non-empty, function is
empty, and defaultValue is empty. bindAliasToConversions() performs the
same getOrCreateModelConversion(entityAlias).addConversion(field,
aliasName) call that simple aliases use, and is only reachable through
the isPassThrough guard in ComplexAlias.
TEST CASES
----------
File:
framework/entity/src/test/java/org/apache/ofbiz/entity/model/ModelViewEntityComplexAliasTests.java
No additional test infrastructure files are required. ModelConversion is
a final non-static inner class of ModelViewEntity and cannot be mocked
with the standard Mockito subclass mock maker. The positive test
therefore avoids mocking ModelConversion entirely — it stubs
getOrCreateModelConversion() to throw a sentinel exception, which proves
the dispatch was reached and the correct entityAlias was passed, without
needing to observe the ModelConversion internals. The addConversion()
call is a trivial HashMap put and does not require a dedicated assertion
here.
The class is declared final (public final class
ModelViewEntityComplexAliasTests) to satisfy the OFBiz checkstyle
DesignForExtension rule on the setUp and tearDown lifecycle methods. All
test method names follow the OFBiz checkstyle pattern
^[a-z][a-zA-Z0-9]*$ — camelCase, no underscores.
11 tests in two groups, all passing. All three quality gates pass
cleanly: checkstyleTest, check + javadoc, codenarcMain + codenarcTest.
Group 1 — ComplexAliasField.isPassThrough() (6 tests):
isPassThroughPlainFieldReferenceReturnsTrue
Creates a ComplexAliasField with entityAlias="ME", field="myField", no
function, no defaultValue. Asserts isPassThrough() returns true.
isPassThroughWithFunctionReturnsFalse
Creates a ComplexAliasField with function="upper". Asserts
isPassThrough() returns false. A transforming function means rawField !=
computedAlias.
isPassThroughWithDefaultValueReturnsFalse
Creates a ComplexAliasField with defaultValue="0". Asserts
isPassThrough() returns false. COALESCE changes the value when the field
is null.
isPassThroughEmptyEntityAliasReturnsFalse
Creates a ComplexAliasField with an empty entityAlias. Asserts
isPassThrough() returns false. Cannot register a conversion without
knowing which member entity to look up.
isPassThroughEmptyFieldReturnsFalse
Creates a ComplexAliasField with an empty field name. Asserts
isPassThrough() returns false.
isPassThroughLiteralValueConstantReturnsFalse
Creates a ComplexAliasField with entityAlias="" and field="" but
value="LITERAL_VALUE" (a SQL literal constant, not a column reference).
Asserts isPassThrough() returns false.
Group 2 — ComplexAlias.bindAliasToConversions() safety gate (5 tests):
bindAliasToConversionsSinglePassThroughFieldRegistersConversion
[POSITIVE]
Creates a ComplexAlias with one pass-through ComplexAliasField
(ME.myField, no function, no default). Stubs
getOrCreateModelConversion() to throw a sentinel
UnsupportedOperationException. Calls bindAliasToConversions("myAlias",
mockViewEntity) and asserts the exception is thrown, confirming the
dispatch reached the registration code. Then verifies
getOrCreateModelConversion was called with entityAlias "ME", confirming
the correct member entity was targeted.
bindAliasToConversionsSingleFieldWithFunctionDoesNotRegister [NEGATIVE]
Creates a ComplexAlias with one ComplexAliasField that has
function="upper". Calls bindAliasToConversions. Verifies
getOrCreateModelConversion was never called — the alias stays as a
wildcard.
bindAliasToConversionsSingleFieldWithDefaultValueDoesNotRegister
[NEGATIVE]
Creates a ComplexAlias with one ComplexAliasField that has
defaultValue="0". Calls bindAliasToConversions. Verifies
getOrCreateModelConversion was never called.
bindAliasToConversionsMultipleMembersDoesNotRegister [NEGATIVE]
Creates a ComplexAlias with two ComplexAliasField members (unitPrice and
quantity, simulating an arithmetic expression like unitPrice +
quantity). Calls bindAliasToConversions. Verifies
getOrCreateModelConversion was never called — the computed sum cannot be
represented as a raw field value.
bindAliasToConversionsNestedComplexAliasAsSoleMemberDoesNotRegister
[NEGATIVE]
Creates an outer ComplexAlias whose sole member is another (inner)
ComplexAlias, not a ComplexAliasField. Calls bindAliasToConversions on
the outer alias. Verifies getOrCreateModelConversion was never called —
the instanceof ComplexAliasField check in the safety gate correctly
blocks this case.
---
.../apache/ofbiz/entity/model/ModelViewEntity.java | 37 ++++-
.../model/ModelViewEntityComplexAliasTests.java | 170 +++++++++++++++++++++
2 files changed, 204 insertions(+), 3 deletions(-)
diff --git
a/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelViewEntity.java
b/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelViewEntity.java
index 76dcfa3a34..5543310d85 100644
---
a/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelViewEntity.java
+++
b/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelViewEntity.java
@@ -670,9 +670,7 @@ public class ModelViewEntity extends ModelEntity {
while (it.hasNext()) {
ModelViewEntity.ModelAlias alias = it.next();
if (alias.isComplexAlias()) {
- // TODO: conversion for complex-alias needs to be implemented
for cache and in-memory eval stuff to work correctly
- Debug.logVerbose("[" + this.getEntityName() + "]: Conversion
for complex-alias needs to be implemented for cache and "
- + "in-memory eval stuff to work correctly, will not
work for alias: " + alias.getName(), MODULE);
+
alias.getComplexAliasMember().bindAliasToConversions(alias.getName(), this);
} else {
ModelConversion conversion =
getOrCreateModelConversion(alias.getEntityAlias());
conversion.addConversion(alias.getField(), alias.getName());
@@ -1060,6 +1058,10 @@ public class ModelViewEntity extends ModelEntity {
this.complexAliasMember = complexAliasMember;
}
+ public ComplexAliasMember getComplexAliasMember() {
+ return this.complexAliasMember;
+ }
+
public boolean isComplexAlias() {
return complexAliasMember != null;
}
@@ -1118,6 +1120,7 @@ public class ModelViewEntity extends ModelEntity {
public interface ComplexAliasMember extends Serializable {
void makeAliasColName(StringBuilder colNameBuffer, StringBuilder
fieldTypeBuffer, ModelViewEntity modelViewEntity, ModelReader modelReader);
+ void bindAliasToConversions(String aliasName, ModelViewEntity
modelViewEntity);
}
public static final class ComplexAlias implements ComplexAliasMember {
@@ -1149,6 +1152,18 @@ public class ModelViewEntity extends ModelEntity {
this.complexAliasMembers.addAll(complexAliasMembers);
}
+ @Override
+ public void bindAliasToConversions(String aliasName, ModelViewEntity
modelViewEntity) {
+ // Only safe for a single pass-through field — arithmetic across
multiple members
+ // or any transforming function makes rawField != computedAlias,
breaking cache probes.
+ if (complexAliasMembers.size() == 1 && complexAliasMembers.get(0)
instanceof ComplexAliasField) {
+ ComplexAliasField sole = (ComplexAliasField)
complexAliasMembers.get(0);
+ if (sole.isPassThrough()) {
+ sole.bindAliasToConversions(aliasName, modelViewEntity);
+ }
+ }
+ }
+
@Override
public void makeAliasColName(StringBuilder colNameBuffer,
StringBuilder fieldTypeBuffer, ModelViewEntity modelViewEntity,
ModelReader modelReader) {
@@ -1204,6 +1219,22 @@ public class ModelViewEntity extends ModelEntity {
this.value = value;
}
+ /** True when this is a plain column reference with no function or
default applied. */
+ public boolean isPassThrough() {
+ return UtilValidate.isNotEmpty(entityAlias)
+ && UtilValidate.isNotEmpty(field)
+ && UtilValidate.isEmpty(function)
+ && UtilValidate.isEmpty(defaultValue);
+ }
+
+ @Override
+ public void bindAliasToConversions(String aliasName, ModelViewEntity
modelViewEntity) {
+ if (UtilValidate.isNotEmpty(entityAlias) &&
UtilValidate.isNotEmpty(field)) {
+ ModelConversion conversion =
modelViewEntity.getOrCreateModelConversion(entityAlias);
+ conversion.addConversion(field, aliasName);
+ }
+ }
+
/**
* Make the alias as follows: function(coalesce(entityAlias.field,
defaultValue))
*/
diff --git
a/framework/entity/src/test/java/org/apache/ofbiz/entity/model/ModelViewEntityComplexAliasTests.java
b/framework/entity/src/test/java/org/apache/ofbiz/entity/model/ModelViewEntityComplexAliasTests.java
new file mode 100644
index 0000000000..20e71c9d95
--- /dev/null
+++
b/framework/entity/src/test/java/org/apache/ofbiz/entity/model/ModelViewEntityComplexAliasTests.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * 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.ofbiz.entity.model;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for the Tier-2 complex-alias conversion binding:
+ * a single-member, no-function, no-default ComplexAliasField is the only
+ * case where a ModelConversion entry is safe to register. All other shapes
+ * (arithmetic, functions, defaultValue, nested alias) must stay as wildcards.
+ */
+public final class ModelViewEntityComplexAliasTests {
+
+ @Mock
+ private ModelViewEntity mockViewEntity;
+
+ private AutoCloseable mocks;
+
+ @BeforeEach
+ void setUp() {
+ mocks = MockitoAnnotations.openMocks(this);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ mocks.close();
+ }
+
+ // ── ComplexAliasField.isPassThrough() ──────────────────────────────────
+
+ @Test
+ void isPassThroughPlainFieldReferenceReturnsTrue() {
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("ME", "myField", "", "");
+ assertTrue(field.isPassThrough());
+ }
+
+ @Test
+ void isPassThroughWithFunctionReturnsFalse() {
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("ME", "myField", "",
"upper");
+ assertFalse(field.isPassThrough());
+ }
+
+ @Test
+ void isPassThroughWithDefaultValueReturnsFalse() {
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("ME", "myField", "0",
"");
+ assertFalse(field.isPassThrough());
+ }
+
+ @Test
+ void isPassThroughEmptyEntityAliasReturnsFalse() {
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("", "myField", "", "");
+ assertFalse(field.isPassThrough());
+ }
+
+ @Test
+ void isPassThroughEmptyFieldReturnsFalse() {
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("ME", "", "", "");
+ assertFalse(field.isPassThrough());
+ }
+
+ @Test
+ void isPassThroughLiteralValueConstantReturnsFalse() {
+ // entityAlias and field are both empty — this is a SQL literal
constant, not a column ref
+ ModelViewEntity.ComplexAliasField field =
+ new ModelViewEntity.ComplexAliasField("", "", "", "",
"LITERAL_VALUE");
+ assertFalse(field.isPassThrough());
+ }
+
+ // ── ComplexAlias.bindAliasToConversions() — positive ───────────────────
+
+ @Test
+ void bindAliasToConversionsSinglePassThroughFieldRegistersConversion() {
+ // ModelConversion is a final inner class and cannot be mocked without
the inline mock maker.
+ // We verify the dispatch reached getOrCreateModelConversion with the
correct entityAlias,
+ // which is the key decision point. addConversion() is a trivial
HashMap put tested separately.
+ doThrow(new UnsupportedOperationException("reached conversion
registration"))
+ .when(mockViewEntity).getOrCreateModelConversion(anyString());
+
+ ModelViewEntity.ComplexAlias alias = new
ModelViewEntity.ComplexAlias("+");
+ alias.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "myField", "", ""));
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ alias.bindAliasToConversions("myAlias", mockViewEntity));
+
+ verify(mockViewEntity).getOrCreateModelConversion("ME");
+ }
+
+ // ── ComplexAlias.bindAliasToConversions() — negative (stays wildcard) ──
+
+ @Test
+ void bindAliasToConversionsSingleFieldWithFunctionDoesNotRegister() {
+ ModelViewEntity.ComplexAlias alias = new
ModelViewEntity.ComplexAlias("+");
+ alias.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "myField", "", "upper"));
+
+ alias.bindAliasToConversions("myAlias", mockViewEntity);
+
+ verify(mockViewEntity,
never()).getOrCreateModelConversion(anyString());
+ }
+
+ @Test
+ void bindAliasToConversionsSingleFieldWithDefaultValueDoesNotRegister() {
+ ModelViewEntity.ComplexAlias alias = new
ModelViewEntity.ComplexAlias("+");
+ alias.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "myField", "0", ""));
+
+ alias.bindAliasToConversions("myAlias", mockViewEntity);
+
+ verify(mockViewEntity,
never()).getOrCreateModelConversion(anyString());
+ }
+
+ @Test
+ void bindAliasToConversionsMultipleMembersDoesNotRegister() {
+ // Arithmetic across two fields — computed value != either raw field
value
+ ModelViewEntity.ComplexAlias alias = new
ModelViewEntity.ComplexAlias("+");
+ alias.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "unitPrice", "", ""));
+ alias.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "quantity", "", ""));
+
+ alias.bindAliasToConversions("totalAmount", mockViewEntity);
+
+ verify(mockViewEntity,
never()).getOrCreateModelConversion(anyString());
+ }
+
+ @Test
+ void bindAliasToConversionsNestedComplexAliasAsSoleMemberDoesNotRegister()
{
+ // Sole member is another ComplexAlias, not a ComplexAliasField
+ ModelViewEntity.ComplexAlias inner = new
ModelViewEntity.ComplexAlias("+");
+ inner.addComplexAliasMember(new
ModelViewEntity.ComplexAliasField("ME", "myField", "", ""));
+
+ ModelViewEntity.ComplexAlias outer = new
ModelViewEntity.ComplexAlias("+");
+ outer.addComplexAliasMember(inner);
+
+ outer.bindAliasToConversions("myAlias", mockViewEntity);
+
+ verify(mockViewEntity,
never()).getOrCreateModelConversion(anyString());
+ }
+}