/*
 * Copyright 2020 Red Hat, Inc. and/or its affiliates.
 *
 * 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.optaplanner.core.impl.io.jaxb;

import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.optaplanner.core.impl.io.XmlUnmarshallingException;

public final class JaxbIO<T> {
    private static final int DEFAULT_INDENTATION = 2;

    private final Unmarshaller unmarshaller;
    private final Marshaller marshaller;
    private final Class<T> rootClass;
    private final int indentation;

    public JaxbIO(Class<T> rootClass) {
        this(rootClass, DEFAULT_INDENTATION);
    }

    public JaxbIO(Class<T> rootClass, int indentation) {
        Objects.requireNonNull(rootClass);
        this.rootClass = rootClass;
        this.indentation = indentation;
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(rootClass);
            unmarshaller = jaxbContext.createUnmarshaller();
            marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.toString());
        } catch (JAXBException jaxbException) {
            String errMessage = String.format("Unable to create JAXB marshaller or unmarshaller for a root element class %s.",
                    rootClass.getName());
            throw new IllegalStateException(errMessage, jaxbException);
        }
    }

    public T read(Reader reader) {
        Objects.requireNonNull(reader);
        try {
            return (T) unmarshaller.unmarshal(reader);
        } catch (JAXBException jaxbException) {
            String errMessage = String.format("Unable to read the %s from XML.", rootClass.getName());
            throw new XmlUnmarshallingException(errMessage, jaxbException);
        }
    }

    public void write(T root, Writer writer) {
        Objects.requireNonNull(root);
        Objects.requireNonNull(writer);
        DOMResult domResult = new DOMResult();
        try {
            marshaller.marshal(root, domResult);
        } catch (JAXBException jaxbException) {
            String errMessage = String.format("Unable to marshall the %s to XML.", rootClass.getName());
            throw new RuntimeException(errMessage, jaxbException);
        }

        // see https://stackoverflow.com/questions/46708498/jaxb-marshaller-indentation
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            /*
             * Disable usage of external entities, see:
             * https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A4-XML_External_Entities_(XXE)
             */
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");

            transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indentation));

            transformer.transform(new DOMSource(domResult.getNode()), new StreamResult(writer));
        } catch (TransformerException transformerException) {
            String errMessage = String.format("Unable to format %s XML.", rootClass.getName());
            throw new RuntimeException(errMessage, transformerException);
        }
    }
}

@@ -778,39 +778,8 @@
 s;%0A%0A
-import javax.xml.XMLConstants;%0A
 impo
@@ -3353,17 +3353,17 @@
      // 
-s
+S
 ee https
@@ -3429,16 +3429,18 @@
 entation
+.%0A
 %0A       
@@ -3444,204 +3444,229 @@
     
-TransformerFactory transformerFactory = TransformerFactory.newInstance();%0A        Transformer transformer;%0A        try %7B%0A            /*%0A             * Disable usage of external entities, see:%0A    
+/*%0A         * The code is not vulnerable to XXE-based attacks as it does not process any external XML nor XSL input.%0A         * Should the transformerFactory be used for such purposes, it has to be appropriately secured:%0A
     
@@ -3785,27 +3785,77 @@
     
+*/%0A
     
-*/%0A
     
+@SuppressWarnings(%7B %22java:S2755%22, %22java:S4435%22 %7D)%0A
         
 tran
@@ -3842,33 +3842,33 @@
 435%22 %7D)%0A        
-t
+T
 ransformerFactor
@@ -3872,150 +3872,77 @@
 tory
-.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, %22%22);%0A            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, %22%22);
+ transformerFactory = TransformerFactory.newInstance();%0A        try %7B
 %0A%0A  
@@ -3943,32 +3943,44 @@
  %7B%0A%0A            
+Transformer 
 transformer = Tr
@@ -3977,17 +3977,17 @@
 ormer = 
-T
+t
 ransform
@@ -4000,22 +4000,8 @@
 ory.
-newInstance().
 newT

Suppress a false positive issue from SonarCloud