ACS-11644 Optionally Preserve unset properties upon new version creation (#4078) (#4096)

This commit is contained in:
Eva Vasques
2026-05-21 13:09:00 +01:00
committed by GitHub
parent e41ff45028
commit 12e6799082
5 changed files with 149 additions and 7 deletions
+2 -2
View File
@@ -1250,7 +1250,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false,
"line_number": 779,
"line_number": 784,
"is_secret": false
}
],
@@ -1845,5 +1845,5 @@
}
]
},
"generated_at": "2026-05-07T12:42:51Z"
"generated_at": "2026-05-12T12:14:02Z"
}
@@ -115,6 +115,8 @@ public abstract class VersionServiceImpl extends AbstractVersionServiceImpl impl
protected Comparator<Version> versionComparatorDesc;
protected boolean preserveUnsetProperties;
/**
* Sets the db node service, used as the version store implementation
*
@@ -164,13 +166,21 @@ public abstract class VersionServiceImpl extends AbstractVersionServiceImpl impl
catch (Exception e)
{
throw new AlfrescoRuntimeException(
"Failed to create a Comparator<Version> using the class name " +
versionComparatorClass,
e);
"Failed to create a Comparator<Version> using the class name " + versionComparatorClass, e);
}
}
}
public void setPreserveUnsetProperties(boolean preserveUnsetProperties)
{
this.preserveUnsetProperties = preserveUnsetProperties;
}
public boolean isPreserveUnsetProperties()
{
return preserveUnsetProperties;
}
/**
* Register version label policy for the specified type
*
@@ -1400,10 +1410,14 @@ public abstract class VersionServiceImpl extends AbstractVersionServiceImpl impl
{
// Copy the properties (along with their aspect)
Map<QName, PropertyDefinition> propertyDefinitions = classDefinition.getProperties();
Map<QName, Serializable> sourceProperties = this.nodeService.getProperties(nodeRef);
for (QName propertyName : propertyDefinitions.keySet())
{
Serializable propValue = this.nodeService.getProperty(nodeRef, propertyName);
nodeDetails.addProperty(classRef, propertyName, propValue);
Serializable propValue = sourceProperties.get(propertyName);
if (sourceProperties.containsKey(propertyName) || !isPreserveUnsetProperties())
{
nodeDetails.addProperty(classRef, propertyName, propValue);
}
}
// Also copy the aspect with no properties in its definition
if (classDefinition.isAspect() && !nodeDetails.getAspects().contains(classRef))
@@ -506,6 +506,9 @@
<property name="useVersionAssocIndex">
<value>${version.store.useVersionAssocIndex}</value>
</property>
<property name="preserveUnsetProperties">
<value>${version.store.preserveUnsetProperties}</value>
</property>
</bean>
<bean id="versionNodeService" class="org.alfresco.repo.version.Node2ServiceImpl">
@@ -399,6 +399,11 @@ version.store.versionComparatorClass=
# Please, see MNT-22715 for details.
version.store.useVersionAssocIndex=false
# Optional to preserve unset properties when creating a new version. With this setting enabled,
# we no longer set all optional properties as null on the version node and preserve the unset
# properties from the source instead.
version.store.preserveUnsetProperties=false
# Folders for storing people
system.system_container.childname=sys:system
system.people_container.childname=sys:people
@@ -3224,4 +3224,124 @@ public class VersionServiceImplTest extends BaseVersionStoreTest
return null;
}
}
/**
* With preserveUnsetProperties disabled (default), a type property that was never set on the source node (and has no model default) is still written to the version node with a null value.
*/
@Test
public void testPreserveUnsetPropertiesDisabled()
{
// MULTI_PROP has no <default> in the model so it is absent when not set
NodeRef node = createNodeWithoutMultiProp();
Version version = this.versionService.createVersion(node, this.versionProperties);
Map<QName, Serializable> frozenProps = dbNodeService.getProperties(VersionUtil.convertNodeRef(version.getFrozenStateNodeRef()));
assertTrue("PROP_1 should be in the version node", frozenProps.containsKey(PROP_1));
assertEquals("PROP_1 value should match source", VALUE_1, frozenProps.get(PROP_1));
assertTrue("Unset MULTI_PROP should be present in the version node with null value when preserveUnsetProperties is disabled", frozenProps.containsKey(MULTI_PROP));
assertNull("MULTI_PROP value should be null in the version node", frozenProps.get(MULTI_PROP));
}
/**
* With preserveUnsetProperties enabled, a type property that was never set on the source node (and has no model default) is omitted from the version node entirely.
*/
@Test
public void testPreserveUnsetPropertiesEnabled()
{
VersionServiceImpl versionServiceImpl = (VersionServiceImpl) versionService;
versionServiceImpl.setPreserveUnsetProperties(true);
try
{
NodeRef node = createNodeWithoutMultiProp();
Version version = this.versionService.createVersion(node, this.versionProperties);
Map<QName, Serializable> frozenProps = dbNodeService.getProperties(VersionUtil.convertNodeRef(version.getFrozenStateNodeRef()));
assertTrue("PROP_1 should be in the version node", frozenProps.containsKey(PROP_1));
assertEquals("PROP_1 value should match source", VALUE_1, frozenProps.get(PROP_1));
assertFalse("Unset MULTI_PROP should be absent from the version node when preserveUnsetProperties is enabled", frozenProps.containsKey(MULTI_PROP));
}
finally
{
versionServiceImpl.setPreserveUnsetProperties(false);
}
}
/**
* With preserveUnsetProperties enabled, a property that IS explicitly set on the source node is still copied to the version node.
*/
@Test
public void testPreserveUnsetPropertiesEnabledCopiesSetProperties()
{
VersionServiceImpl versionServiceImpl = (VersionServiceImpl) versionService;
versionServiceImpl.setPreserveUnsetProperties(true);
try
{
NodeRef node = createNodeWithoutMultiProp();
dbNodeService.setProperty(node, MULTI_PROP, (Serializable) multiValue);
Version version = this.versionService.createVersion(node, this.versionProperties);
Map<QName, Serializable> frozenProps = dbNodeService.getProperties(VersionUtil.convertNodeRef(version.getFrozenStateNodeRef()));
assertTrue("MULTI_PROP should be in the version node when explicitly set on source", frozenProps.containsKey(MULTI_PROP));
assertEquals("MULTI_PROP value should match source", multiValue, frozenProps.get(MULTI_PROP));
}
finally
{
versionServiceImpl.setPreserveUnsetProperties(false);
}
}
/**
* Reverting a version must not set null for properties that were never set on the source node when preserveUnsetProperties is enabled.
*/
@Test
public void testPreserveUnsetPropertiesEnabledRevertDoesNotWriteNullForUnsetProperties()
{
VersionServiceImpl versionServiceImpl = (VersionServiceImpl) versionService;
versionServiceImpl.setPreserveUnsetProperties(true);
try
{
// Create a node where MULTI_PROP was never set (no model default either)
NodeRef node = createNodeWithoutMultiProp();
// Version 1: MULTI_PROP is absent
Version version1 = this.versionService.createVersion(node, this.versionProperties);
// Modify the source node so it differs from version 1
this.dbNodeService.setProperty(node, PROP_1, UPDATED_VALUE_1);
// Revert back to version 1
this.versionService.revert(node, version1);
// MULTI_PROP must remain truly absent — not present with a null value — after the revert
Map<QName, Serializable> propsAfterRevert = dbNodeService.getProperties(node);
assertFalse(
"MULTI_PROP must not be written back as null when reverting to a version where it was never set and preserveUnsetProperties is enabled",
propsAfterRevert.containsKey(MULTI_PROP));
}
finally
{
versionServiceImpl.setPreserveUnsetProperties(false);
}
}
/**
* Creates a versionable node with PROP_1 set but MULTI_PROP never set. MULTI_PROP has no model default so it will not be present when doing getProperties().
*/
private NodeRef createNodeWithoutMultiProp()
{
Map<QName, Serializable> props = new HashMap<>();
props.put(PROP_1, VALUE_1);
props.put(ContentModel.PROP_CONTENT, new ContentData(null, "text/plain", 0L, "UTF-8"));
NodeRef node = dbNodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName("{test}preserveUnsetPropsNode"),
TEST_TYPE_QNAME,
props).getChildRef();
dbNodeService.addAspect(node, ContentModel.ASPECT_VERSIONABLE, new HashMap<>());
return node;
}
}