com.haulmont.cuba.restapi.XMLConverter.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.restapi.XMLConverter.java

Source

/*
 * Based on JEST, part of the OpenJPA framework.
 *
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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.haulmont.cuba.restapi;

import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.security.entity.EntityAttrAccess;
import com.haulmont.cuba.security.entity.EntityOp;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.w3c.dom.*;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSParser;
import org.w3c.dom.ls.LSParserFilter;
import org.w3c.dom.traversal.NodeFilter;

import javax.activation.MimeType;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.persistence.Embedded;
import javax.persistence.Id;
import javax.persistence.Version;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.*;

/**
 * This class is deprecated and probably will be removed in next releases
 * It does not support new platform's major features as : dynamic attributes, in memory row level security
 * Please use XML API v.2
 *
 */
@Deprecated
@Component
public class XMLConverter implements Converter {
    public static final MimeType MIME_TYPE_XML;
    public static final String MIME_STR = "text/xml;charset=UTF-8";
    public static final String TYPE_XML = "xml";

    public static final String ELEMENT_INSTANCE = "instance";
    public static final String ELEMENT_URI = "uri";
    public static final String ELEMENT_REF = "ref";
    public static final String ELEMENT_NULL_REF = "null";
    public static final String ELEMENT_MEMBER = "member";
    public static final String ATTR_ID = "id";
    public static final String ATTR_NAME = "name";
    public static final String ATTR_VERSION = "version";
    public static final String ATTR_NULL = "null";
    public static final String ATTR_MEMBER_TYPE = "member-type";
    public static final String NULL_VALUE = "null";

    protected static final String EMPTY_TEXT = " ";
    public static final char DASH = '-';
    public static final char UNDERSCORE = '_';

    public static final String ROOT_ELEMENT_INSTANCE = "instances";

    public static final String MAPPING_ROOT_ELEMENT_INSTANCE = "mapping";
    public static final String PAIR_ELEMENT = "pair";

    static {
        try {
            MIME_TYPE_XML = new MimeType(MIME_STR);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Inject
    protected Metadata metadata;

    @Inject
    protected Configuration configuration;

    protected RestConfig restConfig;

    @PostConstruct
    protected void init() {
        restConfig = configuration.getConfig(RestConfig.class);
    }

    @Override
    public MimeType getMimeType() {
        return MIME_TYPE_XML;
    }

    @Override
    public String getType() {
        return TYPE_XML;
    }

    @Override
    public String process(Entity entity, MetaClass metaclass, View view)
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        Element root = newDocument(ROOT_ELEMENT_INSTANCE);
        encodeEntityInstance(new HashSet<Entity>(), entity, root, false, metaclass, view);
        Document doc = root.getOwnerDocument();
        return documentToString(doc);
    }

    @Override
    public String process(List<Entity> entities, MetaClass metaClass, View view)
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        Element root = newDocument(ROOT_ELEMENT_INSTANCE);
        for (Entity entity : entities) {
            encodeEntityInstance(new HashSet<Entity>(), entity, root, false, metaClass, view);
        }
        Document doc = root.getOwnerDocument();
        return documentToString(doc);
    }

    @Override
    public String process(Set<Entity> entities)
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        Element root = newDocument(MAPPING_ROOT_ELEMENT_INSTANCE);
        Document doc = root.getOwnerDocument();
        for (Entity entity : entities) {
            Element pair = doc.createElement(PAIR_ELEMENT);
            root.appendChild(pair);
            encodeEntityInstance(new HashSet<Entity>(), entity, pair, false, getMetaClass(entity), null);
            encodeEntityInstance(new HashSet<Entity>(), entity, pair, false, getMetaClass(entity), null);
        }
        return documentToString(doc);
    }

    @Override
    public CommitRequest parseCommitRequest(String content) {
        try {
            DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
            DOMImplementationLS lsImpl = (DOMImplementationLS) registry.getDOMImplementation("LS");
            LSParser requestConfigParser = lsImpl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);

            // Set options on the parser
            DOMConfiguration config = requestConfigParser.getDomConfig();
            config.setParameter("validate", Boolean.TRUE);
            config.setParameter("element-content-whitespace", Boolean.FALSE);
            config.setParameter("comments", Boolean.FALSE);
            requestConfigParser.setFilter(new LSParserFilter() {
                @Override
                public short startElement(Element elementArg) {
                    return LSParserFilter.FILTER_ACCEPT;
                }

                @Override
                public short acceptNode(Node nodeArg) {
                    return StringUtils.isBlank(nodeArg.getTextContent()) ? LSParserFilter.FILTER_REJECT
                            : LSParserFilter.FILTER_ACCEPT;
                }

                @Override
                public int getWhatToShow() {
                    return NodeFilter.SHOW_TEXT;
                }
            });
            LSInput lsInput = lsImpl.createLSInput();
            lsInput.setStringData(content);
            Document commitRequestDoc = requestConfigParser.parse(lsInput);
            Node rootNode = commitRequestDoc.getFirstChild();
            if (!"CommitRequest".equals(rootNode.getNodeName()))
                throw new IllegalArgumentException("Not a CommitRequest xml passed: " + rootNode.getNodeName());

            CommitRequest result = new CommitRequest();

            NodeList children = rootNode.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);
                String childNodeName = child.getNodeName();
                if ("commitInstances".equals(childNodeName)) {
                    NodeList entitiesNodeList = child.getChildNodes();

                    Set<String> commitIds = new HashSet<>(entitiesNodeList.getLength());
                    for (int j = 0; j < entitiesNodeList.getLength(); j++) {
                        Node idNode = entitiesNodeList.item(j).getAttributes().getNamedItem("id");
                        if (idNode == null)
                            continue;

                        String id = idNode.getTextContent();
                        if (id.startsWith("NEW-"))
                            id = id.substring(id.indexOf('-') + 1);
                        commitIds.add(id);
                    }

                    result.setCommitIds(commitIds);
                    result.setCommitInstances(parseNodeList(result, entitiesNodeList));
                } else if ("removeInstances".equals(childNodeName)) {
                    NodeList entitiesNodeList = child.getChildNodes();

                    List removeInstances = parseNodeList(result, entitiesNodeList);
                    result.setRemoveInstances(removeInstances);
                } else if ("softDeletion".equals(childNodeName)) {
                    result.setSoftDeletion(Boolean.parseBoolean(child.getTextContent()));
                }
            }
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private List parseNodeList(CommitRequest commitRequest, NodeList entitiesNodeList)
            throws InstantiationException, IllegalAccessException, InvocationTargetException,
            IntrospectionException, ParseException {
        List entities = new ArrayList(entitiesNodeList.getLength());
        for (int j = 0; j < entitiesNodeList.getLength(); j++) {
            Node entityNode = entitiesNodeList.item(j);
            if (ELEMENT_INSTANCE.equals(entityNode.getNodeName())) {
                InstanceRef ref = commitRequest.parseInstanceRefAndRegister(getIdAttribute(entityNode));
                MetaClass metaClass = ref.getMetaClass();
                Object instance = ref.getInstance();
                parseEntity(commitRequest, instance, metaClass, entityNode);
                entities.add(instance);
            }
        }
        return entities;
    }

    private void parseEntity(CommitRequest commitRequest, Object bean, MetaClass metaClass, Node node)
            throws InstantiationException, IllegalAccessException, InvocationTargetException,
            IntrospectionException, ParseException {
        MetadataTools metadataTools = AppBeans.get(MetadataTools.NAME);
        NodeList fields = node.getChildNodes();
        for (int i = 0; i < fields.getLength(); i++) {
            Node fieldNode = fields.item(i);
            String fieldName = getFieldName(fieldNode);
            MetaProperty property = metaClass.getProperty(fieldName);

            if (!attrModifyPermitted(metaClass, property.getName()))
                continue;

            if (metadataTools.isTransient(bean, fieldName))
                continue;

            String xmlValue = fieldNode.getTextContent();
            if (isNullValue(fieldNode)) {
                setNullField(bean, fieldName);
                continue;
            }

            Object value;

            switch (property.getType()) {
            case DATATYPE:
                if (property.getAnnotatedElement().isAnnotationPresent(Id.class)) {
                    // it was parsed in the beginning
                    continue;
                }

                Class type = property.getRange().asDatatype().getJavaClass();
                if (!type.equals(String.class) && "null".equals(xmlValue)) {
                    value = null;
                } else {
                    value = property.getRange().asDatatype().parse(xmlValue);
                }
                setField(bean, fieldName, value);
                break;
            case ENUM:
                value = property.getRange().asEnumeration().parse(xmlValue);
                setField(bean, fieldName, value);
                break;
            case COMPOSITION:
            case ASSOCIATION: {
                if ("null".equals(xmlValue)) {
                    setField(bean, fieldName, null);
                    break;
                }
                MetaClass propertyMetaClass = propertyMetaClass(property);
                //checks if the user permitted to read and update a property
                if (!updatePermitted(propertyMetaClass) && !readPermitted(propertyMetaClass))
                    break;

                if (!property.getRange().getCardinality().isMany()) {
                    if (property.getAnnotatedElement().isAnnotationPresent(Embedded.class)) {
                        MetaClass embeddedMetaClass = property.getRange().asClass();
                        value = metadata.create(embeddedMetaClass);
                        parseEntity(commitRequest, value, embeddedMetaClass, fieldNode);
                    } else {
                        String id = getRefId(fieldNode);

                        //reference to an entity that also a commit instance
                        //will be registered later
                        if (commitRequest.getCommitIds().contains(id)) {
                            EntityLoadInfo loadInfo = EntityLoadInfo.parse(id);
                            Entity ref = metadata.create(loadInfo.getMetaClass());
                            ref.setValue("id", loadInfo.getId());
                            setField(bean, fieldName, ref);
                            break;
                        }

                        value = parseEntityReference(fieldNode, commitRequest);
                    }
                    setField(bean, fieldName, value);
                } else {
                    NodeList memberNodes = fieldNode.getChildNodes();
                    Collection<Object> members = property.getRange().isOrdered() ? new ArrayList<>()
                            : new HashSet<>();

                    for (int memberIndex = 0; memberIndex < memberNodes.getLength(); memberIndex++) {
                        Node memberNode = memberNodes.item(memberIndex);
                        members.add(parseEntityReference(memberNode, commitRequest));
                    }
                    setField(bean, fieldName, members);
                }
                break;
            }
            default:
                throw new IllegalStateException("Unknown property type");
            }
        }
    }

    private String getRefId(Node refNode) {
        Node childNode = refNode.getFirstChild();
        do {
            if (ELEMENT_REF.equals(childNode.getNodeName())) {
                Node idNode = childNode.getAttributes().getNamedItem(ATTR_ID);
                return idNode != null ? idNode.getTextContent() : null;
            }
            childNode = childNode.getNextSibling();
        } while (childNode != null);
        return null;
    }

    private Object parseEntityReference(Node node, CommitRequest commitRequest) throws InstantiationException,
            IllegalAccessException, InvocationTargetException, IntrospectionException {
        Node childNode = node.getFirstChild();
        if (ELEMENT_NULL_REF.equals(childNode.getNodeName())) {
            return null;
        }

        InstanceRef ref = commitRequest.parseInstanceRefAndRegister(getIdAttribute(childNode));
        return ref.getInstance();
    }

    private void setField(Object result, String fieldName, Object value)
            throws IllegalAccessException, InvocationTargetException, IntrospectionException {
        new PropertyDescriptor(fieldName, result.getClass()).getWriteMethod().invoke(result, value);
    }

    private void setNullField(Object bean, String fieldName)
            throws IllegalAccessException, InvocationTargetException, IntrospectionException {
        setField(bean, fieldName, null);
    }

    private String getFieldName(Node fieldNode) {
        return getAttributeValue(fieldNode, ATTR_NAME);
    }

    private String getIdAttribute(Node node) {
        return getAttributeValue(node, ATTR_ID);
    }

    private String getAttributeValue(Node node, String name) {
        return node.getAttributes().getNamedItem(name).getNodeValue();
    }

    /**
     * Create a new document with the given tag as the root element.
     *
     * @param rootTag the tag of the root element
     * @return the document element of a new document
     */

    public Element newDocument(String rootTag) {
        DocumentBuilder builder;
        try {
            builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
        Document doc = builder.newDocument();
        Element root = doc.createElement(rootTag);
        doc.appendChild(root);
        String[] nvpairs = new String[] { "xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
                //                "xsi:noNamespaceSchemaLocation", INSTANCE_XSD,
                ATTR_VERSION, "1.0", };
        for (int i = 0; i < nvpairs.length; i += 2) {
            root.setAttribute(nvpairs[i], nvpairs[i + 1]);
        }
        return root;
    }

    Document decorate(Document doc, String uri) {
        Element root = doc.getDocumentElement();
        Element instance = (Element) root.getElementsByTagName(ELEMENT_INSTANCE).item(0);
        Element uriElement = doc.createElement(ELEMENT_URI);
        uriElement.setTextContent(uri == null ? NULL_VALUE : uri);
        root.insertBefore(uriElement, instance);
        return doc;
    }

    /**
     * Encodes the closure of a persistent instance into a XML element.
     *
     * @param visited
     * @param entity    the managed instance to be encoded. Can be null.
     * @param parent    the parent XML element to which the new XML element be added. Must not be null. Must be
     *                  owned by a document.
     * @param isRef
     * @param metaClass @return the new element. The element has been appended as a child to the given parent in this method.
     * @param view view on which loaded the entity
     */
    private Element encodeEntityInstance(HashSet<Entity> visited, final Entity entity, final Element parent,
            boolean isRef, MetaClass metaClass, View view)
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        if (!readPermitted(metaClass))
            return null;

        if (parent == null)
            throw new NullPointerException("No parent specified");

        Document doc = parent.getOwnerDocument();
        if (doc == null)
            throw new NullPointerException("No document specified");

        if (entity == null) {
            return encodeRef(parent, entity);
        }

        isRef |= !visited.add(entity);

        if (isRef) {
            return encodeRef(parent, entity);
        }
        Element root = doc.createElement(ELEMENT_INSTANCE);
        parent.appendChild(root);
        root.setAttribute(ATTR_ID, ior(entity));

        MetadataTools metadataTools = AppBeans.get(MetadataTools.NAME);
        List<MetaProperty> properties = ConverterHelper.getOrderedProperties(metaClass);
        for (MetaProperty property : properties) {
            Element child;

            if (!attrViewPermitted(metaClass, property.getName()))
                continue;

            if (!isPropertyIncluded(view, property, metadataTools)) {
                continue;
            }

            Object value = entity.getValue(property.getName());
            switch (property.getType()) {
            case DATATYPE:
                String nodeType;
                if (property.equals(metadataTools.getPrimaryKeyProperty(metaClass))
                        && !property.getJavaType().equals(String.class)) {
                    // skipping id for non-String-key entities
                    continue;
                } else if (property.getAnnotatedElement().isAnnotationPresent(Version.class)) {
                    nodeType = "version";
                } else {
                    nodeType = "basic";
                }
                child = doc.createElement(nodeType);
                child.setAttribute(ATTR_NAME, property.getName());
                if (value == null) {
                    encodeNull(child);
                } else {
                    String str = property.getRange().asDatatype().format(value);
                    encodeBasic(child, str, property.getJavaType());
                }
                break;
            case ENUM:
                child = doc.createElement("enum");
                child.setAttribute(ATTR_NAME, property.getName());
                if (value == null) {
                    encodeNull(child);
                } else {
                    //noinspection unchecked
                    String str = property.getRange().asEnumeration().format(value);
                    encodeBasic(child, str, property.getJavaType());
                }
                break;
            case COMPOSITION:
            case ASSOCIATION: {
                MetaClass meta = propertyMetaClass(property);
                //checks if the user permitted to read a property
                if (!readPermitted(meta)) {
                    child = null;
                    break;
                }

                View propertyView = (view == null ? null : view.getProperty(property.getName()).getView());

                if (!property.getRange().getCardinality().isMany()) {
                    boolean isEmbedded = property.getAnnotatedElement().isAnnotationPresent(Embedded.class);
                    child = doc.createElement(isEmbedded ? "embedded"
                            : property.getRange().getCardinality().name().replace(UNDERSCORE, DASH).toLowerCase());
                    child.setAttribute(ATTR_NAME, property.getName());
                    if (isEmbedded) {
                        encodeEntityInstance(visited, (Entity) value, child, false, property.getRange().asClass(),
                                propertyView);
                    } else {
                        encodeEntityInstance(visited, (Entity) value, child, false, property.getRange().asClass(),
                                propertyView);
                    }
                } else {
                    child = doc.createElement(getCollectionReferenceTag(property));
                    child.setAttribute(ATTR_NAME, property.getName());
                    child.setAttribute(ATTR_MEMBER_TYPE, typeOfEntityProperty(property));
                    if (value == null) {
                        encodeNull(child);
                        break;
                    }
                    Collection<?> members = (Collection<?>) value;
                    for (Object o : members) {
                        Element member = doc.createElement(ELEMENT_MEMBER);
                        child.appendChild(member);
                        if (o == null) {
                            encodeNull(member);
                        } else {
                            encodeEntityInstance(visited, (Entity) o, member, true, property.getRange().asClass(),
                                    propertyView);
                        }
                    }
                }
                break;
            }
            default:
                throw new IllegalStateException("Unknown property type");
            }

            if (child != null) {
                root.appendChild(child);
            }
        }
        return root;
    }

    private String typeOfEntityProperty(MetaProperty property) {
        return property.getRange().asClass().getName();
    }

    private MetaClass propertyMetaClass(MetaProperty property) {
        return property.getRange().asClass();
    }

    /**
     * Sets the given value element as null. The <code>null</code> attribute is set to true.
     *
     * @param element the XML element to be set
     */
    protected void encodeNull(Element element) {
        element.setAttribute(ATTR_NULL, "true");
    }

    protected boolean isPropertyIncluded(View view, MetaProperty metaProperty, MetadataTools metadataTools) {
        if (view == null) {
            return true;
        }

        ViewProperty viewProperty = view.getProperty(metaProperty.getName());
        return (viewProperty != null);
    }

    protected boolean isNullValue(Node fieldNode) {
        Node nullAttr = fieldNode.getAttributes().getNamedItem(ATTR_NULL);
        return nullAttr == null ? false : "true".equals(nullAttr.getNodeValue());
    }

    protected Element encodeRef(Element parent, Entity entity) {
        Element ref = parent.getOwnerDocument().createElement(entity == null ? ELEMENT_NULL_REF : ELEMENT_REF);
        if (entity != null)
            ref.setAttribute(ATTR_ID, ior(entity));

        // IMPORTANT: for xml transformer not to omit the closing tag, otherwise dojo is confused
        ref.setTextContent(EMPTY_TEXT);
        parent.appendChild(ref);
        return ref;
    }

    /**
     * Sets the given value element. The <code>type</code> is set to the given runtime type.
     * String form of the given object is set as the text content.
     *
     * @param element     the XML element to be set
     * @param obj         value of the element. Never null.
     * @param runtimeType attribute type
     */
    protected void encodeBasic(Element element, Object obj, Class<?> runtimeType) {
        element.setTextContent(obj == null ? NULL_VALUE : obj.toString());
    }

    String ior(Entity entity) {
        return EntityLoadInfo.create(entity).toString();
    }

    String typeOf(Class<?> cls) {
        return cls.getSimpleName();
    }

    protected String getCollectionReferenceTag(MetaProperty property) {
        return property.getRange().getCardinality().name().replace(UNDERSCORE, DASH).toLowerCase();
    }

    protected MetaClass getMetaClass(Entity entity) {
        Metadata metadata = AppBeans.get(Metadata.NAME);
        return metadata.getSession().getClass(entity.getClass());
    }

    protected boolean attrViewPermitted(MetaClass metaClass, String property) {
        return attrPermitted(metaClass, property, EntityAttrAccess.VIEW);
    }

    protected boolean attrModifyPermitted(MetaClass metaClass, String property) {
        return attrPermitted(metaClass, property, EntityAttrAccess.MODIFY);
    }

    protected boolean attrPermitted(MetaClass metaClass, String property, EntityAttrAccess entityAttrAccess) {
        Security security = AppBeans.get(Security.NAME);
        return security.isEntityAttrPermitted(metaClass, property, entityAttrAccess);
    }

    protected boolean readPermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.READ);
    }

    protected boolean updatePermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.UPDATE);
    }

    protected boolean entityOpPermitted(MetaClass metaClass, EntityOp entityOp) {
        Security security = AppBeans.get(Security.NAME);
        return security.isEntityOpPermitted(metaClass, entityOp);
    }

    @Override
    public String processServiceMethodResult(Object result, Class resultType)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        throw new UnsupportedOperationException();
    }

    @Override
    public ServiceRequest parseServiceRequest(String content) throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public Entity parseEntity(String content) {
        throw new UnsupportedOperationException();
    }

    @Override
    public QueryRequest parseQueryRequest(String content) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Collection parseEntitiesCollection(String content, Class<? extends Collection> collectionClass) {
        throw new UnsupportedOperationException();
    }

    @Override
    public List<Integer> getApiVersions() {
        return Arrays.asList(1);
    }

    protected String documentToString(Document document) {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
            transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

            StringWriter sw = new StringWriter();
            transformer.transform(new DOMSource(document), new StreamResult(sw));
            return sw.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}