|
4 | 4 | import java.io.StringWriter; |
5 | 5 | import java.util.Objects; |
6 | 6 | import java.util.Optional; |
| 7 | +import javax.xml.XMLConstants; |
7 | 8 | import javax.xml.parsers.ParserConfigurationException; |
8 | 9 | import javax.xml.parsers.SAXParserFactory; |
9 | 10 | import javax.xml.transform.*; |
10 | 11 | import javax.xml.transform.sax.SAXSource; |
11 | 12 | import javax.xml.transform.stream.StreamResult; |
12 | 13 | import javax.xml.transform.stream.StreamSource; |
13 | | -import org.xml.sax.EntityResolver; |
| 14 | +import javax.xml.validation.Schema; |
| 15 | +import javax.xml.validation.SchemaFactory; |
| 16 | +import org.xml.sax.ErrorHandler; |
14 | 17 | import org.xml.sax.InputSource; |
15 | 18 | import org.xml.sax.SAXException; |
| 19 | +import org.xml.sax.SAXParseException; |
16 | 20 | import org.xml.sax.XMLReader; |
17 | 21 |
|
18 | 22 | /** |
|
21 | 25 | */ |
22 | 26 | public final class CursorRuleGenerator { |
23 | 27 |
|
24 | | - private static final String DTD_FILE_NAME = "system-prompt.dtd"; |
| 28 | + private static final String XSD_FILE_NAME = "system-prompt.xsd"; |
25 | 29 |
|
26 | 30 | // =============================================================== |
27 | 31 | // PUBLIC API - Entry point for cursor rule generation |
@@ -64,17 +68,28 @@ private Optional<InputStream> loadResource(String fileName) { |
64 | 68 | } |
65 | 69 |
|
66 | 70 | /** |
67 | | - * Step 2: Creates SAXSource with custom EntityResolver. |
68 | | - * 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. |
69 | 73 | */ |
70 | 74 | private SAXSource createSaxSource(TransformationSources sources) { |
71 | 75 | try { |
72 | | - // Modern approach: Use SAXParserFactory instead of deprecated XMLReaderFactory |
73 | | - XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); |
74 | | - 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 | + |
75 | 90 | return new SAXSource(xmlReader, new InputSource(sources.xmlStream())); |
76 | 91 | } catch (SAXException | ParserConfigurationException e) { |
77 | | - 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); |
78 | 93 | } |
79 | 94 | } |
80 | 95 |
|
@@ -125,27 +140,40 @@ private record TransformationSources(InputStream xmlStream, InputStream xslStrea |
125 | 140 | } |
126 | 141 |
|
127 | 142 | /** |
128 | | - * Custom EntityResolver as functional interface implementation. |
129 | | - * Used by createSaxSource to resolve DTD references. |
| 143 | + * Loads XSD schema from classpath for validation. |
| 144 | + * Returns Optional to handle missing schema gracefully. |
130 | 145 | */ |
131 | | - 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 { |
132 | 163 | @Override |
133 | | - public InputSource resolveEntity(String publicId, String systemId) throws SAXException { |
134 | | - // Only handle system IDs we can resolve; return null for default SAX behavior otherwise |
135 | | - return Optional.ofNullable(systemId) |
136 | | - .filter(id -> id.endsWith(DTD_FILE_NAME)) |
137 | | - .flatMap(this::loadDtdFromClasspath) |
138 | | - .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()); |
139 | 167 | } |
140 | 168 |
|
141 | | - private Optional<InputSource> loadDtdFromClasspath(String systemId) { |
142 | | - return Optional.ofNullable( |
143 | | - getClass().getClassLoader().getResourceAsStream(DTD_FILE_NAME) |
144 | | - ).map(dtdStream -> { |
145 | | - InputSource inputSource = new InputSource(dtdStream); |
146 | | - inputSource.setSystemId(systemId); |
147 | | - return inputSource; |
148 | | - }); |
| 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); |
149 | 177 | } |
150 | 178 | } |
151 | 179 | } |
0 commit comments