@Override
    public void endElement(final String uri, final String localName, final String qName) throws SAXException {
        LOGGER.trace("endElement {} {} {} level:{}", uri, localName, qName, depthLevel);

        insideElement = false;
        if (VALUE_ELEMENT.equalsIgnoreCase(localName)) {
            handleValueEndElement();
        }

        if (insideValueElement) {
            String newUri = uri;
            String newQName = qName;
            if (uri.equals(valueXmlDefaultNamespaceUri)) {
                // This is the default namespace so remove it from the element
                newQName = localName;
                newUri = "";
            }
            fastInfosetEndElement(localName, newUri, newQName);
        } else {
            if (MAP_ELEMENT.equalsIgnoreCase(localName)) {
                // capture the name of the map that the subsequent values will belong to. A ref
                // stream can contain data for multiple maps
                mapName = content.toString().toLowerCase(Locale.ROOT);
                if (!NullSafe.isBlankString(mapName)) {
                    stateDoc = stateDocMap.computeIfAbsent(mapName, k -> {
                        StateDoc doc = null;

                        if (!ScyllaDbNameValidator.isValidName(k)) {
                            error("Bad map name: " + k);
                        } else {
                            try {
                                doc = stateDocCache.get(k);
                                if (doc == null) {
                                    error("Unable to find state doc for map name: " + k);
                                }
                            } catch (final RuntimeException e) {
                                error(e);
                            }
                        }

                        return Optional.ofNullable(doc);
                    }).orElse(null);
                }

            } else if (KEY_ELEMENT.equalsIgnoreCase(localName)) {
                // the key for the KV pair
                key = content.toString();

            } else if (FROM_ELEMENT.equalsIgnoreCase(localName)) {
                // the start key for the key range
                final String string = content.toString();
                try {
                    rangeFrom = Long.parseLong(string);
                } catch (final RuntimeException e) {
                    error("Unable to parse string \"" + string + "\" as long for range from", e);
                }
            } else if (TO_ELEMENT.equalsIgnoreCase(localName)) {
                // the end key for the key range
                final String string = content.toString();
                try {
                    rangeTo = Long.parseLong(string);
                } catch (final RuntimeException e) {
                    error("Unable to parse string \"" + string + "\" as long for range to", e);
                }
            } else if (REFERENCE_ELEMENT.equalsIgnoreCase(localName)) {
                addData();

            } else if ("time".equals(localName)) {
                time = DateUtil.parseNormalDateTimeStringToInstant(content.toString());

            } else if ("timeout".equals(localName)) {
                timeout = StroomDuration.parse(content.toString());

            } else if ("session".equals(localName) ||
                       "session-start".equals(localName) ||
                       "session-end".equals(localName)) {
                addData();
            }
        }

        // Manually call endPrefixMapping for those prefixes we added
        final Set<String> manuallyAddedPrefixes = manuallyAddedLevelToPrefixMap.getOrDefault(
                depthLevel,
                Collections.emptySet());

        if (!manuallyAddedPrefixes.isEmpty()) {
            LOGGER.trace(() ->
                    LogUtil.message("Ending {} manually added prefixes at level {}",
                            manuallyAddedPrefixes.size(),
                            depthLevel));

            // Can't use .forEach() due to the SaxException
            for (final String manuallyAddedPrefix : manuallyAddedPrefixes) {
                fastInfosetEndPrefixMapping(manuallyAddedPrefix);
            }

            // We are leaving this level so can now delete the prefix mappings for this level
            manuallyAddedLevelToPrefixMap.get(depthLevel)
                    .clear();
        }

        // Leaving this level so
        depthLevel--;

        content.clear();
        super.endElement(uri, localName, qName);
    }
This method applies a post processing function if we are currently within
a function element. At this stage we should have details of the function
to apply from the corresponding start element and content to apply it to
from the characters event.

@param uri       The element's Namespace URI, or the empty string.
@param localName The element's local name, or the empty string.
@param qName     The element's qualified (prefixed) name, or the empty string.
@throws SAXException The client may throw an exception during processing.
@see stroom.pipeline.filter.AbstractXMLFilter#endElement(String,
String, String)
stroom-state/stroom-state-impl/src/main/java/stroom/state/impl/pipeline/StateFilter.java