001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io;
007
008import java.util.ArrayDeque;
009import java.util.Deque;
010
011import dev.metaschema.core.util.ObjectUtils;
012import edu.umd.cs.findbugs.annotations.NonNull;
013import edu.umd.cs.findbugs.annotations.Nullable;
014
015/**
016 * A lightweight utility for tracking the current path during parsing.
017 * <p>
018 * This class maintains a stack of path segments that can be pushed and popped
019 * as the parser descends into and ascends from nested elements. The current
020 * path can be retrieved at any time as a formatted string.
021 * <p>
022 * Path format:
023 * <ul>
024 * <li>Empty stack: "/" (root)</li>
025 * <li>Single element: "/element"</li>
026 * <li>Nested elements: "/parent/child/grandchild"</li>
027 * </ul>
028 * <p>
029 * This class is not thread-safe and should be used within a single parsing
030 * context.
031 */
032public class PathTracker {
033  private final Deque<String> segments;
034
035  /**
036   * Construct a new empty path tracker.
037   */
038  public PathTracker() {
039    this.segments = new ArrayDeque<>();
040  }
041
042  /**
043   * Push a new segment onto the path.
044   *
045   * @param segment
046   *          the segment name to add, must not be null
047   */
048  public void push(@NonNull String segment) {
049    segments.push(segment);
050  }
051
052  /**
053   * Pop the most recent segment from the path.
054   *
055   * @return the removed segment, or null if the path was empty
056   */
057  @Nullable
058  public String pop() {
059    return segments.poll();
060  }
061
062  /**
063   * Get the current path as a formatted string.
064   * <p>
065   * Returns "/" for an empty path, or "/segment1/segment2/..." for nested paths.
066   *
067   * @return the current path string
068   */
069  @NonNull
070  public String getCurrentPath() {
071    if (segments.isEmpty()) {
072      return "/";
073    }
074    // Deque iterates from top (most recent) to bottom, so we need to reverse
075    StringBuilder sb = new StringBuilder();
076    // Convert to list and reverse to get correct order
077    Object[] arr = segments.toArray();
078    for (int i = arr.length - 1; i >= 0; i--) {
079      sb.append('/').append(arr[i]);
080    }
081    return ObjectUtils.notNull(sb.toString());
082  }
083
084  /**
085   * Get the depth of the current path (number of segments).
086   *
087   * @return the number of segments in the path
088   */
089  public int getDepth() {
090    return segments.size();
091  }
092
093  /**
094   * Check if the path is empty (at root level).
095   *
096   * @return true if the path has no segments
097   */
098  public boolean isEmpty() {
099    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}