Java tutorial
/** * The MIT License * Copyright (c) 2015 Estonian Information System Authority (RIA), Population Register Centre (VRK) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package ee.ria.xroad.common.message; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.function.Consumer; import javax.xml.namespace.QName; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.soap.SOAPException; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.ext.DefaultHandler2; import ee.ria.xroad.common.CodedException; import ee.ria.xroad.common.identifier.CentralServiceId; import ee.ria.xroad.common.identifier.ClientId; import ee.ria.xroad.common.identifier.SecurityServerId; import ee.ria.xroad.common.identifier.ServiceId; import ee.ria.xroad.common.identifier.XRoadObjectType; import ee.ria.xroad.common.util.MimeUtils; import static ee.ria.xroad.common.ErrorCodes.*; import static ee.ria.xroad.common.message.SoapUtils.validateMimeType; import static ee.ria.xroad.common.util.MimeUtils.UTF8; import static ee.ria.xroad.common.util.MimeUtils.hasUtf8Charset; /** * SOAP message parser that does not construct a DOM tree of the message. */ @Slf4j public class SaxSoapParserImpl implements SoapParser { private static final String LEXICAL_HANDLER_PROPERTY = "http://xml.org/sax/properties/lexical-handler"; private static final String URI_IDENTIFIERS = "http://x-road.eu/xsd/identifiers"; private static final String URI_REPRESENTATION = "http://x-road.eu/xsd/representation.xsd"; private static final String URI_ENVELOPE = "http://schemas.xmlsoap.org/soap/envelope/"; private static final String URI_ENCODING = "http://schemas.xmlsoap.org/soap/encoding/"; private static final String ENVELOPE = "Envelope"; private static final String HEADER = "Header"; private static final String BODY = "Body"; private static final String FAULT = "Fault"; private static final String FAULT_CODE = "faultcode"; private static final String FAULT_STRING = "faultstring"; private static final String FAULT_ACTOR = "faultactor"; private static final String FAULT_DETAIL = "detail"; private static final String QUERY_ID = "id"; private static final String USER_ID = "userId"; private static final String ISSUE = "issue"; private static final String REPRESENTED_PARTY = "representedParty"; private static final String PARTY_CLASS = "partyClass"; private static final String PARTY_CODE = "partyCode"; private static final String PROTOCOL_VERSION = "protocolVersion"; private static final String CLIENT = "client"; private static final String SERVICE = "service"; private static final String CENTRAL_SERVICE = "centralService"; private static final String SECURITY_SERVER = "securityServer"; private static final String REQUEST_HASH = "requestHash"; private static final String INSTANCE = "xRoadInstance"; private static final String MEMBER_CLASS = "memberClass"; private static final String MEMBER_CODE = "memberCode"; private static final String SUBSYSTEM_CODE = "subsystemCode"; private static final String SERVICE_CODE = "serviceCode"; private static final String SERVICE_VERSION = "serviceVersion"; private static final String SERVER_CODE = "serverCode"; protected static final String ATTR_OBJECT_TYPE = "objectType"; protected static final String ATTR_ALGORITHM_ID = "algorithmId"; protected static final String ATTR_ENCODING_STYLE = "encodingStyle"; protected static final QName QNAME_SOAP_ENVELOPE = new QName(SoapUtils.NS_SOAPENV, ENVELOPE); protected static final QName QNAME_SOAP_HEADER = new QName(SoapUtils.NS_SOAPENV, HEADER); protected static final QName QNAME_SOAP_BODY = new QName(SoapUtils.NS_SOAPENV, BODY); protected static final QName QNAME_SOAP_FAULT = new QName(SoapUtils.NS_SOAPENV, FAULT); protected static final QName QNAME_XROAD_QUERY_ID = new QName(SoapHeader.NS_XROAD, QUERY_ID); protected static final QName QNAME_XROAD_USER_ID = new QName(SoapHeader.NS_XROAD, USER_ID); protected static final QName QNAME_XROAD_ISSUE = new QName(SoapHeader.NS_XROAD, ISSUE); protected static final QName QNAME_REPR_REPRESENTED_PARTY = new QName(SoapHeader.NS_REPR, REPRESENTED_PARTY); protected static final QName QNAME_XROAD_PROTOCOL_VERSION = new QName(SoapHeader.NS_XROAD, PROTOCOL_VERSION); protected static final QName QNAME_XROAD_REQUEST_HASH = new QName(SoapHeader.NS_XROAD, REQUEST_HASH); protected static final QName QNAME_XROAD_CLIENT = new QName(SoapHeader.NS_XROAD, CLIENT); protected static final QName QNAME_XROAD_SERVICE = new QName(SoapHeader.NS_XROAD, SERVICE); protected static final QName QNAME_XROAD_CENTRAL_SERVICE = new QName(SoapHeader.NS_XROAD, CENTRAL_SERVICE); protected static final QName QNAME_XROAD_SECURITY_SERVER = new QName(SoapHeader.NS_XROAD, SECURITY_SERVER); protected static final QName QNAME_ID_INSTANCE = new QName(URI_IDENTIFIERS, INSTANCE); protected static final QName QNAME_ID_MEMBER_CLASS = new QName(URI_IDENTIFIERS, MEMBER_CLASS); protected static final QName QNAME_ID_MEMBER_CODE = new QName(URI_IDENTIFIERS, MEMBER_CODE); protected static final QName QNAME_ID_SUBSYSTEM_CODE = new QName(URI_IDENTIFIERS, SUBSYSTEM_CODE); protected static final QName QNAME_ID_SERVICE_CODE = new QName(URI_IDENTIFIERS, SERVICE_CODE); protected static final QName QNAME_ID_SERVICE_VERSION = new QName(URI_IDENTIFIERS, SERVICE_VERSION); protected static final QName QNAME_ID_SERVER_CODE = new QName(URI_IDENTIFIERS, SERVER_CODE); protected static final QName QNAME_PARTY_CLASS = new QName(URI_REPRESENTATION, PARTY_CLASS); protected static final QName QNAME_PARTY_CODE = new QName(URI_REPRESENTATION, PARTY_CODE); private static final String MISSING_HEADER_MESSAGE = "Malformed SOAP message: header missing"; private static final String MISSING_SERVICE_MESSAGE = "Message header must contain either service id or central service id"; private static final String MISSING_HEADER_FIELD_MESSAGE = "Required field '%s' is missing"; private static final String DUPLICATE_HEADER_MESSAGE = "SOAP header contains duplicate field '%s'"; private static final String MISSING_BODY_MESSAGE = "Malformed SOAP message: body missing"; private static final String INVALID_BODY_MESSAGE = "Malformed SOAP message: body must have exactly one child element"; private static final String MISSING_ENVELOPE_MESSAGE = "Malformed SOAP message: envelope missing"; private static final char[] CDATA_START = "<![CDATA[".toCharArray(); private static final char[] CDATA_END = "]]>".toCharArray(); private static final char[] COMMENT_START = "<!--".toCharArray(); private static final char[] COMMENT_END = "-->".toCharArray(); private static final char[] ENTITY_START = { '&' }; private static final char[] ENTITY_END = { ';' }; private static final SAXParserFactory PARSER_FACTORY = createSaxParserFactory(); @Override public Soap parse(String contentType, InputStream is) { String mimeType = MimeUtils.getBaseContentType(contentType); String charset = MimeUtils.getCharset(contentType); charset = StringUtils.isNotBlank(charset) ? charset : UTF8; // Explicitly check content type to produce better error code // for client. if (mimeType != null) { validateMimeType(mimeType); } try { return parseMessage(is, mimeType, contentType, charset); } catch (Exception e) { throw translateException(e); } } private Soap parseMessage(InputStream is, String mimeType, String contentType, String charset) throws Exception { log.trace("parseMessage({}, {})", mimeType, charset); ByteArrayOutputStream rawXml = new ByteArrayOutputStream(); ByteArrayOutputStream processedXml = new ByteArrayOutputStream(); InputStream proxyStream = excludeUtf8Bom(contentType, new TeeInputStream(is, rawXml)); Writer outputWriter = new OutputStreamWriter(processedXml, charset); XRoadSoapHandler handler = handleSoap(outputWriter, proxyStream); CodedException fault = handler.getFault(); if (fault != null) { return createSoapFault(charset, rawXml, fault); } byte[] xmlBytes = isProcessedXmlRequired() ? processedXml.toByteArray() : rawXml.toByteArray(); return createSoapMessage(contentType, charset, handler, xmlBytes); } private XRoadSoapHandler handleSoap(Writer writer, InputStream inputStream) throws Exception { try (BufferedWriter out = new BufferedWriter(writer)) { XRoadSoapHandler handler = new XRoadSoapHandler(out); SAXParser saxParser = PARSER_FACTORY.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, handler); // ensure both builtin entities and character entities are reported to the parser xmlReader.setFeature("http://apache.org/xml/features/scanner/notify-char-refs", true); xmlReader.setFeature("http://apache.org/xml/features/scanner/notify-builtin-refs", true); saxParser.parse(inputStream, handler); return handler; } catch (SAXException ex) { throw new SOAPException(ex); } } private static Soap createSoapMessage(String contentType, String charset, XRoadSoapHandler handler, byte[] xmlBytes) throws Exception { return new SoapMessageImpl(xmlBytes, charset, handler.getHeader(), null, handler.getServiceName(), handler.isRpc(), contentType); } private static Soap createSoapFault(String charset, ByteArrayOutputStream rawXml, CodedException fault) { return new SoapFault(fault.getFaultCode(), fault.getFaultString(), fault.getFaultActor(), fault.getFaultDetail(), rawXml.toByteArray(), charset); } @SneakyThrows private static SAXParserFactory createSaxParserFactory() { SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setNamespaceAware(true); factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); // disable external entity parsing to avoid DOS attacks factory.setValidating(false); 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); return factory; } /** * Determines whether the raw XML of the SOAP message should be re-encoded * or if the original should be used in the output. * @return false by default */ protected boolean isProcessedXmlRequired() { return false; } private InputStream excludeUtf8Bom(String contentType, InputStream soapStream) { return hasUtf8Charset(contentType) ? new BOMInputStream(soapStream) : soapStream; } @SneakyThrows protected void writeStartElementXml(String prefix, QName element, Attributes attributes, Writer writer) { writer.append('<'); String localName = element.getLocalPart(); String tag = StringUtils.isEmpty(prefix) ? localName : prefix + ":" + localName; writer.append(tag); for (int i = 0; i < attributes.getLength(); i++) { String escapedAttrValue = StringEscapeUtils.escapeXml11(attributes.getValue(i)); writer.append(String.format(" %s=\"%s\"", attributes.getQName(i), escapedAttrValue)); } writer.append('>'); } @SneakyThrows protected void writeEndElementXml(String prefix, QName element, Attributes attributes, Writer writer) { writer.append("</"); String localName = element.getLocalPart(); String tag = StringUtils.isEmpty(prefix) ? localName : prefix + ":" + localName; writer.append(tag); writer.append('>'); } @SneakyThrows protected void writeCharactersXml(char[] characters, int start, int length, Writer writer) { writer.write(characters, start, length); } protected SoapHeaderHandler getSoapHeaderHandler(SoapHeader header) { return new SoapHeaderHandler(header); } @RequiredArgsConstructor private class XRoadSoapHandler extends DefaultHandler2 { private static final String NAMESPACE_PREFIX_SEPARATOR = ":"; private static final String XML_VERSION_ENCODING = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; private final BufferedWriter out; private char[] xmlEntity; private Stack<XmlElementHandler> elementHandlers = new Stack<>(); private SoapEnvelopeHandler envelopeHandler; @Getter private SoapHeader header; public String getServiceName() { return envelopeHandler != null ? envelopeHandler.getServiceName() : null; } public boolean isRpc() { return envelopeHandler != null && envelopeHandler.isRpc(); } public CodedException getFault() { return envelopeHandler != null ? envelopeHandler.getFault() : null; } private void reset() { envelopeHandler = null; header = new SoapHeader(); elementHandlers.clear(); } @Override public void startDocument() { log.trace("startDocument()"); reset(); if (isProcessedXmlRequired()) { writeXmlDeclaration(); } } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { QName element = new QName(uri, localName); if (elementHandlers.isEmpty()) { handleRootElement(attributes, element); } else { handleElement(attributes, element); } if (isProcessedXmlRequired()) { String prefix = findNamespacePrefix(qName); writeStartElementXml(prefix, element, attributes, out); } } private void handleElement(Attributes attributes, QName element) { XmlElementHandler elementHandler = elementHandlers.peek().getChildElementHandler(element); elementHandler.setAttributes(attributes); elementHandler.openTag(); elementHandlers.push(elementHandler); } private void handleRootElement(Attributes attributes, QName element) { if (element.equals(QNAME_SOAP_ENVELOPE)) { envelopeHandler = new SoapEnvelopeHandler(getSoapHeaderHandler(header)); envelopeHandler.setAttributes(attributes); envelopeHandler.openTag(); elementHandlers.push(envelopeHandler); } else { throw new CodedException(X_INVALID_SOAP, MISSING_ENVELOPE_MESSAGE); } } @Override public void characters(char[] ch, int start, int length) { XmlElementHandler elementParser = elementHandlers.peek(); elementParser.characters(ch, start, length); if (isProcessedXmlRequired()) { // Make sure XML entities are not resolved in processed XML if (xmlEntity != null) { writeCharactersXml(ENTITY_START, 0, 1, out); writeCharactersXml(xmlEntity, 0, xmlEntity.length, out); writeCharactersXml(ENTITY_END, 0, 1, out); xmlEntity = null; } else { writeCharactersXml(ch, start, length, out); } } } @Override public void comment(char[] ch, int start, int length) { if (isProcessedXmlRequired()) { writeCharactersXml(COMMENT_START, 0, COMMENT_START.length, out); writeCharactersXml(ch, start, length, out); writeCharactersXml(COMMENT_END, 0, COMMENT_END.length, out); } } @Override public void startEntity(String name) { if (isProcessedXmlRequired()) { xmlEntity = name.toCharArray(); } } @Override public void startCDATA() { if (isProcessedXmlRequired()) { writeCharactersXml(CDATA_START, 0, CDATA_START.length, out); } } @Override public void endCDATA() { if (isProcessedXmlRequired()) { writeCharactersXml(CDATA_END, 0, CDATA_END.length, out); } } @Override public void endElement(String uri, String localName, String qName) { XmlElementHandler elementHandler = elementHandlers.pop(); Attributes attributes = elementHandler.getAttributes(); elementHandler.valueInternal(); elementHandler.closeTag(); if (isProcessedXmlRequired()) { QName element = new QName(uri, localName); String prefix = findNamespacePrefix(qName); writeEndElementXml(prefix, element, attributes, out); } } @Override public void endDocument() { log.trace("endDocument()"); if (isProcessedXmlRequired()) { writeNewLine(); } } @Override public void warning(SAXParseException e) { log.warn(e.getMessage()); } @Override public void error(SAXParseException e) { throw translateException(e); } @SneakyThrows private void writeNewLine() { out.newLine(); } @SneakyThrows private void writeXmlDeclaration() { out.append(XML_VERSION_ENCODING); out.newLine(); } private String findNamespacePrefix(String qName) { String prefix = ""; if (qName.contains(NAMESPACE_PREFIX_SEPARATOR)) { prefix = qName.substring(0, qName.indexOf(NAMESPACE_PREFIX_SEPARATOR)); } return prefix; } } private static void validateDuplicateHeader(QName qName, Object existing) { if (existing != null) { throw new CodedException(X_DUPLICATE_HEADER_FIELD, DUPLICATE_HEADER_MESSAGE, qName); } } /** * Abstract XML element handler for processing SAX parser events. Includes * callbacks for beginning of element tag, character content and end element * of element tag. By default returns a NOOP handler for child elements. */ protected abstract static class XmlElementHandler { private static final XmlElementHandler NOOP_HANDLER = new NoOpHandler(); private StringBuilder valueBuffer = new StringBuilder(); @Getter @Setter private Attributes attributes; /** * Returns a handler for child elements of the given qualified name. * @param element the element's qualified name * @return the handler for the given child element */ protected XmlElementHandler getChildElementHandler(QName element) { return NOOP_HANDLER; } /** * Called when the parser encounters an element start event. */ protected void openTag() { } /** * Called when the parser encounters characters between this * element's start and end tags. By default collects characters * into an internal buffer. * @param ch the character array * @param start start of the relevant region * @param length content length */ protected void characters(char[] ch, int start, int length) { valueBuffer.append(ch, start, length); } protected final void valueInternal() { String value = ""; if (valueBuffer.length() > 0) { value = valueBuffer.toString(); valueBuffer.setLength(0); } value(value); } /** * Called when parser finishes reading the handled element's value. * @param value the element's value */ protected void value(String value) { } /** * Called when the parser encounters an element end event. */ protected void closeTag() { } /** * Create a generic handler that will apply a function to * the string value of the handled element. * @param stringValueHandler the function that will be applied * @return the created element handler */ public static XmlElementHandler createValueElementHandler(Consumer<String> stringValueHandler) { return new XmlElementHandler() { @Override protected void value(String val) { stringValueHandler.accept(val); } }; } } /** * Handler that ignores the element in it's entirety. */ private static class NoOpHandler extends XmlElementHandler { @Override protected void characters(char[] ch, int start, int length) { // ignore character content } } /** * Handler for the SOAP envelope, has child handlers for the * SOAP header and the SOAP body. */ @RequiredArgsConstructor private static class SoapEnvelopeHandler extends XmlElementHandler { private final SoapHeaderHandler headerHandler; private SoapBodyHandler bodyHandler; @Getter private boolean rpc; public CodedException getFault() { return bodyHandler != null ? bodyHandler.getFault() : null; } public String getServiceName() { return bodyHandler != null ? bodyHandler.getServiceName() : null; } @Override protected void openTag() { rpc = URI_ENCODING.equals(getAttributes().getValue(URI_ENVELOPE, ATTR_ENCODING_STYLE)); } @Override protected XmlElementHandler getChildElementHandler(QName element) { if (element.equals(QNAME_SOAP_HEADER)) { return headerHandler; } else if (element.equals(QNAME_SOAP_BODY)) { bodyHandler = new SoapBodyHandler(); return bodyHandler; } return super.getChildElementHandler(element); } @Override protected void closeTag() { if (getFault() == null) { validateHeader(); validateBody(); } } private void validateHeader() { if (!headerHandler.isFinished()) { throw new CodedException(X_MISSING_HEADER, MISSING_HEADER_MESSAGE); } SoapHeader header = headerHandler.getHeader(); if (header.getProtocolVersion() == null) { onMissingRequiredField(PROTOCOL_VERSION); } if (header.getClient() == null) { onMissingRequiredField(CLIENT); } if (header.getQueryId() == null) { onMissingRequiredField(QUERY_ID); } if (getService() == null) { throw new CodedException(X_MISSING_HEADER_FIELD, MISSING_SERVICE_MESSAGE); } } private void onMissingRequiredField(String fieldName) { throw new CodedException(X_MISSING_HEADER_FIELD, MISSING_HEADER_FIELD_MESSAGE, fieldName); } private void validateBody() { if (bodyHandler == null) { throw new CodedException(X_MISSING_BODY, MISSING_BODY_MESSAGE); } if (getServiceName() == null) { throw new CodedException(X_INVALID_BODY, INVALID_BODY_MESSAGE); } ServiceId service = getService(); SoapUtils.validateServiceName(service.getServiceCode(), getServiceName()); } private ServiceId getService() { SoapHeader header = headerHandler.getHeader(); ServiceId service = header.getService() != null ? header.getService() : header.getCentralService(); return service; } } /** * Handler for the SOAP header. Parses XRoad protocol header * elements and populates the header object given to it. * Throws an exception if duplicate elements are encountered. */ @RequiredArgsConstructor protected static class SoapHeaderHandler extends XmlElementHandler { @Getter protected final SoapHeader header; @Getter private boolean finished; /** * Called when a security server header has been parsed. * @param securityServerId the parsed client ID */ protected void onSecurityServer(SecurityServerId securityServerId) { header.setSecurityServer(securityServerId); } /** * Called when a client header has been parsed. * @param clientId the parsed client ID */ protected void onClient(ClientId clientId) { header.setClient(clientId); } /** * Called when a service header has been parsed. * @param serviceId the parsed service ID */ protected void onService(ServiceId serviceId) { header.setService(serviceId); } /** * Called when a central service header has been parsed. * @param centralServiceId the parsed central service ID */ protected void onCentralService(CentralServiceId centralServiceId) { header.setCentralService(centralServiceId); } /** * Called when a represented party header has been paresed. * @param representedParty the represented party */ protected void onRepresentedParty(RepresentedParty representedParty) { header.setRepresentedParty(representedParty); } @Override protected XmlElementHandler getChildElementHandler(QName element) { if (element.equals(QNAME_XROAD_QUERY_ID)) { validateDuplicateHeader(element, header.getQueryId()); return createValueElementHandler(header::setQueryId); } else if (element.equals(QNAME_XROAD_USER_ID)) { validateDuplicateHeader(element, header.getUserId()); return createValueElementHandler(header::setUserId); } else if (element.equals(QNAME_XROAD_ISSUE)) { validateDuplicateHeader(element, header.getIssue()); return createValueElementHandler(header::setIssue); } else if (element.equals(QNAME_XROAD_PROTOCOL_VERSION)) { validateDuplicateHeader(element, header.getProtocolVersion()); return createValueElementHandler(this::setProtocolVersion); } else if (element.equals(QNAME_XROAD_CLIENT)) { validateDuplicateHeader(element, header.getClient()); return new XRoadClientHeaderHandler(this::onClient); } else if (element.equals(QNAME_XROAD_SERVICE)) { validateDuplicateHeader(element, header.getService()); return new XRoadServiceHeaderHandler(this::onService); } else if (element.equals(QNAME_REPR_REPRESENTED_PARTY)) { validateDuplicateHeader(element, header.getRepresentedParty()); return new XRoadRepresentedPartyHeaderHandler(this::onRepresentedParty); } else if (element.equals(QNAME_XROAD_CENTRAL_SERVICE)) { validateDuplicateHeader(element, header.getService()); return new XRoadCentralServiceHeaderHandler(this::onCentralService); } else if (element.equals(QNAME_XROAD_SECURITY_SERVER)) { validateDuplicateHeader(element, header.getSecurityServer()); return new XRoadSecurityServerHeaderHandler(this::onSecurityServer); } else if (element.equals(QNAME_XROAD_REQUEST_HASH)) { validateDuplicateHeader(element, header.getRequestHash()); return new XRoadRequestHashElementHandler(); } return super.getChildElementHandler(element); } @SneakyThrows private void setProtocolVersion(String val) { header.setProtocolVersion(new ProtocolVersion(val)); } @Override @SneakyThrows protected void closeTag() { finished = true; } private class XRoadRequestHashElementHandler extends XmlElementHandler { private String hashAlgoId; @Override public void openTag() { hashAlgoId = getAttributes().getValue("", ATTR_ALGORITHM_ID); } @Override protected void value(String val) { header.setRequestHash(new RequestHash(hashAlgoId, val)); } } } /** * Generic handler for XRoad protocol identifiers, holds identifier * element values in a internal map and parses only elements returned * by the getAllowedChildElements method. */ @RequiredArgsConstructor private abstract static class XRoadIdentifierHeaderHandler extends XmlElementHandler { private final List<XRoadObjectType> expected; private Map<QName, String> identifierValues = new HashMap<>(); protected String getValue(QName key) { return identifierValues.get(key); } protected void setValue(QName key, String value) { identifierValues.put(key, value); } @Override public void openTag() { XRoadObjectType objectType = getObjectType(); if (!expected.contains(objectType)) { throw new CodedException(X_INVALID_XML, "Unexpected objectType: %s", objectType); } } protected abstract List<QName> getAllowedChildElements(); @Override protected XmlElementHandler getChildElementHandler(QName element) { if (getAllowedChildElements().contains(element)) { validateDuplicateHeader(element, getValue(element)); return new IdentifierElementHandler(element); } return super.getChildElementHandler(element); } @RequiredArgsConstructor private class IdentifierElementHandler extends XmlElementHandler { private final QName key; @Override protected void openTag() { validateDuplicateHeader(key, getValue(key)); } @Override protected void value(String val) { setValue(key, val); } } private XRoadObjectType getObjectType() { String objectType = getAttributes().getValue(URI_IDENTIFIERS, ATTR_OBJECT_TYPE); if (objectType == null) { throw new CodedException(X_INVALID_XML, "Missing objectType attribute"); } try { return XRoadObjectType.valueOf(objectType); } catch (IllegalArgumentException e) { throw new CodedException(X_INVALID_XML, "Unknown objectType: %s", objectType); } } } /** * Handler for the XRoad protocol client header. */ private static class XRoadClientHeaderHandler extends XRoadIdentifierHeaderHandler { protected static final List<QName> CLIENT_ID_PARTS = Arrays.asList(QNAME_ID_INSTANCE, QNAME_ID_MEMBER_CLASS, QNAME_ID_MEMBER_CODE, QNAME_ID_SUBSYSTEM_CODE); private final Consumer<ClientId> onClientCallback; XRoadClientHeaderHandler(Consumer<ClientId> callback) { super(Arrays.asList(XRoadObjectType.MEMBER, XRoadObjectType.SUBSYSTEM)); this.onClientCallback = callback; } @Override protected List<QName> getAllowedChildElements() { return CLIENT_ID_PARTS; } @Override public void closeTag() { onClientCallback.accept(ClientId.create(getValue(QNAME_ID_INSTANCE), getValue(QNAME_ID_MEMBER_CLASS), getValue(QNAME_ID_MEMBER_CODE), getValue(QNAME_ID_SUBSYSTEM_CODE))); } } /** * Handler for the XRoad protocol service header. */ private static class XRoadServiceHeaderHandler extends XRoadIdentifierHeaderHandler { protected static final List<QName> SERVICE_ID_PARTS = Arrays.asList(QNAME_ID_INSTANCE, QNAME_ID_MEMBER_CLASS, QNAME_ID_MEMBER_CODE, QNAME_ID_SUBSYSTEM_CODE, QNAME_ID_SERVICE_CODE, QNAME_ID_SERVICE_VERSION); private final Consumer<ServiceId> onServiceCallback; XRoadServiceHeaderHandler(Consumer<ServiceId> callback) { super(Collections.singletonList(XRoadObjectType.SERVICE)); this.onServiceCallback = callback; } @Override protected List<QName> getAllowedChildElements() { return SERVICE_ID_PARTS; } @Override protected void closeTag() { onServiceCallback.accept(ServiceId.create(getValue(QNAME_ID_INSTANCE), getValue(QNAME_ID_MEMBER_CLASS), getValue(QNAME_ID_MEMBER_CODE), getValue(QNAME_ID_SUBSYSTEM_CODE), getValue(QNAME_ID_SERVICE_CODE), getValue(QNAME_ID_SERVICE_VERSION))); } } /** * Handler for the XRoad protocol extension represented party header. */ @RequiredArgsConstructor private static class XRoadRepresentedPartyHeaderHandler extends XmlElementHandler { protected static final List<QName> REPRESENTED_PARTY_PARTS = Arrays.asList(QNAME_PARTY_CLASS, QNAME_PARTY_CODE); private final Consumer<RepresentedParty> onRepresentedPartyCallback; private Map<QName, String> representedPartyValues = new HashMap<>(); protected String getValue(QName key) { return representedPartyValues.get(key); } protected void setValue(QName key, String value) { representedPartyValues.put(key, value); } private List<QName> getAllowedChildElements() { return REPRESENTED_PARTY_PARTS; } @Override protected XmlElementHandler getChildElementHandler(QName element) { if (getAllowedChildElements().contains(element)) { validateDuplicateHeader(element, getValue(element)); return new RepresentedPartyElementHandler(element); } return super.getChildElementHandler(element); } @RequiredArgsConstructor private class RepresentedPartyElementHandler extends XmlElementHandler { private final QName key; @Override protected void openTag() { validateDuplicateHeader(key, getValue(key)); } @Override protected void value(String val) { setValue(key, val); } } @Override public void closeTag() { onRepresentedPartyCallback .accept(new RepresentedParty(getValue(QNAME_PARTY_CLASS), getValue(QNAME_PARTY_CODE))); } } /** * Handler for the XRoad protocol central service header. */ private static class XRoadCentralServiceHeaderHandler extends XRoadIdentifierHeaderHandler { protected static final List<QName> CENTRAL_SERVICE_ID_PARTS = Arrays.asList(QNAME_ID_INSTANCE, QNAME_ID_SERVICE_CODE); private final Consumer<CentralServiceId> onServiceCallback; XRoadCentralServiceHeaderHandler(Consumer<CentralServiceId> callback) { super(Collections.singletonList(XRoadObjectType.CENTRALSERVICE)); this.onServiceCallback = callback; } @Override protected List<QName> getAllowedChildElements() { return CENTRAL_SERVICE_ID_PARTS; } @Override protected void closeTag() { onServiceCallback .accept(CentralServiceId.create(getValue(QNAME_ID_INSTANCE), getValue(QNAME_ID_SERVICE_CODE))); } } /** * Handler for the XRoad protocol security server header. */ private static class XRoadSecurityServerHeaderHandler extends XRoadIdentifierHeaderHandler { protected static final List<QName> SECURITY_SERVER_ID_PARTS = Arrays.asList(QNAME_ID_INSTANCE, QNAME_ID_MEMBER_CLASS, QNAME_ID_MEMBER_CODE, QNAME_ID_SERVER_CODE); private final Consumer<SecurityServerId> onServiceCallback; XRoadSecurityServerHeaderHandler(Consumer<SecurityServerId> callback) { super(Collections.singletonList(XRoadObjectType.SERVER)); this.onServiceCallback = callback; } @Override protected List<QName> getAllowedChildElements() { return SECURITY_SERVER_ID_PARTS; } @Override protected void closeTag() { onServiceCallback .accept(SecurityServerId.create(getValue(QNAME_ID_INSTANCE), getValue(QNAME_ID_MEMBER_CLASS), getValue(QNAME_ID_MEMBER_CODE), getValue(QNAME_ID_SERVER_CODE))); } } /** * Handler for the SOAP body, looks at the root element's name for * the service code or the SOAP fault element, should it exists. * Ignores everything else. */ private static class SoapBodyHandler extends XmlElementHandler { private SoapFaultHandler soapFaultHandler; @Getter private CodedException fault; @Getter private String serviceName; @Override protected XmlElementHandler getChildElementHandler(QName element) { if (element.equals(QNAME_SOAP_FAULT)) { soapFaultHandler = new SoapFaultHandler(); return soapFaultHandler; } else if (serviceName == null) { // If no body elements have been encountered yet we assume // the first one to be the request wrapper element serviceName = element.getLocalPart(); } else { // If one body element has already been closed then we know // it's name to be the service name and expect no more top // level elements in the body throw new CodedException(X_INVALID_BODY, INVALID_BODY_MESSAGE); } return super.getChildElementHandler(element); } @Override protected void closeTag() { if (soapFaultHandler != null) { fault = CodedException.fromFault(soapFaultHandler.getFaultCode(), soapFaultHandler.getFaultString(), soapFaultHandler.getFaultActor(), soapFaultHandler.getFaultDetail(), null); } } } /** * Handler that extracts information from the SOAP fault. */ private static class SoapFaultHandler extends XmlElementHandler { private static final String DETAIL = "faultDetail"; @Getter private String faultCode; @Getter private String faultString; @Getter private String faultActor; @Getter private String faultDetail; @Override protected XmlElementHandler getChildElementHandler(QName element) { if (element.getLocalPart().equals(FAULT_CODE)) { return createValueElementHandler(val -> faultCode = val); } else if (element.getLocalPart().equals(FAULT_STRING)) { return createValueElementHandler(val -> faultString = val); } else if (element.getLocalPart().equals(FAULT_ACTOR)) { return createValueElementHandler(val -> faultActor = val); } else if (element.getLocalPart().equals(FAULT_DETAIL)) { return createFaultDetailHandler(); } return super.getChildElementHandler(element); } private XmlElementHandler createFaultDetailHandler() { return new XmlElementHandler() { @Override protected XmlElementHandler getChildElementHandler(QName element) { if (element.getLocalPart().equals(DETAIL)) { return createValueElementHandler(val -> faultDetail = val); } return super.getChildElementHandler(element); } @Override protected void value(String val) { if (StringUtils.isEmpty(faultDetail)) { faultDetail = val; } } }; } } }