yegor256/takes

View on GitHub
src/main/java/org/takes/rs/RsPrettyXml.java

Summary

Maintainability
A
1 hr
Test Coverage
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2024 Yegor Bugayenko
 *
 * 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 NON-INFRINGEMENT. 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 org.takes.rs;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import lombok.ToString;
import org.cactoos.scalar.And;
import org.cactoos.scalar.HashCode;
import org.cactoos.scalar.Or;
import org.cactoos.scalar.Unchecked;
import org.takes.Response;
import org.w3c.dom.DocumentType;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * Response with properly indented XML body.
 *
 * <p>The class is immutable and thread-safe.
 * @since 1.0
 */
@ToString(of = "origin")
public final class RsPrettyXml implements Response {

    /**
     * Xerces feature to disable external DTD validation.
     */
    private static final String LOAD_EXTERNAL_DTD =
        "http://apache.org/xml/features/nonvalidating/load-external-dtd";

    /**
     * Original response.
     */
    private final Response origin;

    /**
     * Response with properly transformed body.
     */
    private final List<Response> transformed;

    /**
     * Synchronization lock.
     */
    private final Object lock;

    /**
     * Ctor.
     * @param res Original response
     */
    public RsPrettyXml(final Response res) {
        this.transformed = new CopyOnWriteArrayList<>();
        this.origin = res;
        this.lock = new Object();
    }

    @Override
    public Iterable<String> head() throws IOException {
        return this.make().head();
    }

    @Override
    public InputStream body() throws IOException {
        return this.make().body();
    }

    @Override
    @SuppressFBWarnings("EQ_UNUSUAL")
    public boolean equals(final Object that) {
        return new Unchecked<>(
            new Or(
                () -> this == that,
                new And(
                    () -> that != null,
                    () -> RsPrettyXml.class.equals(that.getClass()),
                    () -> {
                        final RsPrettyXml other = (RsPrettyXml) that;
                        return new And(
                            () -> this.origin.equals(other.origin),
                            () -> this.transformed.equals(other.transformed)
                        ).value();
                    }
                )
            )
        ).value();
    }

    @Override
    public int hashCode() {
        return new HashCode(this.origin, this.transformed).value();
    }

    /**
     * Make a response.
     * @return Response just made
     * @throws IOException If fails
     */
    private Response make() throws IOException {
        synchronized (this.lock) {
            if (this.transformed.isEmpty()) {
                this.transformed.add(
                    new RsWithBody(
                        this.origin,
                        RsPrettyXml.transform(this.origin.body())
                    )
                );
            }
        }
        return this.transformed.get(0);
    }

    /**
     * Format body with proper indents using SAX.
     * @param body Response body
     * @return New properly formatted body
     * @throws IOException If fails
     */
    private static byte[] transform(final InputStream body) throws IOException {
        final SAXSource source = new SAXSource(new InputSource(body));
        final ByteArrayOutputStream result = new ByteArrayOutputStream();
        try {
            final XMLReader xmlreader = SAXParserFactory.newInstance()
                .newSAXParser().getXMLReader();
            source.setXMLReader(xmlreader);
            xmlreader.setFeature(
                RsPrettyXml.LOAD_EXTERNAL_DTD, false
            );
            final String yes = "yes";
            final Transformer transformer = TransformerFactory.newInstance()
                .newTransformer();
            transformer.setOutputProperty(
                OutputKeys.OMIT_XML_DECLARATION, yes
            );
            RsPrettyXml.prepareDocType(body, transformer);
            transformer.setOutputProperty(OutputKeys.INDENT, yes);
            transformer.transform(source, new StreamResult(result));
        } catch (final TransformerException
            | ParserConfigurationException
            | SAXException ex) {
            throw new IOException(ex);
        }
        return result.toByteArray();
    }

    /**
     * Parses body to get DOCTYPE and configure Transformer
     * with proper method, public id and system id.
     * @param body The body to be parsed.
     * @param transformer Transformer to configure with proper properties.
     * @throws IOException if something goes wrong.
     */
    private static void prepareDocType(final InputStream body,
        final Transformer transformer) throws IOException {
        try {
            final String html = "html";
            final DocumentType doctype = RsPrettyXml.getDocType(body);
            if (null != doctype) {
                if (null == doctype.getSystemId()
                    && null == doctype.getPublicId()
                    && html.equalsIgnoreCase(doctype.getName())) {
                    transformer.setOutputProperty(OutputKeys.METHOD, html);
                    transformer.setOutputProperty(OutputKeys.VERSION, "5.0");
                    return;
                }
                if (null != doctype.getSystemId()) {
                    transformer.setOutputProperty(
                        OutputKeys.DOCTYPE_SYSTEM,
                        doctype.getSystemId()
                    );
                }
                if (null != doctype.getPublicId()) {
                    transformer.setOutputProperty(
                        OutputKeys.DOCTYPE_PUBLIC,
                        doctype.getPublicId()
                    );
                }
            }
        } finally {
            body.reset();
        }
    }

    /**
     * Parses the input stream and returns DocumentType built without loading
     * any external DTD schemas.
     * @param body The body to be parsed.
     * @return The documents DocumentType.
     * @throws IOException if something goes wrong.
     */
    private static DocumentType getDocType(final InputStream body)
        throws IOException {
        final DocumentBuilderFactory factory =
            DocumentBuilderFactory.newInstance();
        try {
            factory.setFeature(RsPrettyXml.LOAD_EXTERNAL_DTD, false);
            final DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(body).getDoctype();
        } catch (final ParserConfigurationException | SAXException ex) {
            throw new IOException(ex);
        }
    }
}