001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io;
007
008import java.net.URI;
009
010import dev.metaschema.core.model.IResourceLocation;
011import dev.metaschema.core.model.SimpleResourceLocation;
012import dev.metaschema.core.util.ObjectUtils;
013import edu.umd.cs.findbugs.annotations.NonNull;
014import edu.umd.cs.findbugs.annotations.Nullable;
015
016/**
017 * Provides contextual information for validation errors during parsing.
018 * <p>
019 * This class bundles together:
020 * <ul>
021 * <li>Source URI - the document being parsed</li>
022 * <li>Location - line and column within the document</li>
023 * <li>Path - the path to the current element in the document structure</li>
024 * <li>Format - whether parsing XML, JSON, or YAML</li>
025 * </ul>
026 * <p>
027 * This context is passed to problem handlers to enable rich, informative error
028 * messages that help users locate and understand validation errors.
029 */
030public final class ValidationContext {
031  @Nullable
032  private final URI source;
033  @NonNull
034  private final IResourceLocation location;
035  @NonNull
036  private final String path;
037  @NonNull
038  private final Format format;
039
040  /**
041   * Construct a new validation context.
042   *
043   * @param source
044   *          the source URI of the document being parsed, may be null
045   * @param location
046   *          the location within the document
047   * @param path
048   *          the path to the current element
049   * @param format
050   *          the format being parsed
051   */
052  private ValidationContext(
053      @Nullable URI source,
054      @NonNull IResourceLocation location,
055      @NonNull String path,
056      @NonNull Format format) {
057    this.source = source;
058    this.location = location;
059    this.path = path;
060    this.format = format;
061  }
062
063  /**
064   * Create a new validation context.
065   *
066   * @param source
067   *          the source URI, may be null
068   * @param location
069   *          the resource location, must not be null
070   * @param path
071   *          the current path, must not be null
072   * @param format
073   *          the format being parsed, must not be null
074   * @return a new validation context
075   */
076  @NonNull
077  public static ValidationContext of(
078      @Nullable URI source,
079      @NonNull IResourceLocation location,
080      @NonNull String path,
081      @NonNull Format format) {
082    return new ValidationContext(source, location, path, format);
083  }
084
085  /**
086   * Create a validation context with unknown location.
087   *
088   * @param source
089   *          the source URI, may be null
090   * @param path
091   *          the current path
092   * @param format
093   *          the format being parsed
094   * @return a new validation context with unknown location
095   */
096  @NonNull
097  public static ValidationContext ofUnknownLocation(
098      @Nullable URI source,
099      @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}