1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.io;
7   
8   import java.net.URI;
9   
10  import dev.metaschema.core.model.IResourceLocation;
11  import dev.metaschema.core.model.SimpleResourceLocation;
12  import dev.metaschema.core.util.ObjectUtils;
13  import edu.umd.cs.findbugs.annotations.NonNull;
14  import edu.umd.cs.findbugs.annotations.Nullable;
15  
16  /**
17   * Provides contextual information for validation errors during parsing.
18   * <p>
19   * This class bundles together:
20   * <ul>
21   * <li>Source URI - the document being parsed</li>
22   * <li>Location - line and column within the document</li>
23   * <li>Path - the path to the current element in the document structure</li>
24   * <li>Format - whether parsing XML, JSON, or YAML</li>
25   * </ul>
26   * <p>
27   * This context is passed to problem handlers to enable rich, informative error
28   * messages that help users locate and understand validation errors.
29   */
30  public final class ValidationContext {
31    @Nullable
32    private final URI source;
33    @NonNull
34    private final IResourceLocation location;
35    @NonNull
36    private final String path;
37    @NonNull
38    private final Format format;
39  
40    /**
41     * Construct a new validation context.
42     *
43     * @param source
44     *          the source URI of the document being parsed, may be null
45     * @param location
46     *          the location within the document
47     * @param path
48     *          the path to the current element
49     * @param format
50     *          the format being parsed
51     */
52    private ValidationContext(
53        @Nullable URI source,
54        @NonNull IResourceLocation location,
55        @NonNull String path,
56        @NonNull Format format) {
57      this.source = source;
58      this.location = location;
59      this.path = path;
60      this.format = format;
61    }
62  
63    /**
64     * Create a new validation context.
65     *
66     * @param source
67     *          the source URI, may be null
68     * @param location
69     *          the resource location, must not be null
70     * @param path
71     *          the current path, must not be null
72     * @param format
73     *          the format being parsed, must not be null
74     * @return a new validation context
75     */
76    @NonNull
77    public static ValidationContext of(
78        @Nullable URI source,
79        @NonNull IResourceLocation location,
80        @NonNull String path,
81        @NonNull Format format) {
82      return new ValidationContext(source, location, path, format);
83    }
84  
85    /**
86     * Create a validation context with unknown location.
87     *
88     * @param source
89     *          the source URI, may be null
90     * @param path
91     *          the current path
92     * @param format
93     *          the format being parsed
94     * @return a new validation context with unknown location
95     */
96    @NonNull
97    public static ValidationContext ofUnknownLocation(
98        @Nullable URI source,
99        @NonNull String path,
100       @NonNull Format format) {
101     return new ValidationContext(source, SimpleResourceLocation.UNKNOWN, path, format);
102   }
103 
104   /**
105    * Get the source URI of the document being parsed.
106    *
107    * @return the source URI, or null if not available
108    */
109   @Nullable
110   public URI getSource() {
111     return source;
112   }
113 
114   /**
115    * Get the location within the document.
116    *
117    * @return the resource location
118    */
119   @NonNull
120   public IResourceLocation getLocation() {
121     return location;
122   }
123 
124   /**
125    * Get the path to the current element.
126    *
127    * @return the element path
128    */
129   @NonNull
130   public String getPath() {
131     return path;
132   }
133 
134   /**
135    * Get the format being parsed.
136    *
137    * @return the format
138    */
139   @NonNull
140   public Format getFormat() {
141     return format;
142   }
143 
144   /**
145    * Format the location information as a human-readable string.
146    * <p>
147    * The format is: "in 'source' at line:column" or "at line:column" if no source
148    * is available, or empty string if location is unknown.
149    *
150    * @return a formatted location string
151    */
152   @NonNull
153   public String formatLocation() {
154     StringBuilder sb = new StringBuilder();
155 
156     int line = location.getLine();
157     int column = location.getColumn();
158 
159     if (source != null) {
160       sb.append("in '").append(formatSourceName()).append("'");
161       if (line >= 0) {
162         sb.append(" at ").append(line);
163         if (column >= 0) {
164           sb.append(':').append(column);
165         }
166       }
167     } else if (line >= 0) {
168       sb.append("at ").append(line);
169       if (column >= 0) {
170         sb.append(':').append(column);
171       }
172     }
173 
174     return ObjectUtils.notNull(sb.toString());
175   }
176 
177   /**
178    * Format the source name for display.
179    * <p>
180    * Uses the file name if available, otherwise the full URI.
181    *
182    * @return a formatted source name, or empty string if no source
183    */
184   @NonNull
185   private String formatSourceName() {
186     URI sourceUri = source;
187     if (sourceUri == null) {
188       return "";
189     }
190     String path = sourceUri.getPath();
191     if (path != null && !path.isEmpty()) {
192       int lastSlash = path.lastIndexOf('/');
193       if (lastSlash >= 0 && lastSlash < path.length() - 1) {
194         return ObjectUtils.notNull(path.substring(lastSlash + 1));
195       }
196       return ObjectUtils.notNull(path);
197     }
198     return ObjectUtils.notNull(sourceUri.toString());
199   }
200 
201   /**
202    * Format the path information for display.
203    *
204    * @return the path, or "at document root" if path is empty or "/"
205    */
206   @NonNull
207   public String formatPath() {
208     if (path.isEmpty() || "/".equals(path)) {
209       return "at document root";
210     }
211     return "Path: " + path;
212   }
213 
214   @Override
215   public String toString() {
216     StringBuilder sb = new StringBuilder("ValidationContext[");
217     sb.append("format=").append(format);
218     if (source != null) {
219       sb.append(", source=").append(source);
220     }
221     sb.append(", location=").append(location);
222     sb.append(", path=").append(path);
223     sb.append(']');
224     return sb.toString();
225   }
226 }