AbstractStaxParser.java
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.saml.common.parsers;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.util.StaxParserUtil;
import java.util.Objects;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
/**
* Simple support for STaX type of parsing. Parses single element and allows processing its direct children.
*
* @param <T> Java class that will be result of parsing this element
* @param <E> Type containing all tokens that can be found in subelements of the element parsed by this parser, usually an enum
* @author hmlnarik
*/
public abstract class AbstractStaxParser<T, E> implements StaxParser {
protected static final PicketLinkLogger LOGGER = PicketLinkLoggerFactory.getLogger();
protected final QName expectedStartElement;
private final E unknownElement;
public AbstractStaxParser(QName expectedStartElement, E unknownElement) {
this.unknownElement = unknownElement;
this.expectedStartElement = expectedStartElement;
}
@Override
public T parse(XMLEventReader xmlEventReader) throws ParsingException {
// STATE: should be before the expected start element
// Get the start element and validate it is the expected one
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
final QName actualQName = startElement.getName();
validateStartElement(startElement);
T target = instantiateElement(xmlEventReader, startElement);
// STATE: Start element has been read.
QName currentSubelement = null;
while (xmlEventReader.hasNext()) {
// STATE: the only end element that can be found at this phase must correspond to the expected start element
XMLEvent xmlEvent = StaxParserUtil.peekNextTag(xmlEventReader);
if (xmlEvent == null) {
break;
}
if (xmlEvent instanceof EndElement) {
EndElement endElement = (EndElement) xmlEvent;
final QName qName = endElement.getName();
// If leftover from processed subelement, just consume.
if (Objects.equals(qName, currentSubelement)) {
StaxParserUtil.advance(xmlEventReader);
currentSubelement = null;
continue;
}
// If end element corresponding to this start element, stop processing.
if (Objects.equals(qName, actualQName)) {
// consume the end element and finish parsing of this tag
StaxParserUtil.advance(xmlEventReader);
break;
}
// No other case is valid
String elementName = StaxParserUtil.getElementName(endElement);
throw LOGGER.parserUnknownEndElement(elementName, xmlEvent.getLocation());
}
startElement = (StartElement) xmlEvent;
currentSubelement = startElement.getName();
E token = getElementFromName(currentSubelement);
if (token == null) {
token = unknownElement;
}
processSubElement(xmlEventReader, target, token, startElement);
// If the XMLEventReader has not advanced inside processSubElement (hence using "==" and not "equals"), advance it.
if (StaxParserUtil.peek(xmlEventReader) == startElement) {
StaxParserUtil.bypassElementBlock(xmlEventReader);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("Element %s bypassed", currentSubelement));
}
}
// In case of recursive nesting the same element, the corresponding end element MUST be handled
// in the {@code processSubElement} method and MUST NOT be consumed here.
if (Objects.equals(actualQName, currentSubelement) || isUnknownElement(token)) {
currentSubelement = null;
}
}
return target;
}
/**
* Validates that the given startElement has the expected properties (namely {@link QName} matches the expected one).
* @param startElement
* @return
*/
protected void validateStartElement(StartElement startElement) {
StaxParserUtil.validate(startElement, expectedStartElement);
}
protected boolean isUnknownElement(E token) {
return token == null || Objects.equals(token, unknownElement);
}
protected abstract E getElementFromName(QName name);
/**
* Instantiates the target Java class representing the current element.<br>
* <b>Precondition:</b> Current event is the {@link StartElement}<br>
* <b>Postcondition:</b> Current event is the {@link StartElement} or the {@link EndElement} corresponding to the {@link StartElement}
* @param xmlEventReader
* @param element The XML event that was just read from the {@code xmlEventReader}
* @return
* @throws ParsingException
*/
protected abstract T instantiateElement(XMLEventReader xmlEventReader, StartElement element) throws ParsingException;
/**
* Processes the subelement of the element processed in {@link #instantiateElement} method.<br>
* <b>Precondition:</b> Current event: Last before the {@link StartElement} corresponding to the processed subelement, i.e.
* event obtained by {@link XMLEventReader#next()} is the {@link StartElement} of the subelement being processed<br>
* <b>Postcondition:</b> Event obtained by {@link XMLEventReader#next()} is either
* the same {@link StartElement} (i.e. no change in position which causes this subelement to be skipped),
* the corresponding {@link EndElement}, or the event after the corresponding {@link EndElement}.
* <p>
* Note that in case of recursive nesting the same element, the corresponding end element MUST be consumed in this method.
* @param xmlEventReader
* @param target Target object (the one created by the {@link #instantiateElement} method.
* @param element The constant corresponding to the current start element.
* @param elementDetail The XML event that was just read from the {@code xmlEventReader}
* @return
* @throws ParsingException
*/
protected abstract void processSubElement(XMLEventReader xmlEventReader, T target, E element, StartElement elementDetail) throws ParsingException;
}