Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.jspecify.annotations.Nullable;

Expand All @@ -43,12 +43,12 @@ public final class PathExpression implements Comparable<PathExpression> {
public static final String ROOT_EXPRESSION = "$";

private final List<Segment> segments;
private int cachedHashCode;
private String cachedExpression;
private PathExpression cachedParent;
private Map<String, PathExpression> cachedChildren;
private byte cachedHasWildcard; // 0=not computed, 1=false, 2=true
private byte cachedHasTypeSelector; // 0=not computed, 1=false, 2=true
private volatile int cachedHashCode;
private volatile String cachedExpression;
private volatile PathExpression cachedParent;
private final ConcurrentMap<String, PathExpression> cachedChildren = new ConcurrentHashMap<>(16);
private volatile byte cachedHasWildcard; // 0=not computed, 1=false, 2=true
private volatile byte cachedHasTypeSelector; // 0=not computed, 1=false, 2=true

private PathExpression(List<Segment> segments) {
this.segments = Collections.unmodifiableList(new ArrayList<>(segments));
Expand Down Expand Up @@ -204,26 +204,18 @@ private static Segment parseKeyValueUnion(String[] parts, String expression) {
public PathExpression child(String propertyName) {
Objects.requireNonNull(propertyName, "propertyName must not be null");

Map<String, PathExpression> children = cachedChildren;
if (children != null) {
PathExpression cached = children.get(propertyName);
if (cached != null) {
return cached;
}
PathExpression cached = cachedChildren.get(propertyName);
if (cached != null) {
return cached;
}

List<Segment> newSegments = new ArrayList<>(segments.size() + 1);
newSegments.addAll(segments);
newSegments.add(Segment.ofName(propertyName));
PathExpression result = new PathExpression(newSegments, true);

if (children == null) {
children = new HashMap<>();
cachedChildren = children;
}
children.put(propertyName, result);

return result;
PathExpression existing = cachedChildren.putIfAbsent(propertyName, result);
return existing != null ? existing : result;
}

public PathExpression index(int index) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ void cacheSubtree(
new ArrayList<>(children),
new HashMap<>(parentChildMap)
);
subtreeCache.put(jvmType, snapshot);
subtreeCache.putIfAbsent(jvmType, snapshot);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,14 @@ private void buildPathMappings() {
// Build paths by traversing the tree
buildPathsRecursive(rootNode, PathExpression.root(), paths, parents);

this.nodePaths = paths;
// IMPORTANT: nodePaths must be written LAST.
// It acts as the gate variable in ensurePathMappingsInitialized() —
// other threads skip the synchronized block when they see nodePaths != null.
// Writing childToParent before nodePaths ensures that the volatile write of
// nodePaths happens-after childToParent, so any thread that reads
// nodePaths != null is guaranteed to see childToParent != null by transitivity.
this.childToParent = parents;
this.nodePaths = paths;
}

private void buildPathsRecursive(
Expand Down
Loading