ArchiMateSchema.java
package com.taxonomy.archimate.exchange;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Set;
/** Offline normative validation at both exchange boundaries, using the pinned upstream XSDs. */
public final class ArchiMateSchema {
public static final String NAMESPACE = "http://www.opengroup.org/xsd/archimate/3.0/";
public static final int MAX_BYTES = 32 * 1024 * 1024;
public static final int MAX_ELEMENTS = 10_000;
public static final int MAX_RELATIONSHIPS = 30_000;
private static final Set<String> FILES = Set.of("archimate3_Diagram.xsd", "archimate3_View.xsd",
"archimate3_Model.xsd", "dc.xsd", "xml.xsd");
private static final Schema SCHEMA = load();
private ArchiMateSchema() { }
public static Document parse(byte[] xml) {
if (xml == null || xml.length == 0 || xml.length > MAX_BYTES) {
throw new IllegalArgumentException("ArchiMate document must contain 1.." + MAX_BYTES + " bytes");
}
try {
// The supported Java runtime owns these security limits. Do not let a
// classpath JAXP provider replace the parser or silently disable them.
DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance();
factory.setNamespaceAware(true);
factory.setSchema(SCHEMA);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
factory.setAttribute("http://www.oracle.com/xml/jaxp/properties/maxElementDepth", 128);
var builder = factory.newDocumentBuilder();
builder.setErrorHandler(new ErrorHandler() {
@Override public void warning(SAXParseException e) throws SAXException { throw e; }
@Override public void error(SAXParseException e) throws SAXException { throw e; }
@Override public void fatalError(SAXParseException e) throws SAXException { throw e; }
});
Document document = builder.parse(new ByteArrayInputStream(xml));
if (!NAMESPACE.equals(document.getDocumentElement().getNamespaceURI())
|| !"model".equals(document.getDocumentElement().getLocalName())) {
throw new IllegalArgumentException("Expected an ArchiMate model");
}
if (document.getElementsByTagNameNS(NAMESPACE, "element").getLength() > MAX_ELEMENTS
|| document.getElementsByTagNameNS(NAMESPACE, "relationship").getLength() > MAX_RELATIONSHIPS
|| document.getElementsByTagNameNS(NAMESPACE, "view").getLength() > 32
|| document.getElementsByTagNameNS(NAMESPACE, "node").getLength() > 50_000
|| document.getElementsByTagNameNS(NAMESPACE, "connection").getLength() > 150_000) {
throw new IllegalArgumentException("ArchiMate model exceeds the bounded exchange profile");
}
return document;
} catch (javax.xml.parsers.ParserConfigurationException | SAXException | IOException | RuntimeException exception) {
throw new IllegalArgumentException("Invalid ArchiMate 3.1 Exchange XML: " + exception.getMessage(), exception);
}
}
private static Schema load() {
try {
SchemaFactory factory = SchemaFactory.newDefaultInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
DOMImplementationLS implementation = (DOMImplementationLS) DocumentBuilderFactory.newDefaultInstance()
.newDocumentBuilder().getDOMImplementation().getFeature("LS", "3.0");
factory.setResourceResolver((type, namespace, publicId, systemId, baseUri) -> {
String name = systemId == null ? "" : systemId.substring(systemId.lastIndexOf('/') + 1);
if (!FILES.contains(name)) throw new IllegalArgumentException("Unpinned schema reference");
URL url = resource(name);
LSInput input = implementation.createLSInput();
input.setSystemId(url.toExternalForm());
input.setPublicId(publicId);
try (InputStream stream = url.openStream()) {
input.setByteStream(new ByteArrayInputStream(stream.readAllBytes()));
} catch (IOException exception) {
throw new java.io.UncheckedIOException(exception);
}
return input;
});
URL root = resource("archimate3_Diagram.xsd");
try (InputStream stream = root.openStream()) {
return factory.newSchema(new StreamSource(stream, root.toExternalForm()));
}
} catch (SAXException | IOException | javax.xml.parsers.ParserConfigurationException exception) {
throw new ExceptionInInitializerError(exception);
}
}
private static URL resource(String name) {
return java.util.Objects.requireNonNull(ArchiMateSchema.class.getResource("/archimate-3.1/" + name),
"Missing pinned ArchiMate schema: " + name);
}
}