MavenStaxReaderTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.model.v4;

import javax.xml.stream.XMLStreamException;

import java.io.StringReader;

import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Model;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MavenStaxReaderTest {

    @Test
    void testNamespaceReporting() throws Exception {
        String xml = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "  <modelVersion>4.0.0</modelVersion>\n"
                + "</project>";

        Model model = fromXml(xml);
        assertEquals("http://maven.apache.org/POM/4.0.0", model.getNamespaceUri());
    }

    @Test
    void testEmptyNamespaceReporting() throws Exception {
        String xml = "<project>\n" + "  <modelVersion>4.0.0</modelVersion>\n" + "</project>";

        Model model = fromXml(xml);
        assertEquals("", model.getNamespaceUri());
    }

    @Test
    void testNamespaceConsistency() throws XMLStreamException {
        String xml = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "  <build xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "    <plugins>\n"
                + "      <plugin>\n"
                + "        <artifactId>maven-test-plugin</artifactId>\n"
                + "      </plugin>\n"
                + "    </plugins>\n"
                + "  </build>\n"
                + "</project>";

        Model model = fromXml(xml);
        assertEquals("http://maven.apache.org/POM/4.0.0", model.getNamespaceUri());
    }

    @Test
    void testNamespaceInconsistencyThrows() {
        String xml = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "  <build xmlns=\"http://maven.apache.org/POM/4.1.0\">\n"
                + "    <plugins>\n"
                + "      <plugin>\n"
                + "        <artifactId>maven-test-plugin</artifactId>\n"
                + "      </plugin>\n"
                + "    </plugins>\n"
                + "  </build>\n"
                + "</project>";

        XMLStreamException ex = assertThrows(XMLStreamException.class, () -> fromXml(xml));
        assertTrue(ex.getMessage().contains("Unexpected namespace for element 'build'"));
        assertTrue(ex.getMessage().contains("found 'http://maven.apache.org/POM/4.1.0'"));
        assertTrue(ex.getMessage().contains("expected 'http://maven.apache.org/POM/4.0.0'"));
    }

    @Test
    void testEmptyNamespaceConsistency() throws XMLStreamException {
        String xml = "<project>\n"
                + "  <build>\n"
                + "    <plugins>\n"
                + "      <plugin>\n"
                + "        <artifactId>maven-test-plugin</artifactId>\n"
                + "      </plugin>\n"
                + "    </plugins>\n"
                + "  </build>\n"
                + "</project>";

        Model model = fromXml(xml);
        assertEquals("", model.getNamespaceUri());
    }

    @Test
    void testEmptyNamespaceInconsistencyThrows() {
        String xml = "<project>\n"
                + "  <build xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "    <plugins>\n"
                + "      <plugin>\n"
                + "        <artifactId>maven-test-plugin</artifactId>\n"
                + "      </plugin>\n"
                + "    </plugins>\n"
                + "  </build>\n"
                + "</project>";

        XMLStreamException ex = assertThrows(XMLStreamException.class, () -> fromXml(xml));
        assertTrue(ex.getMessage().contains("Unexpected namespace for element 'build'"));
        assertTrue(ex.getMessage().contains("found 'http://maven.apache.org/POM/4.0.0'"));
        assertTrue(ex.getMessage().contains("expected ''"));
    }

    @Test
    void testPluginConfigurationAllowsOtherNamespaces() throws XMLStreamException {
        String xml = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
                + "  <build>\n"
                + "    <plugins>\n"
                + "      <plugin>\n"
                + "        <artifactId>maven-test-plugin</artifactId>\n"
                + "        <configuration>\n"
                + "          <customConfig xmlns:custom=\"http://custom.namespace.org\">\n"
                + "            <custom:element>value</custom:element>\n"
                + "          </customConfig>\n"
                + "        </configuration>\n"
                + "      </plugin>\n"
                + "    </plugins>\n"
                + "  </build>\n"
                + "</project>";

        Model model = fromXml(xml);
        assertNotNull(model);
        assertEquals("http://maven.apache.org/POM/4.0.0", model.getNamespaceUri());
    }

    @Test
    void testLocationReportingForElements() throws Exception {
        String xml = "<project>\n"
                + "  <modelVersion>4.0.0</modelVersion>\n"
                + "  <groupId>org.example</groupId>\n"
                + "  <artifactId>test-artifact</artifactId>\n"
                + "  <version>1.0.0</version>\n"
                + "  <dependencies>\n"
                + "    <dependency>\n"
                + "      <groupId>junit</groupId>\n"
                + "      <artifactId>junit</artifactId>\n"
                + "      <version>4.13.2</version>\n"
                + "    </dependency>\n"
                + "  </dependencies>\n"
                + "</project>";

        MavenStaxReader reader = new MavenStaxReader();
        reader.setAddLocationInformation(true);
        Model model = reader.read(new StringReader(xml), true, InputSource.of("test.xml"));

        // Check root element location - should point to <project> tag on line 1, column 1
        InputLocation projectLocation = model.getLocation("");
        assertNotNull(projectLocation, "Project location should not be null");
        assertEquals(1, projectLocation.getLineNumber(), "Project should start at line 1");
        assertEquals(1, projectLocation.getColumnNumber(), "Project should start at column 1");

        // Check modelVersion location - should point to <modelVersion> tag on line 2, column 3
        InputLocation modelVersionLocation = model.getLocation("modelVersion");
        assertNotNull(modelVersionLocation, "ModelVersion location should not be null");
        assertEquals(2, modelVersionLocation.getLineNumber(), "ModelVersion should start at line 2");
        assertEquals(3, modelVersionLocation.getColumnNumber(), "ModelVersion should start at column 3");

        // Check groupId location - should point to <groupId> tag on line 3, column 3
        InputLocation groupIdLocation = model.getLocation("groupId");
        assertNotNull(groupIdLocation, "GroupId location should not be null");
        assertEquals(3, groupIdLocation.getLineNumber(), "GroupId should start at line 3");
        assertEquals(3, groupIdLocation.getColumnNumber(), "GroupId should start at column 3");

        // Check dependencies location - should point to <dependencies> tag on line 6, column 3
        InputLocation dependenciesLocation = model.getLocation("dependencies");
        assertNotNull(dependenciesLocation, "Dependencies location should not be null");
        assertEquals(6, dependenciesLocation.getLineNumber(), "Dependencies should start at line 6");
        assertEquals(3, dependenciesLocation.getColumnNumber(), "Dependencies should start at column 3");

        // Check dependency location - should point to <dependency> tag on line 7, column 5
        Dependency dependency = model.getDependencies().get(0);
        InputLocation dependencyLocation = dependency.getLocation("");
        assertNotNull(dependencyLocation, "Dependency location should not be null");
        assertEquals(7, dependencyLocation.getLineNumber(), "Dependency should start at line 7");
        assertEquals(5, dependencyLocation.getColumnNumber(), "Dependency should start at column 5");

        // Check dependency groupId location - should point to <groupId> tag on line 8, column 7
        InputLocation depGroupIdLocation = dependency.getLocation("groupId");
        assertNotNull(depGroupIdLocation, "Dependency groupId location should not be null");
        assertEquals(8, depGroupIdLocation.getLineNumber(), "Dependency groupId should start at line 8");
        assertEquals(7, depGroupIdLocation.getColumnNumber(), "Dependency groupId should start at column 7");
    }

    @Test
    void testLocationReportingForAttributes() throws Exception {
        String xml = "<project root=\"true\">\n"
                + "  <modelVersion>4.0.0</modelVersion>\n"
                + "  <groupId>org.example</groupId>\n"
                + "  <artifactId>test-artifact</artifactId>\n"
                + "  <version>1.0.0</version>\n"
                + "  <scm child.scm.connection.inherit.append.path=\"false\">\n"
                + "    <connection>scm:git:https://github.com/example/repo.git</connection>\n"
                + "  </scm>\n"
                + "</project>";

        MavenStaxReader reader = new MavenStaxReader();
        reader.setAddLocationInformation(true);
        Model model = reader.read(new StringReader(xml), true, InputSource.of("test.xml"));

        // Check project root attribute - attributes get the location of their containing element
        // since XMLStreamReader doesn't provide individual attribute positions
        InputLocation rootLocation = model.getLocation("root");
        assertNotNull(rootLocation, "Root attribute location should not be null");
        assertEquals(1, rootLocation.getLineNumber(), "Root attribute should be on line 1 (element line)");
        assertEquals(1, rootLocation.getColumnNumber(), "Root attribute should point to column 1 (element column)");
        assertTrue(model.isRoot(), "Root should be true");

        // Check scm element location
        InputLocation scmLocation = model.getScm().getLocation("");
        assertNotNull(scmLocation, "SCM location should not be null");
        assertEquals(6, scmLocation.getLineNumber(), "SCM should start at line 6");
        assertEquals(3, scmLocation.getColumnNumber(), "SCM should start at column 3");

        // Check scm child.scm.connection.inherit.append.path attribute
        // Like all attributes, it gets the location of its containing element
        InputLocation scmInheritLocation = model.getScm().getLocation("child.scm.connection.inherit.append.path");
        assertNotNull(scmInheritLocation, "SCM inherit attribute location should not be null");
        assertEquals(6, scmInheritLocation.getLineNumber(), "SCM inherit attribute should be on line 6 (element line)");
        assertEquals(
                3,
                scmInheritLocation.getColumnNumber(),
                "SCM inherit attribute should point to column 3 (element column)");
        assertEquals("false", model.getScm().getChildScmConnectionInheritAppendPath());
    }

    @Test
    void testLocationReportingForListElements() throws Exception {
        String xml = "<project>\n"
                + "  <modelVersion>4.0.0</modelVersion>\n"
                + "  <modules>\n"
                + "    <module>module1</module>\n"
                + "    <module>module2</module>\n"
                + "    <module>module3</module>\n"
                + "  </modules>\n"
                + "</project>";

        MavenStaxReader reader = new MavenStaxReader();
        reader.setAddLocationInformation(true);
        Model model = reader.read(new StringReader(xml), true, InputSource.of("test.xml"));

        // Check modules location - should point to <modules> tag on line 3, column 3
        InputLocation modulesLocation = model.getLocation("modules");
        assertNotNull(modulesLocation, "Modules location should not be null");
        assertEquals(3, modulesLocation.getLineNumber(), "Modules should start at line 3");
        assertEquals(3, modulesLocation.getColumnNumber(), "Modules should start at column 3");

        // Check individual module locations
        InputLocation module1Location = modulesLocation.getLocation(0);
        assertNotNull(module1Location, "Module 1 location should not be null");
        assertEquals(4, module1Location.getLineNumber(), "Module 1 should start at line 4");
        assertEquals(5, module1Location.getColumnNumber(), "Module 1 should start at column 5");

        InputLocation module2Location = modulesLocation.getLocation(1);
        assertNotNull(module2Location, "Module 2 location should not be null");
        assertEquals(5, module2Location.getLineNumber(), "Module 2 should start at line 5");
        assertEquals(5, module2Location.getColumnNumber(), "Module 2 should start at column 5");

        InputLocation module3Location = modulesLocation.getLocation(2);
        assertNotNull(module3Location, "Module 3 location should not be null");
        assertEquals(6, module3Location.getLineNumber(), "Module 3 should start at line 6");
        assertEquals(5, module3Location.getColumnNumber(), "Module 3 should start at column 5");
    }

    private Model fromXml(String xml) throws XMLStreamException {
        MavenStaxReader reader = new MavenStaxReader();
        return reader.read(new StringReader(xml), true, null);
    }
}