@@ -27,10 +27,8 @@ public final class JsonPath {
2727 private static final Logger LOG = Logger .getLogger (JsonPath .class .getName ());
2828
2929 private final JsonPathAst .Root ast ;
30- private final String pathExpression ;
3130
32- private JsonPath (String pathExpression , JsonPathAst .Root ast ) {
33- this .pathExpression = pathExpression ;
31+ private JsonPath (JsonPathAst .Root ast ) {
3432 this .ast = ast ;
3533 }
3634
@@ -43,7 +41,7 @@ public static JsonPath parse(String path) {
4341 Objects .requireNonNull (path , "path must not be null" );
4442 LOG .fine (() -> "Parsing path: " + path );
4543 final var ast = JsonPathParser .parse (path );
46- return new JsonPath (path , ast );
44+ return new JsonPath (ast );
4745 }
4846
4947 /// Selects matching values from a JSON document.
@@ -66,20 +64,26 @@ public List<JsonValue> select(JsonValue json) {
6664 /// @throws NullPointerException if json is null
6765 public List <JsonValue > query (JsonValue json ) {
6866 Objects .requireNonNull (json , "json must not be null" );
69- LOG .fine (() -> "Querying document with path: " + pathExpression );
67+ LOG .fine (() -> "Querying document with path: " + this );
7068 return evaluate (ast , json );
7169 }
7270
73- /// Returns the original path expression.
74- public String expression () {
75- return pathExpression ;
71+ /// Reconstructs the JsonPath expression from the AST.
72+ @ Override
73+ public String toString () {
74+ return reconstruct (ast );
7675 }
7776
7877 /// Returns the parsed AST.
7978 public JsonPathAst .Root ast () {
8079 return ast ;
8180 }
8281
82+ /// Returns the original path expression.
83+ public String expression () {
84+ return "Todo" ;
85+ }
86+
8387 /// Evaluates a compiled JsonPath against a JSON document.
8488 /// @param path a compiled JsonPath (typically cached)
8589 /// @param json the JSON document to query
@@ -473,4 +477,140 @@ private static void evaluateScriptExpression(
473477 }
474478 }
475479 }
480+
481+ private static String reconstruct (JsonPathAst .Root root ) {
482+ final var sb = new StringBuilder ("$" );
483+ for (final var segment : root .segments ()) {
484+ appendSegment (sb , segment );
485+ }
486+ return sb .toString ();
487+ }
488+
489+ private static void appendSegment (StringBuilder sb , JsonPathAst .Segment segment ) {
490+ switch (segment ) {
491+ case JsonPathAst .PropertyAccess prop -> {
492+ if (isSimpleIdentifier (prop .name ())) {
493+ sb .append ("." ).append (prop .name ());
494+ } else {
495+ sb .append ("['" ).append (escape (prop .name ())).append ("']" );
496+ }
497+ }
498+ case JsonPathAst .ArrayIndex arr -> sb .append ("[" ).append (arr .index ()).append ("]" );
499+ case JsonPathAst .ArraySlice slice -> {
500+ sb .append ("[" );
501+ if (slice .start () != null ) sb .append (slice .start ());
502+ sb .append (":" );
503+ if (slice .end () != null ) sb .append (slice .end ());
504+ if (slice .step () != null ) sb .append (":" ).append (slice .step ());
505+ sb .append ("]" );
506+ }
507+ case JsonPathAst .Wildcard w -> sb .append (".*" );
508+ case JsonPathAst .RecursiveDescent desc -> {
509+ sb .append (".." );
510+ // RecursiveDescent target is usually PropertyAccess or Wildcard,
511+ // but can be other things in theory.
512+ // If target is PropertyAccess("foo"), append "foo".
513+ // If target is Wildcard, append "*".
514+ // Our AST structure wraps the target segment.
515+ // We need to handle how it's appended.
516+ // appendSegment prepends "." or "[" usually.
517+ // But ".." replaces the dot.
518+ // Let's special case the target printing.
519+ appendRecursiveTarget (sb , desc .target ());
520+ }
521+ case JsonPathAst .Filter filter -> {
522+ sb .append ("[?(" );
523+ appendFilterExpression (sb , filter .expression ());
524+ sb .append (")]" );
525+ }
526+ case JsonPathAst .Union union -> {
527+ sb .append ("[" );
528+ final var selectors = union .selectors ();
529+ for (int i = 0 ; i < selectors .size (); i ++) {
530+ if (i > 0 ) sb .append ("," );
531+ appendUnionSelector (sb , selectors .get (i ));
532+ }
533+ sb .append ("]" );
534+ }
535+ case JsonPathAst .ScriptExpression script -> sb .append ("[(" ).append (script .script ()).append (")]" );
536+ }
537+ }
538+
539+ private static void appendRecursiveTarget (StringBuilder sb , JsonPathAst .Segment target ) {
540+ if (target instanceof JsonPathAst .PropertyAccess prop ) {
541+ sb .append (prop .name ()); // ..name
542+ } else if (target instanceof JsonPathAst .Wildcard ) {
543+ sb .append ("*" ); // ..*
544+ } else {
545+ // Fallback for other types if they ever occur in recursive position
546+ appendSegment (sb , target );
547+ }
548+ }
549+
550+ private static void appendUnionSelector (StringBuilder sb , JsonPathAst .Segment selector ) {
551+ if (selector instanceof JsonPathAst .PropertyAccess prop ) {
552+ sb .append ("'" ).append (escape (prop .name ())).append ("'" );
553+ } else if (selector instanceof JsonPathAst .ArrayIndex arr ) {
554+ sb .append (arr .index ());
555+ } else {
556+ // Fallback
557+ appendSegment (sb , selector );
558+ }
559+ }
560+
561+ private static void appendFilterExpression (StringBuilder sb , JsonPathAst .FilterExpression expr ) {
562+ switch (expr ) {
563+ case JsonPathAst .ExistsFilter exists -> {
564+ appendFilterExpression (sb , exists .path ()); // Should print the path
565+ }
566+ case JsonPathAst .ComparisonFilter comp -> {
567+ appendFilterExpression (sb , comp .left ());
568+ sb .append (comp .op ().symbol ());
569+ appendFilterExpression (sb , comp .right ());
570+ }
571+ case JsonPathAst .LogicalFilter logical -> {
572+ if (logical .op () == JsonPathAst .LogicalOp .NOT ) {
573+ sb .append ("!" );
574+ appendFilterExpression (sb , logical .left ());
575+ } else {
576+ sb .append ("(" );
577+ appendFilterExpression (sb , logical .left ());
578+ sb .append (" " ).append (logical .op ().symbol ()).append (" " );
579+ appendFilterExpression (sb , logical .right ());
580+ sb .append (")" );
581+ }
582+ }
583+ case JsonPathAst .CurrentNode cn -> sb .append ("@" );
584+ case JsonPathAst .PropertyPath path -> {
585+ sb .append ("@" );
586+ for (String p : path .properties ()) {
587+ if (isSimpleIdentifier (p )) {
588+ sb .append ("." ).append (p );
589+ } else {
590+ sb .append ("['" ).append (escape (p )).append ("']" );
591+ }
592+ }
593+ }
594+ case JsonPathAst .LiteralValue lit -> {
595+ if (lit .value () instanceof String s ) {
596+ sb .append ("'" ).append (escape (s )).append ("'" );
597+ } else {
598+ sb .append (lit .value ());
599+ }
600+ }
601+ }
602+ }
603+
604+ private static boolean isSimpleIdentifier (String name ) {
605+ if (name .isEmpty ()) return false ;
606+ if (!Character .isJavaIdentifierStart (name .charAt (0 ))) return false ;
607+ for (int i = 1 ; i < name .length (); i ++) {
608+ if (!Character .isJavaIdentifierPart (name .charAt (i ))) return false ;
609+ }
610+ return true ;
611+ }
612+
613+ private static String escape (String s ) {
614+ return s .replace ("'" , "\\ '" );
615+ }
476616}
0 commit comments