|
3 | 3 | import java.io.InputStream; |
4 | 4 | import java.io.StringWriter; |
5 | 5 | import java.util.Objects; |
| 6 | +import java.util.Optional; |
| 7 | +import javax.xml.parsers.ParserConfigurationException; |
| 8 | +import javax.xml.parsers.SAXParserFactory; |
6 | 9 | import javax.xml.transform.*; |
7 | 10 | import javax.xml.transform.sax.SAXSource; |
8 | 11 | import javax.xml.transform.stream.StreamResult; |
9 | 12 | import javax.xml.transform.stream.StreamSource; |
10 | 13 | import org.xml.sax.EntityResolver; |
11 | 14 | import org.xml.sax.InputSource; |
| 15 | +import org.xml.sax.SAXException; |
12 | 16 | import org.xml.sax.XMLReader; |
13 | | -import org.xml.sax.helpers.XMLReaderFactory; |
14 | 17 |
|
15 | | -public class CursorRuleGenerator { |
| 18 | +/** |
| 19 | + * Generator for Cursor Rules using XML/XSLT transformation. |
| 20 | + * Follows functional programming principles with immutability and pure functions. |
| 21 | + */ |
| 22 | +public final class CursorRuleGenerator { |
16 | 23 |
|
17 | | - // Custom EntityResolver to resolve DTD from classpath resources |
18 | | - private static class ResourceEntityResolver implements EntityResolver { |
19 | | - @Override |
20 | | - public InputSource resolveEntity(String publicId, String systemId) { |
21 | | - if (systemId != null && systemId.endsWith("system-prompt.dtd")) { |
22 | | - InputStream dtdStream = getClass().getClassLoader().getResourceAsStream("system-prompt.dtd"); |
23 | | - if (dtdStream != null) { |
24 | | - InputSource inputSource = new InputSource(dtdStream); |
25 | | - inputSource.setSystemId(systemId); |
26 | | - return inputSource; |
27 | | - } |
28 | | - } |
29 | | - //TODO Not return null |
30 | | - return null; // Use default behavior for other entities |
31 | | - } |
32 | | - } |
| 24 | + private static final String DTD_FILE_NAME = "system-prompt.dtd"; |
33 | 25 |
|
| 26 | + // =============================================================== |
| 27 | + // PUBLIC API - Entry point for cursor rule generation |
| 28 | + // =============================================================== |
| 29 | + |
| 30 | + /** |
| 31 | + * Generates cursor rules by transforming XML with XSLT. |
| 32 | + * Pure function that depends only on input parameters. |
| 33 | + */ |
34 | 34 | public String generate(String xmlFileName, String xslFileName) { |
35 | | - try { |
36 | | - // Load XML and XSLT from resources |
37 | | - InputStream xmlStream = getClass().getClassLoader().getResourceAsStream(xmlFileName); |
38 | | - InputStream xslStream = getClass().getClassLoader().getResourceAsStream(xslFileName); |
| 35 | + return loadTransformationSources(xmlFileName, xslFileName) |
| 36 | + .map(this::createSaxSource) |
| 37 | + .flatMap(saxSource -> performTransformation(saxSource, xslFileName)) |
| 38 | + .orElseThrow(() -> new RuntimeException( |
| 39 | + "Failed to generate cursor rules for: " + xmlFileName + ", " + xslFileName)); |
| 40 | + } |
39 | 41 |
|
40 | | - if (Objects.isNull(xmlStream) || Objects.isNull(xslStream)) { |
41 | | - throw new RuntimeException("Could not load XML or XSLT resources: " + xmlFileName + ", " + xslFileName); |
42 | | - } |
| 42 | + // =============================================================== |
| 43 | + // PRIVATE METHODS - Organized in call order for readability |
| 44 | + // =============================================================== |
| 45 | + |
| 46 | + /** |
| 47 | + * Step 1: Loads XML and XSLT resources as a TransformationSources record. |
| 48 | + * Returns Optional to handle missing resources gracefully. |
| 49 | + */ |
| 50 | + private Optional<TransformationSources> loadTransformationSources(String xmlFileName, String xslFileName) { |
| 51 | + return loadResource(xmlFileName) |
| 52 | + .flatMap(xmlStream -> loadResource(xslFileName) |
| 53 | + .map(xslStream -> new TransformationSources(xmlStream, xslStream))); |
| 54 | + } |
43 | 55 |
|
44 | | - //TODO not use deprecated methods |
45 | | - // Create XMLReader with custom EntityResolver |
46 | | - XMLReader xmlReader = XMLReaderFactory.createXMLReader(); |
| 56 | + /** |
| 57 | + * Step 1a: Pure function to load a resource from classpath. |
| 58 | + * Used by loadTransformationSources and performTransformation. |
| 59 | + */ |
| 60 | + private Optional<InputStream> loadResource(String fileName) { |
| 61 | + return Optional.ofNullable( |
| 62 | + getClass().getClassLoader().getResourceAsStream(fileName) |
| 63 | + ); |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * Step 2: Creates SAXSource with custom EntityResolver. |
| 68 | + * Pure function that creates immutable SAXSource using modern SAX API. |
| 69 | + */ |
| 70 | + private SAXSource createSaxSource(TransformationSources sources) { |
| 71 | + try { |
| 72 | + // Modern approach: Use SAXParserFactory instead of deprecated XMLReaderFactory |
| 73 | + XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); |
47 | 74 | xmlReader.setEntityResolver(new ResourceEntityResolver()); |
| 75 | + return new SAXSource(xmlReader, new InputSource(sources.xmlStream())); |
| 76 | + } catch (SAXException | ParserConfigurationException e) { |
| 77 | + throw new RuntimeException("Failed to create SAX source with modern XMLReader API", e); |
| 78 | + } |
| 79 | + } |
48 | 80 |
|
49 | | - // Create SAXSource with custom XMLReader |
50 | | - SAXSource xmlSource = new SAXSource(xmlReader, new InputSource(xmlStream)); |
| 81 | + /** |
| 82 | + * Step 3: Performs the actual XSLT transformation. |
| 83 | + * Returns Optional to handle transformation failures gracefully. |
| 84 | + */ |
| 85 | + private Optional<String> performTransformation(SAXSource xmlSource, String xslFileName) { |
| 86 | + return loadResource(xslFileName) |
| 87 | + .flatMap(xslStream -> executeTransformation(xmlSource, xslStream)); |
| 88 | + } |
51 | 89 |
|
52 | | - // Create transformer factory and transformer |
| 90 | + /** |
| 91 | + * Step 4: Executes the transformation and returns the result. |
| 92 | + * Encapsulates the transformation logic in a pure function. |
| 93 | + */ |
| 94 | + private Optional<String> executeTransformation(SAXSource xmlSource, InputStream xslStream) { |
| 95 | + try { |
53 | 96 | TransformerFactory factory = TransformerFactory.newInstance(); |
54 | 97 | Transformer transformer = factory.newTransformer(new StreamSource(xslStream)); |
55 | 98 |
|
56 | | - // Prepare result |
57 | 99 | StringWriter stringWriter = new StringWriter(); |
58 | 100 | Result result = new StreamResult(stringWriter); |
59 | 101 |
|
60 | | - // Perform transformation |
61 | 102 | transformer.transform(xmlSource, result); |
62 | 103 |
|
63 | | - // Return the transformed content |
64 | | - return stringWriter.toString().trim(); |
| 104 | + return Optional.of(stringWriter.toString().trim()); |
| 105 | + } catch (TransformerException e) { |
| 106 | + // Log the exception in a real application |
| 107 | + return Optional.empty(); |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // =============================================================== |
| 112 | + // SUPPORTING CLASSES - Used by the main processing pipeline |
| 113 | + // =============================================================== |
65 | 114 |
|
66 | | - } catch (Exception e) { |
67 | | - throw new RuntimeException("Error during XML transformation", e); |
| 115 | + /** |
| 116 | + * Record for holding transformation sources - immutable data transfer (internal use only). |
| 117 | + * Used by loadTransformationSources to bundle XML and XSL streams together. |
| 118 | + */ |
| 119 | + private record TransformationSources(InputStream xmlStream, InputStream xslStream) { |
| 120 | + private TransformationSources { |
| 121 | + if (Objects.isNull(xmlStream) || Objects.isNull(xslStream)) { |
| 122 | + throw new IllegalArgumentException("XML and XSL streams cannot be null"); |
| 123 | + } |
68 | 124 | } |
69 | 125 | } |
70 | 126 |
|
| 127 | + /** |
| 128 | + * Custom EntityResolver as functional interface implementation. |
| 129 | + * Used by createSaxSource to resolve DTD references. |
| 130 | + */ |
| 131 | + private static final class ResourceEntityResolver implements EntityResolver { |
| 132 | + @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" |
| 139 | + } |
| 140 | + |
| 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 | + }); |
| 149 | + } |
| 150 | + } |
71 | 151 | } |
0 commit comments