From bda1ec9b7ffe3bd14353c88058601ecd43ce863c Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Wed, 21 Aug 2019 15:29:00 +0200 Subject: [PATCH] [jOOQ/jOOQ#8918] Log warnings regarding unknown elements The `MiniJAXB` implementation now attempts to validate the input XML against the corresponding XSD schema and will log warnings when there are validation errors (e.g. unknown elements). --- .../org/jooq/util/jaxb/tools/MiniJAXB.java | 206 +++++++++++++++--- 1 file changed, 171 insertions(+), 35 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/util/jaxb/tools/MiniJAXB.java b/jOOQ/src/main/java/org/jooq/util/jaxb/tools/MiniJAXB.java index 0450d3671a..08bc2a954e 100644 --- a/jOOQ/src/main/java/org/jooq/util/jaxb/tools/MiniJAXB.java +++ b/jOOQ/src/main/java/org/jooq/util/jaxb/tools/MiniJAXB.java @@ -39,9 +39,12 @@ package org.jooq.util.jaxb.tools; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; @@ -49,24 +52,35 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlEnum; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchema; import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import org.jooq.Constants; import org.jooq.Internal; import org.jooq.exception.ConfigurationException; import org.jooq.tools.Convert; +import org.jooq.tools.JooqLogger; import org.jooq.tools.reflect.Reflect; import org.jooq.tools.reflect.ReflectException; @@ -74,7 +88,10 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; /** * This class allows for mashalling / unmarshalling XML content to jOOQ @@ -90,6 +107,17 @@ import org.xml.sax.InputSource; @Internal public final class MiniJAXB { + private static final JooqLogger log = JooqLogger.getLogger(MiniJAXB.class); + private static final Map PROVIDED_SCHEMAS; + + static { + PROVIDED_SCHEMAS = new HashMap<>(); + PROVIDED_SCHEMAS.put(Constants.NS_CODEGEN, "/xsd/" + Constants.XSD_CODEGEN); + PROVIDED_SCHEMAS.put(Constants.NS_EXPORT, "/xsd/" + Constants.XSD_EXPORT); + PROVIDED_SCHEMAS.put(Constants.NS_META, "/xsd/" + Constants.XSD_META); + PROVIDED_SCHEMAS.put(Constants.NS_RUNTIME, "/xsd/" + Constants.XSD_RUNTIME); + } + public static String marshal(XMLAppendable object) { StringWriter writer = new StringWriter(); marshal(object, writer); @@ -137,9 +165,10 @@ public final class MiniJAXB { private static T unmarshal0(InputSource in, Class type) { try { - Document document = builder().parse(in); + addDefaultNamespace(in, type); + Document document = builder(type).parse(in); T result = Reflect.on(type).create().get(); - unmarshal0(result, document.getDocumentElement()); + unmarshal0(result, document.getDocumentElement(), new IdentityHashMap, Map>()); return result; } catch (Exception e) { @@ -147,15 +176,64 @@ public final class MiniJAXB { } } - private static void unmarshal0(Object result, Element element) throws Exception { + private static void addDefaultNamespace(InputSource in, Class type) + throws IOException { + String namespace = getNamespace(type); + if (namespace != null) { + Reader reader; + if (in.getCharacterStream() != null) + reader = in.getCharacterStream(); + else + reader = new InputStreamReader(in.getByteStream(), + in.getEncoding() != null ? in.getEncoding() : Charset.defaultCharset().name()); + StringWriter writer = new StringWriter(); + copyLarge(reader, writer); + String xml = writer.toString(); + + int startIdx = xml.indexOf('<'); + // skip over all processing instructions (like + while (startIdx > 0 && xml.length() > startIdx + 1 && xml.charAt(startIdx + 1) == '?') + startIdx = xml.indexOf('<', startIdx + 1); + int endIdx = xml.indexOf('>', startIdx); + if (!xml.substring(startIdx, endIdx).contains("xmlns")) { + xml = xml.replaceFirst( + "<([a-z_]+)\\s*(/?>)", + "<$1 xmlns=\"" + namespace + "\"$2"); + } + in.setCharacterStream(new StringReader(xml)); + } + } + + private static long copyLarge(Reader reader, Writer writer) throws IOException { + char[] buffer = new char[1024 * 4]; + long count = 0; + int n = 0; + while (-1 != (n = reader.read(buffer))) { + writer.write(buffer, 0, n); + count += n; + } + return count; + } + + private static void unmarshal0(Object result, Element element, Map, Map> fieldsByClass) throws Exception { if (result == null) return; - Class type = result.getClass(); - for (Field child : type.getDeclaredFields()) { - int modifiers = child.getModifiers(); - if (Modifier.isFinal(modifiers) || - Modifier.isStatic(modifiers)) + Map map = fieldsByElementName(fieldsByClass, result.getClass()); + + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node item = childNodes.item(i); + + if (item.getNodeType() != Node.ELEMENT_NODE) + continue; + + Element childElement = (Element) item; + Field child = map.get(childElement.getTagName()); + if (child == null) + child = map.get(childElement.getLocalName()); + // skip unknown elements + if (child == null) continue; XmlElementWrapper w = child.getAnnotation(XmlElementWrapper.class); @@ -163,23 +241,11 @@ public final class MiniJAXB { XmlJavaTypeAdapter a = child.getAnnotation(XmlJavaTypeAdapter.class); String childName = child.getName(); - String childElementName = - w != null - ? "##default".equals(w.name()) - ? child.getName() - : w.name() - : e == null || "##default".equals(e.name()) - ? childName - : e.name(); - - Element childElement = child(element, childElementName); - if (childElement == null) - continue; - Class childType = child.getType(); + if (List.class.isAssignableFrom(childType) && w != null && e != null) { List list = new ArrayList(); - unmarshalList0(list, childElement, e.name(), (Class) ((ParameterizedType) child.getGenericType()).getActualTypeArguments()[0]); + unmarshalList0(list, childElement, e.name(), (Class) ((ParameterizedType) child.getGenericType()).getActualTypeArguments()[0], fieldsByClass); Reflect.on(result).set(childName, list); } else if (childType.getAnnotation(XmlEnum.class) != null) { @@ -189,7 +255,7 @@ public final class MiniJAXB { Object object = Reflect.on(childType).create().get(); Reflect.on(result).set(childName, object); - unmarshal0(object, childElement); + unmarshal0(object, childElement, fieldsByClass); } else if (a != null) { @SuppressWarnings("unchecked") @@ -202,7 +268,7 @@ public final class MiniJAXB { } } - private static void unmarshalList0(List result, Element element, String name, Class type) throws Exception { + private static void unmarshalList0(List result, Element element, String name, Class type, Map, Map> fieldsByClass) throws Exception { if (result == null) return; @@ -213,28 +279,44 @@ public final class MiniJAXB { if (item.getNodeType() == Node.ELEMENT_NODE) { if (name.equals(((Element) item).getTagName()) || name.equals(((Element) item).getLocalName())) { Object o = Reflect.on(type).create().get(); - unmarshal0(o, (Element) item); + unmarshal0(o, (Element) item, fieldsByClass); result.add(o); } } } } - private static Element child(Element element, String name) { - NodeList list = element.getChildNodes(); + private static Map fieldsByElementName(Map, Map> fieldsByClass, Class type) { + Map result = fieldsByClass.get(type); + if (result == null) { + result = new HashMap(); + fieldsByClass.put(type, result); - for (int i = 0; i < list.getLength(); i++) { - Node item = list.item(i); + for (Field child : type.getDeclaredFields()) { + int modifiers = child.getModifiers(); + if (Modifier.isFinal(modifiers) || + Modifier.isStatic(modifiers)) + continue; - if (item.getNodeType() == Node.ELEMENT_NODE) - if (name.equals(((Element) item).getTagName()) || name.equals(((Element) item).getLocalName())) - return (Element) item; + XmlElementWrapper w = child.getAnnotation(XmlElementWrapper.class); + XmlElement e = child.getAnnotation(XmlElement.class); + + String childName = child.getName(); + String childElementName = w != null + ? "##default".equals(w.name()) + ? child.getName() + : w.name() + : e == null || "##default".equals(e.name()) + ? childName + : e.name(); + + result.put(childElementName, child); + } } - - return null; + return result; } - public static DocumentBuilder builder() { + private static DocumentBuilder builder(Class type) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -261,6 +343,15 @@ public final class MiniJAXB { } catch (UnsupportedOperationException ignore) {} + // [#8918] log warnings for unknown elements + String namespace = getNamespace(type); + if (namespace != null) + try { + Schema schema = getSchema(type, namespace); + factory.setSchema(schema); + } + catch (UnsupportedOperationException ignore) {} + factory.setExpandEntityReferences(false); // [JOOX #136] FIX END // ----------------------------------------------------------------- @@ -269,6 +360,22 @@ public final class MiniJAXB { // features, the internal builder should be namespace-aware factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException exception) throws SAXException { + log.warn(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + log.warn(exception); + } + + @Override + public void error(SAXParseException exception) throws SAXException { + log.warn(exception); + } + }); return builder; } @@ -277,6 +384,35 @@ public final class MiniJAXB { } } + private static String getNamespace(Class type) { + if (type != null && type.getPackage() != null && type.getPackage().isAnnotationPresent(XmlSchema.class)) + return type.getPackage().getAnnotation(XmlSchema.class).namespace(); + return null; + } + + private static Schema getSchema(Class type, String namespace) { + try { + URL url; + if (PROVIDED_SCHEMAS.containsKey(namespace)) + url = type.getResource(PROVIDED_SCHEMAS.get(namespace)); + else + url = new URL(namespace); + + if (url != null) { + SchemaFactory schemaFactory = SchemaFactory.newDefaultInstance(); + Schema schema = schemaFactory.newSchema(url); + return schema; + } + } + catch (MalformedURLException e) { + log.warn("Failed to load schema for namespace " + namespace, e); + } + catch (SAXException e) { + log.warn("Failed to load schema for namespace " + namespace, e); + } + return null; + } + /** * Appends a second JAXB annotated object to a * first one using Maven's