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}