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 }