ConfigurationParser.java

/*
 *    Copyright 2006-2026 the original author or authors.
 *
 *    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
 *
 *       https://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.mybatis.generator.config.xml;

import static org.mybatis.generator.internal.util.messages.Messages.getString;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.jspecify.annotations.Nullable;
import org.mybatis.generator.codegen.XmlConstants;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.exception.XMLParserException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class ConfigurationParser {
    private final List<String> warnings = new ArrayList<>();
    private final List<String> parseErrors = new ArrayList<>();
    private final @Nullable Properties extraProperties;

    public ConfigurationParser() {
        this(null);
    }

    /**
     * This constructor accepts a properties object which may be used to specify
     * an additional property set.  Typically, this property set will be Ant or Maven properties
     * specified in the build.xml file or the POM.
     *
     * <p>If there are name collisions between the different property sets, they will be
     * resolved in this order:
     *
     * <ol>
     *   <li>System properties take the highest precedence</li>
     *   <li>Properties specified in the &lt;properties&gt; configuration
     *       element are next</li>
     *   <li>Properties specified in this "extra" property set are
     *       the lowest precedence.</li>
     * </ol>
     *
     * @param extraProperties an (optional) set of properties used to resolve property
     *     references in the configuration file
     */
    public ConfigurationParser(@Nullable Properties extraProperties) {
        this.extraProperties = extraProperties;
    }

    public List<String> getWarnings() {
        return Collections.unmodifiableList(warnings);
    }

    public Configuration parseConfiguration(File inputFile) throws IOException, XMLParserException {
        try (BufferedReader fr = Files.newBufferedReader(inputFile.toPath())) {
            return parseConfiguration(fr);
        }
    }

    public Configuration parseConfiguration(Reader reader) throws IOException, XMLParserException {
        InputSource is = new InputSource(reader);
        return parseConfiguration(is);
    }

    public Configuration parseConfiguration(InputStream inputStream) throws IOException, XMLParserException {
        InputSource is = new InputSource(inputStream);
        return parseConfiguration(is);
    }

    private Configuration parseConfiguration(InputSource inputSource) throws IOException, XMLParserException {
        parseErrors.clear();
        warnings.clear();

        try {
            Document document = basicParse(inputSource);

            if (document == null || !parseErrors.isEmpty()) {
                throw new XMLParserException(getString("RuntimeError.31"), parseErrors); //$NON-NLS-1$
            }

            Configuration config;
            Element rootNode = document.getDocumentElement();
            DocumentType docType = document.getDoctype();
            if (rootNode.getNodeType() == Node.ELEMENT_NODE
                    && docType.getPublicId().equals(XmlConstants.MYBATIS_GENERATOR_CONFIG_PUBLIC_ID)) {
                config = parseMyBatisGeneratorConfiguration(rootNode);
            } else {
                throw new XMLParserException(getString("RuntimeError.5")); //$NON-NLS-1$
            }

            if (!parseErrors.isEmpty()) {
                throw new XMLParserException(getString("RuntimeError.31"), parseErrors); //$NON-NLS-1$
            }

            return config;
        } catch (ParserConfigurationException e) {
            throw new XMLParserException(e.getMessage(), e, parseErrors);
        }
    }

    private @Nullable Document basicParse(InputSource inputSource) throws IOException, ParserConfigurationException,
            XMLParserException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); //$NON-NLS-1$
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); //$NON-NLS-1$
        factory.setValidating(true);

        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        builder.setEntityResolver(new ParserEntityResolver());

        ParserErrorHandler handler = new ParserErrorHandler(warnings, parseErrors);
        builder.setErrorHandler(handler);

        Document document = null;
        try {
            document = builder.parse(inputSource);
        } catch (SAXParseException e) {
            throw new XMLParserException(e.getMessage(), e, parseErrors);
        } catch (SAXException e) {
            parseErrors.add(e.getMessage());
            if (e.getException() != null) {
                parseErrors.add(e.getException().getMessage());
            }
        }

        return document;
    }

    private Configuration parseMyBatisGeneratorConfiguration(Element rootNode) throws XMLParserException {
        MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(extraProperties, warnings);
        return parser.parseConfiguration(rootNode);
    }
}