1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package dev.metaschema.databind.io;
7
8 import java.util.ArrayDeque;
9 import java.util.Deque;
10
11 import dev.metaschema.core.util.ObjectUtils;
12 import edu.umd.cs.findbugs.annotations.NonNull;
13 import edu.umd.cs.findbugs.annotations.Nullable;
14
15 /**
16 * A lightweight utility for tracking the current path during parsing.
17 * <p>
18 * This class maintains a stack of path segments that can be pushed and popped
19 * as the parser descends into and ascends from nested elements. The current
20 * path can be retrieved at any time as a formatted string.
21 * <p>
22 * Path format:
23 * <ul>
24 * <li>Empty stack: "/" (root)</li>
25 * <li>Single element: "/element"</li>
26 * <li>Nested elements: "/parent/child/grandchild"</li>
27 * </ul>
28 * <p>
29 * This class is not thread-safe and should be used within a single parsing
30 * context.
31 */
32 public class PathTracker {
33 private final Deque<String> segments;
34
35 /**
36 * Construct a new empty path tracker.
37 */
38 public PathTracker() {
39 this.segments = new ArrayDeque<>();
40 }
41
42 /**
43 * Push a new segment onto the path.
44 *
45 * @param segment
46 * the segment name to add, must not be null
47 */
48 public void push(@NonNull String segment) {
49 segments.push(segment);
50 }
51
52 /**
53 * Pop the most recent segment from the path.
54 *
55 * @return the removed segment, or null if the path was empty
56 */
57 @Nullable
58 public String pop() {
59 return segments.poll();
60 }
61
62 /**
63 * Get the current path as a formatted string.
64 * <p>
65 * Returns "/" for an empty path, or "/segment1/segment2/..." for nested paths.
66 *
67 * @return the current path string
68 */
69 @NonNull
70 public String getCurrentPath() {
71 if (segments.isEmpty()) {
72 return "/";
73 }
74 // Deque iterates from top (most recent) to bottom, so we need to reverse
75 StringBuilder sb = new StringBuilder();
76 // Convert to list and reverse to get correct order
77 Object[] arr = segments.toArray();
78 for (int i = arr.length - 1; i >= 0; i--) {
79 sb.append('/').append(arr[i]);
80 }
81 return ObjectUtils.notNull(sb.toString());
82 }
83
84 /**
85 * Get the depth of the current path (number of segments).
86 *
87 * @return the number of segments in the path
88 */
89 public int getDepth() {
90 return segments.size();
91 }
92
93 /**
94 * Check if the path is empty (at root level).
95 *
96 * @return true if the path has no segments
97 */
98 public boolean isEmpty() {
99 return segments.isEmpty();
100 }
101
102 /**
103 * Clear all segments from the path.
104 */
105 public void clear() {
106 segments.clear();
107 }
108
109 @Override
110 public String toString() {
111 return getCurrentPath();
112 }
113 }