From d21221a7854b62674336998a9496359e7ea53981 Mon Sep 17 00:00:00 2001 From: rymsha Date: Fri, 10 May 2024 16:06:47 +0200 Subject: [PATCH] #10534 PropertyTree memory overuse --- .../xp/index/PatternIndexConfigDocument.java | 3 +- .../java/com/enonic/xp/node/NodeType.java | 2 +- .../xp/query/expr/OrderExpressions.java | 15 +- .../com/enonic/xp/util/BinaryReferences.java | 15 +- .../impl/node/dao/NodeVersionServiceImpl.java | 32 ++- .../impl/node/json/ImmutableNodeVersion.java | 36 +++ .../impl/node/json/ImmutableProperty.java | 198 ++++++++++++++++ .../impl/node/json/ImmutableVersionData.java | 216 ++++++++++++++++++ .../node/dao/NodeVersionServiceImplTest.java | 7 + 9 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableNodeVersion.java create mode 100644 modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableProperty.java create mode 100644 modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableVersionData.java diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/index/PatternIndexConfigDocument.java b/modules/core/core-api/src/main/java/com/enonic/xp/index/PatternIndexConfigDocument.java index 0c216c88adf..a1b9d540b51 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/index/PatternIndexConfigDocument.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/index/PatternIndexConfigDocument.java @@ -6,6 +6,7 @@ import java.util.SortedSet; import java.util.TreeSet; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.enonic.xp.annotation.PublicApi; @@ -35,7 +36,7 @@ private PatternIndexConfigDocument( final Builder builder ) { super( builder ); this.pathIndexConfigs = ImmutableSortedSet.copyOf( builder.pathIndexConfigs ); - this.pathIndexConfigMap = builder.stringPathIndexConfigMap; + this.pathIndexConfigMap = ImmutableMap.copyOf( builder.stringPathIndexConfigMap ); this.defaultConfig = builder.defaultConfig; this.allTextConfig = builder.allTextIndexConfig.build(); } diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/node/NodeType.java b/modules/core/core-api/src/main/java/com/enonic/xp/node/NodeType.java index 2c15f0f5ddb..4a5fe47dfd1 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/node/NodeType.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/node/NodeType.java @@ -18,7 +18,7 @@ private NodeType( final String name ) public static NodeType from( final String name ) { - return new NodeType( name ); + return DEFAULT_NODE_COLLECTION.name.equals( name ) ? DEFAULT_NODE_COLLECTION : new NodeType( name ); } public String getName() diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/query/expr/OrderExpressions.java b/modules/core/core-api/src/main/java/com/enonic/xp/query/expr/OrderExpressions.java index 2a67461f0b8..7984006638d 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/query/expr/OrderExpressions.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/query/expr/OrderExpressions.java @@ -11,6 +11,8 @@ public class OrderExpressions extends AbstractImmutableEntityList { + private static final OrderExpressions EMPTY = new OrderExpressions( ImmutableList.of() ); + private OrderExpressions( final ImmutableList list ) { super( list ); @@ -18,17 +20,22 @@ private OrderExpressions( final ImmutableList list ) public static OrderExpressions empty() { - return new OrderExpressions( ImmutableList.of() ); + return EMPTY; } public static OrderExpressions from( final OrderExpr... orderExprs ) { - return new OrderExpressions( ImmutableList.copyOf( orderExprs ) ); + return fromInternal( ImmutableList.copyOf( orderExprs ) ); } public static OrderExpressions from( final Collection orderExprs ) { - return new OrderExpressions( ImmutableList.copyOf( orderExprs ) ); + return fromInternal( ImmutableList.copyOf( orderExprs ) ); + } + + private static OrderExpressions fromInternal( final ImmutableList set ) + { + return set.isEmpty() ? EMPTY : new OrderExpressions( set ); } public static Builder create() @@ -48,7 +55,7 @@ public Builder add( final OrderExpr orderExpr ) public OrderExpressions build() { - return new OrderExpressions( this.orderExprs.build() ); + return fromInternal( orderExprs.build() ); } } diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/util/BinaryReferences.java b/modules/core/core-api/src/main/java/com/enonic/xp/util/BinaryReferences.java index 1bc08cc7e97..340f5d8a92a 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/util/BinaryReferences.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/util/BinaryReferences.java @@ -11,6 +11,8 @@ public class BinaryReferences extends AbstractImmutableEntitySet { + private static final BinaryReferences EMPTY = new BinaryReferences( ImmutableSet.of() ); + private BinaryReferences( final ImmutableSet set ) { super( set ); @@ -18,21 +20,26 @@ private BinaryReferences( final ImmutableSet set ) public static BinaryReferences empty() { - return new BinaryReferences( ImmutableSet.of() ); + return EMPTY; } public static BinaryReferences from( final String... binaryReferences ) { - return new BinaryReferences( Stream.of( binaryReferences ).map( BinaryReference::from ).collect( ImmutableSet.toImmutableSet() ) ); + return fromInternal( Stream.of( binaryReferences ).map( BinaryReference::from ).collect( ImmutableSet.toImmutableSet() ) ); } public static BinaryReferences from( final BinaryReference... binaryReferences ) { - return new BinaryReferences( ImmutableSet.copyOf( binaryReferences ) ); + return fromInternal( ImmutableSet.copyOf( binaryReferences ) ); } public static BinaryReferences from( final Iterable binaryReferences ) { - return new BinaryReferences( ImmutableSet.copyOf( binaryReferences ) ); + return fromInternal( ImmutableSet.copyOf( binaryReferences ) ); + } + + private static BinaryReferences fromInternal( final ImmutableSet set ) + { + return set.isEmpty() ? EMPTY : new BinaryReferences( set ); } } diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImpl.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImpl.java index c74e2ac1a20..413bb2a4916 100644 --- a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImpl.java +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImpl.java @@ -1,5 +1,6 @@ package com.enonic.xp.repo.impl.node.dao; +import java.util.List; import java.util.concurrent.ExecutionException; import org.osgi.service.component.annotations.Activate; @@ -18,11 +19,15 @@ import com.enonic.xp.blob.NodeVersionKey; import com.enonic.xp.blob.Segment; import com.enonic.xp.blob.SegmentLevel; +import com.enonic.xp.data.PropertyTree; import com.enonic.xp.index.IndexConfigDocument; import com.enonic.xp.node.NodeVersion; import com.enonic.xp.repo.impl.InternalContext; import com.enonic.xp.repo.impl.config.RepoConfiguration; import com.enonic.xp.repo.impl.node.NodeConstants; +import com.enonic.xp.repo.impl.node.json.ImmutableNodeVersion; +import com.enonic.xp.repo.impl.node.json.ImmutableProperty; +import com.enonic.xp.repo.impl.node.json.ImmutableVersionData; import com.enonic.xp.repo.impl.node.json.NodeVersionAccessControl; import com.enonic.xp.repo.impl.node.json.NodeVersionJsonSerializer; import com.enonic.xp.repository.RepositoryId; @@ -34,7 +39,7 @@ public class NodeVersionServiceImpl { private final BlobStore blobStore; - private final Cache nodeDataCache; + private final Cache nodeDataCache; private final Cache indexConfigCache; @@ -78,16 +83,19 @@ public NodeVersion get( final NodeVersionKey nodeVersionKey, final InternalConte try { - final NodeVersion nodeVersion = nodeDataCache.get( nodeBlobKey, () -> { + final ImmutableNodeVersion immutableNodeVersion = nodeDataCache.get( nodeBlobKey, () -> { final BlobRecord nodeBlobRecord = getBlobRecord( NodeConstants.NODE_SEGMENT_LEVEL, context.getRepositoryId(), nodeBlobKey ); - return NodeVersionJsonSerializer.toNodeVersionData( nodeBlobRecord.getBytes() ); + + try (var is = nodeBlobRecord.getBytes().openBufferedStream()) + { + return ImmutableVersionData.deserialize( is ); + } } ); final IndexConfigDocument indexConfigDocument = indexConfigCache.get( indexConfigBlobKey, () -> { final BlobRecord indexConfigBlobRecord = getBlobRecord( NodeConstants.INDEX_CONFIG_SEGMENT_LEVEL, context.getRepositoryId(), indexConfigBlobKey ); return NodeVersionJsonSerializer.toIndexConfigDocument( indexConfigBlobRecord.getBytes() ); - } ); final NodeVersionAccessControl accessControl = accessControlCache.get( accessControlBlobKey, () -> { @@ -96,8 +104,13 @@ public NodeVersion get( final NodeVersionKey nodeVersionKey, final InternalConte return NodeVersionJsonSerializer.toNodeVersionAccessControl( accessControlBlobRecord.getBytes() ); } ); - - return NodeVersion.create( nodeVersion ) + return NodeVersion.create() + .id( immutableNodeVersion.id ) + .nodeType( immutableNodeVersion.nodeType ) + .data( toPropertyTree( immutableNodeVersion.data ) ) + .childOrder( immutableNodeVersion.childOrder ) + .manualOrderValue( immutableNodeVersion.manualOrderValue ) + .attachedBinaries( immutableNodeVersion.attachedBinaries ) .indexConfigDocument( indexConfigDocument ) .permissions( accessControl.getPermissions() ) .build(); @@ -115,6 +128,13 @@ public NodeVersion get( final NodeVersionKey nodeVersionKey, final InternalConte } } + static PropertyTree toPropertyTree( final List data ) + { + final PropertyTree result = new PropertyTree(); + ImmutableProperty.addToSet( result.getRoot(), data ); + return result; + } + private BlobRecord getBlobRecord( SegmentLevel segmentLevel, RepositoryId repositoryId, BlobKey blobKey ) { final Segment nodeSegment = RepositorySegmentUtils.toSegment( repositoryId, segmentLevel ); diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableNodeVersion.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableNodeVersion.java new file mode 100644 index 00000000000..0fe7e695ab4 --- /dev/null +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableNodeVersion.java @@ -0,0 +1,36 @@ +package com.enonic.xp.repo.impl.node.json; + +import java.util.List; + +import com.enonic.xp.index.ChildOrder; +import com.enonic.xp.node.AttachedBinaries; +import com.enonic.xp.node.NodeId; +import com.enonic.xp.node.NodeType; + +public final class ImmutableNodeVersion +{ + public final NodeId id; + + public final NodeType nodeType; + + public final List data; + + public final ChildOrder childOrder; + + public final Long manualOrderValue; + + public final AttachedBinaries attachedBinaries; + + public ImmutableNodeVersion( final NodeId id, final NodeType nodeType, final List data, + final ChildOrder childOrder, + final Long manualOrderValue, + final AttachedBinaries attachedBinaries ) + { + this.id = id; + this.nodeType = nodeType; + this.data = List.copyOf( data ); + this.childOrder = childOrder; + this.manualOrderValue = manualOrderValue; + this.attachedBinaries = attachedBinaries; + } +} diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableProperty.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableProperty.java new file mode 100644 index 00000000000..ad8552e5b2c --- /dev/null +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableProperty.java @@ -0,0 +1,198 @@ +package com.enonic.xp.repo.impl.node.json; + +import java.util.List; + +import com.enonic.xp.data.PropertySet; +import com.enonic.xp.data.Value; + +public abstract class ImmutableProperty +{ + /** + * ValueSet with null is a special case when PropertySet is null. + */ + private static final ValueSet NULL_VALUE_SET = new ValueSetN(); + + private static final ValueSet EMPTY_VALUE_SET = new ValueSetN( List.of() ); + + final String name; + + private ImmutableProperty( final String name ) + { + this.name = name; + } + + abstract void addToSet( String name, PropertySet set ); + + public static void addToSet( final PropertySet set, final List data ) + { + for ( ImmutableProperty datum : data ) + { + datum.addToSet( datum.name, set ); + } + } + + static ImmutableProperty ofValue( final String name, final List values ) + { + return values.size() == 1 + ? new ImmutablePropertyValue1( name, values.get( 0 ) ) + : new ImmutablePropertyValueN( name, values ); + } + + static ImmutableProperty ofValueSet( final String name, final List values ) + { + return values.size() == 1 + ? new ImmutablePropertyValueSet1( name, values.get( 0 ) ) + : new ImmutablePropertyValueSetN( name, values ); + } + + static ValueSet nullValueSet() + { + return NULL_VALUE_SET; + } + + static ValueSet toValueSet( final List set ) + { + final int size = set.size(); + switch ( size ) + { + case 0: + return EMPTY_VALUE_SET; + case 1: + return new ValueSet1( set.get( 0 ) ); + default: + return new ValueSetN( set ); + } + } + + abstract static class ValueSet + { + public abstract void addValueSet( String name, PropertySet to ); + } + + private static class ImmutablePropertyValueSetN + extends ImmutableProperty + { + List values; + + ImmutablePropertyValueSetN( final String name, final List values ) + { + super( name ); + this.values = List.copyOf( values ); + } + + public void addToSet( String name, PropertySet set ) + { + for ( var value : values ) + { + value.addValueSet( name, set ); + } + } + } + + private static class ImmutablePropertyValueSet1 + extends ImmutableProperty + { + ValueSet value; + + ImmutablePropertyValueSet1( final String name, ValueSet value ) + { + super( name ); + this.value = value; + } + + public void addToSet( String name, PropertySet set ) + { + value.addValueSet( name, set ); + } + } + + private static class ImmutablePropertyValueN + extends ImmutableProperty + { + final List values; + + ImmutablePropertyValueN( final String name, final List values ) + { + super( name ); + this.values = List.copyOf( values ); + } + + public void addToSet( String name, PropertySet set ) + { + for ( Value value : values ) + { + set.addProperty( name, value ); + } + } + } + + private static class ImmutablePropertyValue1 + extends ImmutableProperty + { + final Value value; + + ImmutablePropertyValue1( final String name, final Value value ) + { + super( name ); + this.value = value; + } + + public void addToSet( String name, PropertySet set ) + { + set.addProperty( name, value ); + } + } + + private static class ValueSet1 + extends ValueSet + { + final ImmutableProperty single; + + ValueSet1( final ImmutableProperty set ) + { + this.single = set; + } + + public void addValueSet( final String name, final PropertySet to ) + { + final PropertySet propertySet = to.getTree().newSet(); + single.addToSet( single.name, propertySet ); + to.addSet( name, propertySet ); + } + } + + private static class ValueSetN + extends ValueSet + { + public final List set; + + /** + * Used for NULL_VALUE_SET only. + * This avoids null checks in the main constructor. + */ + private ValueSetN() + { + this.set = null; + } + + ValueSetN( final List set ) + { + this.set = List.copyOf( set ); + } + + public void addValueSet( final String name, final PropertySet to ) + { + if ( this.set == null ) + { + to.addSet( name, null ); + } + else + { + final PropertySet propertySet = to.getTree().newSet(); + ImmutableProperty.addToSet( propertySet, this.set ); + to.addSet( name, propertySet ); + } + } + } +} + diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableVersionData.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableVersionData.java new file mode 100644 index 00000000000..6f46929ce97 --- /dev/null +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/node/json/ImmutableVersionData.java @@ -0,0 +1,216 @@ +package com.enonic.xp.repo.impl.node.json; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import com.enonic.xp.data.ValueType; +import com.enonic.xp.data.ValueTypes; +import com.enonic.xp.index.ChildOrder; +import com.enonic.xp.json.ObjectMapperHelper; +import com.enonic.xp.node.AttachedBinaries; +import com.enonic.xp.node.AttachedBinary; +import com.enonic.xp.node.NodeId; +import com.enonic.xp.node.NodeType; +import com.enonic.xp.util.BinaryReference; + +public final class ImmutableVersionData +{ + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperHelper.create(); + + static + { + OBJECT_MAPPER.addMixIn( ImmutableNodeVersion.class, ImmutableNodeVersionMixin.class ); + OBJECT_MAPPER.addMixIn( AttachedBinary.class, AttachedBinaryMixin.class ); + } + + private ImmutableVersionData() + { + } + + public static ImmutableNodeVersion deserialize( final InputStream is ) + throws IOException + { + return OBJECT_MAPPER.readValue( is, ImmutableNodeVersion.class ); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private abstract static class ImmutableNodeVersionMixin + { + @JsonCreator + ImmutableNodeVersionMixin( @JsonProperty("id") @JsonDeserialize(using = NodeIdDeserializer.class) final NodeId id, + @JsonProperty("nodeType") @JsonDeserialize(using = NodeTypeDeserializer.class) final NodeType nodeType, + @JsonProperty("data") @JsonDeserialize(contentUsing = ImmutablePropertyArrayDeserializer.class) final List data, + @JsonProperty("childOrder") @JsonDeserialize(using = ChildOrderDeserializer.class) final ChildOrder childOrder, + @JsonProperty("manualOrderValue") final Long manualOrderValue, + @JsonProperty("attachedBinaries") @JsonDeserialize(using = AttachedBinariesDeserializer.class) final AttachedBinaries attachedBinaries ) + { + } + } + + private static class ImmutablePropertyArrayInner + { + @JsonProperty("name") + String name; + + @JsonProperty("type") + @JsonDeserialize(using = ValueTypeDeserializer.class) + ValueType type; + + @JsonProperty("values") + @JsonDeserialize(contentUsing = ValueInnerDeserializer.class) + List values; + } + + private static class AttachedBinaryMixin + { + AttachedBinaryMixin( @JsonProperty("binaryReference") + @JsonDeserialize(using = BinaryReferenceDeserializer.class) + final BinaryReference binaryReference, + @JsonProperty("blobKey") + final String blobKey ) + { + } + } + + private static class ValueInner + { + @JsonProperty("v") + Object v; + + @JsonProperty("set") + List set; + } + + private static class ValueInnerDeserializer + extends JsonDeserializer + { + @Override + public ValueInner deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return jsonParser.readValueAs( ValueInner.class ); + } + } + + private static class ImmutablePropertyArrayDeserializer + extends JsonDeserializer + { + + @Override + public ImmutableProperty deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return ImmutableVersionData.convert( jsonParser.readValueAs( ImmutablePropertyArrayInner.class ) ) ; + } + } + + private static ImmutableProperty convert( final ImmutablePropertyArrayInner internal ) + { + if ( internal.type.equals( ValueTypes.PROPERTY_SET ) ) + { + return ImmutableProperty.ofValueSet( internal.name, internal.values.stream() + .map( value -> value.set == null + ? ImmutableProperty.nullValueSet() + : ImmutableProperty.toValueSet( + value.set.stream().map( ImmutableVersionData::convert ).collect( Collectors.toUnmodifiableList() ) ) ) + .collect( Collectors.toUnmodifiableList() ) ); + } + else + { + return ImmutableProperty.ofValue( internal.name, internal.values.stream() + .map( valueInner -> internal.type.fromJsonValue( valueInner.v ) ) + .collect( Collectors.toUnmodifiableList() ) ); + } + } + + private static class NodeIdDeserializer + extends JsonDeserializer + { + @Override + public NodeId deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return NodeId.from( jsonParser.getText() ); + } + } + + private static class NodeTypeDeserializer + extends JsonDeserializer + { + + @Override + public NodeType deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return NodeType.from( jsonParser.getText() ); + } + } + + private static class ChildOrderDeserializer + extends JsonDeserializer + { + @Override + public ChildOrder deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return ChildOrder.from( jsonParser.getText() ); + } + } + + private static class ValueTypeDeserializer + extends JsonDeserializer + { + @Override + public ValueType deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return ValueTypes.getByName( jsonParser.getText() ); + } + } + + private static class BinaryReferenceDeserializer + extends JsonDeserializer + { + @Override + public BinaryReference deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + return BinaryReference.from( jsonParser.getText() ); + } + } + + private static class AttachedBinariesDeserializer + extends JsonDeserializer + { + + static final TypeReference> VALUE_TYPE_REF = new TypeReference<>() + { + }; + + @Override + public AttachedBinaries deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) + throws IOException + { + final AttachedBinaries.Builder builder = AttachedBinaries.create(); + final List list = jsonParser.readValueAs( VALUE_TYPE_REF ); + for ( final AttachedBinary entry : list ) + { + builder.add( entry ); + } + + return builder.build(); + } + } +} diff --git a/modules/core/core-repo/src/test/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImplTest.java b/modules/core/core-repo/src/test/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImplTest.java index c453cde3990..b90d0f9610c 100644 --- a/modules/core/core-repo/src/test/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImplTest.java +++ b/modules/core/core-repo/src/test/java/com/enonic/xp/repo/impl/node/dao/NodeVersionServiceImplTest.java @@ -15,6 +15,7 @@ import com.enonic.xp.blob.SegmentLevel; import com.enonic.xp.context.Context; import com.enonic.xp.context.ContextAccessor; +import com.enonic.xp.data.PropertySet; import com.enonic.xp.data.PropertyTree; import com.enonic.xp.index.ChildOrder; import com.enonic.xp.index.IndexConfig; @@ -83,6 +84,12 @@ void getVersion() { final PropertyTree data = new PropertyTree(); data.addString( "myName", "myValue" ); + final PropertySet set = data.newSet(); + set.setString( "myNameInSet", "myValueInSet" ); + data.addSet( "mySet", set ); + data.addSet( "myEmptySet", data.newSet() ); + data.addSet( "myNullSet", null ); + data.addString( "myNullString", null ); final NodeVersion nodeVersion = NodeVersion.create(). nodeType( NodeType.DEFAULT_NODE_COLLECTION ).