All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.prowidesoftware.swift.model.mx.XmlEventWriter Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2006-2023 Prowide
 *
 * 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 com.prowidesoftware.swift.model.mx;

import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.logging.Level;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;

/**
 * XMl writer for MX model classes.
 *
 * @since 7.8
 */
public final class XmlEventWriter implements XMLEventWriter {
    private static final transient java.util.logging.Logger log =
            java.util.logging.Logger.getLogger(XmlEventWriter.class.getName());
    static String DEFAULT_INDENT = "    ";
    private String indent = DEFAULT_INDENT;
    private final Writer out;
    private StartElement delayedStart;
    private boolean startTagIncomplete = false;
    private int startElementCount;
    private int nestedLevel;
    private String defaultPrefix;
    private Map preferredPrefixes;
    private boolean includeXMLDeclaration;
    private String rootElement;
    private String currentElement;
    private boolean preserveQnamePrefixes = false;
    private int previousNestedStartLevel;
    private XMLEvent previousEvent;
    private EscapeHandler escapeHandler;

    /**
     * @param baos                  output buffer to write
     * @param defaultPrefix         optional prefix (empty by default) to used for all elements that are not binded to a specific prefix
     * @param includeXMLDeclaration true to include the XML declaration (true by default)
     * @param rootElement           local name of the root element of the XML fragment to create, used to declare namespace
     * @param escapeHandler         escape handler to use or null to use the default
     * @param indent                optional indent string to use when marshalling into XML, if null, a four spaces string will be used as default
     * @see #setPreferredPrefixes(Map)
     * @since 9.1.7
     */
    public XmlEventWriter(
            Writer baos,
            final String defaultPrefix,
            boolean includeXMLDeclaration,
            final String rootElement,
            final EscapeHandler escapeHandler,
            final String indent) {
        this.out = baos;
        this.startElementCount = 0;
        this.nestedLevel = 0;
        this.defaultPrefix = defaultPrefix;
        this.includeXMLDeclaration = includeXMLDeclaration;
        this.rootElement = rootElement;
        this.escapeHandler = escapeHandler != null ? escapeHandler : new DefaultEscapeHandler();
        if (indent != null) {
            this.indent = indent;
        }
    }

    public void add(final XMLEvent event) throws XMLStreamException {
        if (event != null) {
            try {
                final int type = event.getEventType();
                switch (type) {
                    case XMLEvent.START_DOCUMENT: {
                        if (this.includeXMLDeclaration) {
                            String encoding = ((StartDocument) event).getCharacterEncodingScheme();
                            String encodingOrDefault = encoding != null ? encoding : "UTF-8";
                            out.write("\n");
                        } else {
                            log.finest("skipping xml declaration");
                        }
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.START_ELEMENT: {
                        this.startElementCount++;
                        closeStartTagIfNeeded();
                        final StartElement se = event.asStartElement();
                        final String localPart = se.getName().getLocalPart();
                        /*
                         * the startElementyCount below fixes the bug related to not opening nested Document inside xs:any
                         */
                        if (StringUtils.equals(localPart, this.rootElement) && this.startElementCount == 1) {
                            delayedStart = se;
                            log.finest("local part is Document, initializing delayed start, startElementCount="
                                    + this.startElementCount);
                        } else {
                            writeIndentIfNeeded(out, nestedLevel);
                            out.write("<" + prefixString(se.getName()) + localPart);
                            /*
                             * to support attributes instead of closing here we set a flag and close this later
                             */
                            startTagIncomplete = true;
                        }
                        this.previousNestedStartLevel = nestedLevel;
                        this.nestedLevel++;
                        this.currentElement = localPart;
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.NAMESPACE: {
                        final Namespace ne = (Namespace) event;
                        StringBuilder sb = new StringBuilder();
                        if (this.delayedStart != null) {
                            final String localPart = delayedStart.getName().getLocalPart();
                            sb.append("<" + prefixString(ne.getName()) + localPart);
                            this.delayedStart = null;
                            this.currentElement = localPart;
                        }
                        sb.append(namespace(ne));
                        out.write(sb.toString());
                        startTagIncomplete = true;
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.CHARACTERS: {
                        closeStartTagIfNeeded();
                        final Characters ce = event.asCharacters();
                        if (ce.isIgnorableWhiteSpace()) {
                            this.previousEvent = event;
                            break;
                        }
                        if (ce.isWhiteSpace()
                                && this.previousEvent != null
                                && this.previousEvent.getEventType() == XMLEvent.END_ELEMENT) {
                            this.previousEvent = event;
                            break;
                        }
                        final char[] arr = ce.getData().toCharArray();
                        String escapedString = this.escapeHandler.escape(arr, false);
                        out.write(escapedString);
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.END_ELEMENT: {
                        this.nestedLevel--;
                        closeStartTagIfNeeded();
                        final EndElement ee = event.asEndElement();
                        final String localPart = ee.getName().getLocalPart();

                        // Evaluates if previous end tag is the same as current.
                        // Needed because of embedded tags with same name.
                        // E.g:
                        //          2020-09-01
                        //     
                        if (!localPart.equals(this.currentElement)) {
                            // we are closing a nested element
                            writeIndentIfNeeded(out, nestedLevel);
                        } else {
                            if (localPart.equals(this.currentElement) && this.previousNestedStartLevel != nestedLevel) {
                                previousNestedStartLevel--;
                                writeIndentIfNeeded(out, previousNestedStartLevel);
                            }
                        }

                        out.write("");

                        // Records previous level
                        previousNestedStartLevel = nestedLevel;
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.END_DOCUMENT: {
                        closeStartTagIfNeeded();
                        /*
                         * No need to do anything while writing to a string
                         */
                        this.previousEvent = event;
                        break;
                    }

                    case XMLEvent.ATTRIBUTE: {
                        final Attribute a = (Attribute) event;
                        String escapedString = a.getValue() != null
                                ? this.escapeHandler.escape(a.getValue().toCharArray(), true)
                                : "";
                        out.write(" " + a.getName() + "=\"" + escapedString + "\"");
                        this.previousEvent = event;
                        break;
                    }

                    default: {
                        log.finer("PW Unhandled XMLEvent " + ToStringBuilder.reflectionToString(event));
                        this.previousEvent = event;
                        break;
                    }
                }
            } catch (IOException e) {
                log.severe("PW I/O error: " + e.getMessage());
                log.log(Level.FINER, "PW I/O error: ", e);
                throw new XMLStreamException(e);
            }
        }
    }

    /**
     * For a nested level above zero, writes the proportional identation
     */
    private void writeIndentIfNeeded(Writer writer, int nestedLevel) throws IOException {
        out.write(System.lineSeparator());
        if (nestedLevel > 0) {
            for (int i = 0; i < nestedLevel; i++) {
                writer.write(indent);
            }
        }
    }

    /**
     * Given a namespace event, returns the xmlns declaration with proper prefix
     */
    private String namespace(final Namespace namespace) {
        StringBuilder sb = new StringBuilder(" xmlns");
        String prefix = resolvePrefix(namespace);
        if (prefix != null) {
            sb.append(":").append(prefix);
        }
        sb.append("=\"").append(namespace.getValue()).append("\"");
        return sb.toString();
    }

    private String prefixString(final QName qname) {
        String prefix = resolvePrefix(qname);
        if (prefix != null) {
            return prefix + ":";
        } else {
            return "";
        }
    }

    /**
     * Return the prefix for the preferred prefix map, from the current element qname, or from default in that order
     */
    private String resolvePrefix(final QName qname) {
        if (this.preferredPrefixes != null) {
            String prefix = this.preferredPrefixes.get(qname.getNamespaceURI());
            if (prefix != null) {
                return prefix;
            }
        }
        if (this.preserveQnamePrefixes && StringUtils.isNotBlank(qname.getPrefix())) {
            return qname.getPrefix();
        }
        return this.defaultPrefix;
    }

    /**
     * Return the prefix for the preferred prefix map, from the current element qname, or from default in that order
     */
    private String resolvePrefix(final Namespace namespace) {
        if (this.preferredPrefixes != null) {
            String prefix = this.preferredPrefixes.get(namespace.getValue());
            if (prefix != null) {
                return prefix;
            }
        }
        if (this.preserveQnamePrefixes && StringUtils.isNotBlank(namespace.getPrefix())) {
            return namespace.getPrefix();
        }
        return this.defaultPrefix;
    }

    private void closeStartTagIfNeeded() throws IOException {
        if (this.startTagIncomplete) {
            out.write('>');
            this.startTagIncomplete = false;
        }
    }

    public void add(XMLEventReader arg0) {}

    public void close() {}

    public void flush() throws XMLStreamException {
        try {
            out.flush();
        } catch (IOException e) {
            throw new XMLStreamException(e);
        }
    }

    public NamespaceContext getNamespaceContext() {
        return null;
    }

    public void setNamespaceContext(NamespaceContext arg0) {}

    public String getPrefix(String arg0) {
        return null;
    }

    public void setDefaultNamespace(String arg0) {}

    public void setPrefix(String arg0, String arg1) {}

    /**
     * @since 7.9.3
     */
    public String getDefaultPrefix() {
        return defaultPrefix;
    }

    /**
     * @since 7.9.3
     */
    public void setDefaultPrefix(String defaultPrefix) {
        this.defaultPrefix = defaultPrefix;
    }

    /**
     * @since 9.0.2
     */
    public Map getPreferredPrefixes() {
        return preferredPrefixes;
    }

    /**
     * Custom optional prefix configuration, if provided, this prefixes will
     * be used regardless of any other context namespaces and prefix configuration.
     *
     * @param preferredPrefixes a map with namespaceURIs as keys and prefixes as values
     * @since 9.0.2
     */
    public void setPreferredPrefixes(Map preferredPrefixes) {
        this.preferredPrefixes = preferredPrefixes;
    }

    /**
     * @since 7.9.3
     */
    public boolean isIncludeXMLDeclaration() {
        return includeXMLDeclaration;
    }

    /**
     * @since 7.9.3
     */
    public void setIncludeXMLDeclaration(boolean includeXMLDeclaration) {
        this.includeXMLDeclaration = includeXMLDeclaration;
    }

    /**
     * @since 7.9.3
     */
    public String getRootElement() {
        return rootElement;
    }

    /**
     * @since 7.9.3
     */
    public void setRootElement(String rootElement) {
        this.rootElement = rootElement;
    }

    /**
     * @since 9.0.2
     */
    public void setPreserveQnamePrefixes(boolean preserveQnamePrefixes) {
        this.preserveQnamePrefixes = preserveQnamePrefixes;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy