TL;DR: En este post te enseño un mini–proyecto en Java 17 + Maven que procesa un catálogo XML de videojuegos con SAX (Simple API for XML), aplica filtros (por rating, plataforma y género), genera un CSV listo para Excel/LibreOffice y muestra estadísticas por consola. Incluye endurecimiento de parser para evitar XXE y un ErrorHandler propio para depurar.
¿Qué problema resuelve?
Cuando el fichero XML es grande, SAX resulta ideal porque no carga el árbol completo en memoria (como haría DOM), sino que procesa el documento en streaming con callbacks (startElement, characters, endElement). Con esto puedes:
- Leer catálogos extensos sin petar RAM.
- Filtrar al vuelo (p. ej., rating mínimo, plataforma o género).
- Volcar un CSV depurado para tu análisis SEO/analytics o para alimentar otra app.
- Obtener estadísticas rápidas por consola (conteos por plataforma, precio medio por género, top N por rating, etc.).
Estructura del proyecto
sax-juegos/
├─ data/
│ └─ catalogo_juegos.xml
├─ src/main/java/com/ejemplo/saxjuegos/
│ ├─ Main.java # CLI, opciones y pipeline SAX
│ ├─ JuegosHandler.java # DefaultHandler: mapea XML -> objetos Juego
│ ├─ Juego.java # POJO de dominio
│ ├─ CsvWriter.java # Emisor CSV con escape correcto
│ ├─ Stats.java # Métricas: conteos, medias, top por rating
│ └─ AppErrorHandler.java # Manejo de warnings/errores SAX
└─ pom.xml # Maven (exec + assembly para jar “fat”)
El XML de ejemplo (fragmento)
El fichero data/catalogo_juegos.xml usa namespaces y etiquetas con metadatos, por ejemplo:
<cat:catalogo xmlns:cat="http://ejemplo.com/juegos" xmlns:met="http://ejemplo.com/meta">
<cat:juego id="J001" plataforma="PC">
<cat:titulo><![CDATA[Hollow Knight]]></cat:titulo>
<cat:genero>Metroidvania</cat:genero>
<cat:precio moneda="EUR">14.99</cat:precio>
<met:rating>4.7</met:rating>
<cat:fecha>2017-02-24</cat:fecha>
<cat:tags><cat:tag>indie</cat:tag><cat:tag>2D</cat:tag></cat:tags>
</cat:juego>
</cat:catalogo>
Tip: con SAX conviene normalizar nombres (con/ sin prefijo de namespace) y acumular texto en
characters()porque puede llegarte troceado.
Handler SAX: del XML al objeto Juego
La clase JuegosHandler extiende DefaultHandler y rellena un Juego a medida que se cierran etiquetas:
@Override
public void characters(char[] ch, int start, int length) {
text.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) {
String n = name(localName, qName);
String contenido = text.toString().trim();
if (actual != null) {
switch (n) {
case "titulo": if (!contenido.isEmpty()) actual.titulo = contenido; break;
case "genero": if (!contenido.isEmpty()) actual.genero = contenido; break;
case "precio": /* parseo double + moneda */ break;
case "rating": /* parseo double */ break;
case "fecha": if (!contenido.isEmpty()) actual.fecha = contenido;break;
case "tag": if (!contenido.isEmpty()) actual.tags.add(contenido); break;
case "juego": consumer.accept(actual); actual = null; break;
}
}
text.setLength(0);
}
- Se usa un
StringBuilder textpara acumular caracteres. - Al cerrar
<juego>...</juego>se entrega el objeto al consumer definido desdeMain. - El handler ignora espacios y controla conversiones (
Double.parseDouble, etc.).
Seguridad XML: endureciendo el parser (anti‑XXE)
En Main el SAXParserFactory se configura para bloquear DOCTYPE y entidades externas, desactivar XInclude y registrar un ErrorHandler propio:
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
// Endurecimiento contra XXE/expansiones externas
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
SAXParser parser = factory.newSAXParser();
parser.getXMLReader().setErrorHandler(new AppErrorHandler());
Esto neutraliza vectores típicos de XML External Entity y hace el parser predecible en producción.
Emisión de CSV con escaping correcto
CsvWriter escribe una cabecera y cada Juego en formato separado por ; con escapado de comillas y saltos de línea:
public static void writeHeader(Writer w) throws IOException {
w.write("id;plataforma;titulo;genero;precio;moneda;rating;fecha;tags\n");
}
private static String esc(String s) {
if (s == null) return "";
boolean needQuote = s.contains(";") || s.contains(""") || s.contains("\n") || s.contains("\r");
String v = s.replace(""", """");
return needQuote ? """ + v + """ : v;
}
Resultado típico:
id;plataforma;titulo;genero;precio;moneda;rating;fecha;tags(con valores limpios y tags concatenadas).
Estadísticas instantáneas en consola
La clase Stats registra cada juego leído y emitido (tras filtros) y calcula:
- Conteo por plataforma.
- Precio medio por género (en EUR si el dato viene en esa moneda).
- Top N por rating (configurable en el constructor, p. ej.
new Stats(3)).
La salida aparece al terminar el parseo, antes de cerrar el proceso.
CLI: opciones y ejemplos de ejecución
Primero compila el proyecto y genera el fat jar:
mvn -q -f pom.xml package
Ejecución típica (filtrando por rating mínimo y plataforma):
java -jar target/sax-juegos-1.0.0-jar-with-dependencies.jar \
--in data/catalogo_juegos.xml \
--out out/juegos.csv \
--min-rating 4.5 \
--plataforma PC
Otros filtros disponibles:
--genero <string>--plataforma <string>--min-rating <double>
El programa crea la carpeta de salida si no existe y escribe el CSV en UTF‑8.
Por qué SAX aquí y no DOM o StAX
- SAX: memoria constante, excelente para ficheros grandes y pipelines rápidos.
- DOM: acceso aleatorio y transformaciones complejas, pero carga todo el árbol (peor en ficheros enormes).
- StAX: pull‑parser muy cómodo si quieres control fino de cursor; aquí SAX basta y reduce boilerplate.
Posibles mejoras (roadmap)
- Conversión de monedas al vuelo (EUR ⇄ USD) con una tasa configurable.
- Validación contra XSD y reporte de errores de esquema.
- Tests unitarios para el mapeo de etiquetas y las reglas de filtrado.
- Salida JSON/Parquet además de CSV.
- Benchmarks comparando SAX vs StAX/DOM en distintos tamaños de XML.
- Publicación del JAR en GitHub Releases + action de CI/CD.
Conclusión
Este patrón (SAX → objetos → filtros → CSV + stats) es una base sólida para ETL ligeros con XML en Java. Permite crecer hacia validación/monedas/CI sin complicar el núcleo. Si te interesa, puedo adaptarlo a otros catálogos (libros, películas, productos) o integrarlo con tu pipeline SEO y tus dashboards.