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}