Skip to content

Commit 98a61d5

Browse files
cursoragentjabrena
andcommitted
Migrate from DTD to XSD validation with improved error handling
Co-authored-by: bren <bren@juanantonio.info>
1 parent 7d9272e commit 98a61d5

2 files changed

Lines changed: 49 additions & 25 deletions

File tree

spml/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<version>0.1.0</version>
99

1010
<properties>
11-
<java.version>24</java.version>
11+
<java.version>21</java.version>
1212
<maven.version>3.9.10</maven.version>
1313
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1414
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

spml/src/main/java/info/jab/xml/CursorRuleGenerator.java

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
public final class CursorRuleGenerator {
2727

28-
private static final String DTD_FILE_NAME = "system-prompt.dtd";
28+
private static final String XSD_FILE_NAME = "system-prompt.xsd";
2929

3030
// ===============================================================
3131
// PUBLIC API - Entry point for cursor rule generation
@@ -68,17 +68,28 @@ private Optional<InputStream> loadResource(String fileName) {
6868
}
6969

7070
/**
71-
* Step 2: Creates SAXSource with custom EntityResolver.
72-
* Pure function that creates immutable SAXSource using modern SAX API.
71+
* Step 2: Creates SAXSource with XSD validation.
72+
* Pure function that creates immutable SAXSource with schema validation.
7373
*/
7474
private SAXSource createSaxSource(TransformationSources sources) {
7575
try {
76-
// Modern approach: Use SAXParserFactory instead of deprecated XMLReaderFactory
77-
XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
78-
xmlReader.setEntityResolver(new ResourceEntityResolver());
76+
// Create SAX parser factory with namespace awareness and validation
77+
SAXParserFactory factory = SAXParserFactory.newInstance();
78+
factory.setNamespaceAware(true);
79+
factory.setValidating(false); // We'll use schema validation instead
80+
81+
// Load XSD schema
82+
Optional<Schema> schema = loadXsdSchema();
83+
if (schema.isPresent()) {
84+
factory.setSchema(schema.get());
85+
}
86+
87+
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
88+
xmlReader.setErrorHandler(new ValidationErrorHandler());
89+
7990
return new SAXSource(xmlReader, new InputSource(sources.xmlStream()));
8091
} catch (SAXException | ParserConfigurationException e) {
81-
throw new RuntimeException("Failed to create SAX source with modern XMLReader API", e);
92+
throw new RuntimeException("Failed to create SAX source with XSD validation", e);
8293
}
8394
}
8495

@@ -129,27 +140,40 @@ private record TransformationSources(InputStream xmlStream, InputStream xslStrea
129140
}
130141

131142
/**
132-
* Custom EntityResolver as functional interface implementation.
133-
* Used by createSaxSource to resolve DTD references.
143+
* Loads XSD schema from classpath for validation.
144+
* Returns Optional to handle missing schema gracefully.
134145
*/
135-
private static final class ResourceEntityResolver implements EntityResolver {
146+
private Optional<Schema> loadXsdSchema() {
147+
return loadResource(XSD_FILE_NAME)
148+
.map(xsdStream -> {
149+
try {
150+
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
151+
return schemaFactory.newSchema(new StreamSource(xsdStream));
152+
} catch (SAXException e) {
153+
throw new RuntimeException("Failed to load XSD schema: " + XSD_FILE_NAME, e);
154+
}
155+
});
156+
}
157+
158+
/**
159+
* Custom ErrorHandler for XSD validation errors.
160+
* Provides better error reporting for validation issues.
161+
*/
162+
private static final class ValidationErrorHandler implements ErrorHandler {
136163
@Override
137-
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
138-
// Only handle system IDs we can resolve; return null for default SAX behavior otherwise
139-
return Optional.ofNullable(systemId)
140-
.filter(id -> id.endsWith(DTD_FILE_NAME))
141-
.flatMap(this::loadDtdFromClasspath)
142-
.orElse(null); // SAX contract: null means "use default resolution"
164+
public void warning(SAXParseException exception) throws SAXException {
165+
// Log warning in a real application
166+
System.err.println("XSD Validation Warning: " + exception.getMessage());
143167
}
144168

145-
private Optional<InputSource> loadDtdFromClasspath(String systemId) {
146-
return Optional.ofNullable(
147-
getClass().getClassLoader().getResourceAsStream(DTD_FILE_NAME)
148-
).map(dtdStream -> {
149-
InputSource inputSource = new InputSource(dtdStream);
150-
inputSource.setSystemId(systemId);
151-
return inputSource;
152-
});
169+
@Override
170+
public void error(SAXParseException exception) throws SAXException {
171+
throw new SAXException("XSD Validation Error: " + exception.getMessage(), exception);
172+
}
173+
174+
@Override
175+
public void fatalError(SAXParseException exception) throws SAXException {
176+
throw new SAXException("XSD Validation Fatal Error: " + exception.getMessage(), exception);
153177
}
154178
}
155179
}

0 commit comments

Comments
 (0)