MavenPropertiesCompatibilityTest.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.cling.props;

import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.utils.StringInterpolator;
import org.apache.maven.utils.StringInterpolatorException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.io.TempDir;

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

@SuppressWarnings("deprecation")
class MavenPropertiesCompatibilityTest {
    @TempDir
    Path directory;

    @Test
    void preservesConstructorsAndSavedLayout() throws Exception {
        Path file = directory.resolve("settings.properties");
        String sep = System.lineSeparator();
        String text = "# Configuration" + sep + "name=Sahil" + sep + "message=Hello ${name}" + sep;
        Files.writeString(file, text);

        MavenProperties properties = new MavenProperties(file);
        assertEquals("Hello Sahil", properties.getProperty("message"));
        properties.save();
        assertEquals(text, Files.readString(file));
        assertEquals("Hello Sahil", new MavenProperties(file, key -> null).getProperty("message"));

        MavenProperties unresolved = new MavenProperties(file, false);
        unresolved.load(file);
        assertEquals("Hello ${name}", unresolved.getProperty("message"));

        MavenProperties callback = new MavenProperties(file, key -> "external");
        assertEquals("Sahil", callback.getProperty("name"));
    }

    @Test
    void preservesNestedReaderWriterAndLayoutTypes() throws Exception {
        StringWriter output = new StringWriter();
        try (MavenProperties.PropertiesWriter writer = new MavenProperties.PropertiesWriter(output, false)) {
            writer.writeProperty("name", "Sahil");
        }
        assertEquals("name = Sahil" + System.lineSeparator(), output.toString());

        try (MavenProperties.PropertiesReader reader =
                new MavenProperties.PropertiesReader(new StringReader(output.toString()), false)) {
            assertTrue(reader.nextProperty());
            assertEquals("name", reader.getPropertyName());
            assertEquals("Sahil", reader.getPropertyValue());
        }

        MavenProperties.Layout layout = new MavenProperties.Layout(List.of("# comment"), List.of("name=Sahil"));
        assertEquals(List.of("# comment"), layout.getCommentLines());
        assertEquals(List.of("name=Sahil"), layout.getValueLines());
    }

    @Test
    void preservesTheLegacyTypedUpdateOverride() {
        class TrackingProperties extends MavenProperties {
            boolean called;

            @Override
            public boolean update(MavenProperties properties) {
                called = true;
                return super.update(properties);
            }
        }

        TrackingProperties properties = new TrackingProperties();
        Map<String, String> values = Map.of("name", "Sahil");
        assertTrue(properties.update(values));
        assertTrue(properties.called);
        assertEquals("Sahil", properties.getProperty("name"));
    }

    @Test
    void preservesMavenExceptionsForRecursiveReferences() throws Exception {
        MavenProperties properties = new MavenProperties(false);
        properties.put("name", "${name}");
        assertRecursiveFailure(properties::substitute);
        assertRecursiveFailure(() -> MavenPropertiesLoader.substitute(properties, null));
        assertRecursiveFailure(() -> MavenPropertiesLoader.substVars("${name}", "name", null, null));

        Path file = directory.resolve("recursive.properties");
        Files.writeString(file, "name=${name}\n");
        assertRecursiveFailure(() -> new MavenProperties(file));
        assertRecursiveFailure(
                () -> MavenPropertiesLoader.loadProperties(new java.util.Properties(), file, null, false));
    }

    private void assertRecursiveFailure(Executable action) {
        InterpolatorException error = assertThrows(InterpolatorException.class, action);
        StringInterpolatorException cause = assertInstanceOf(StringInterpolatorException.class, error.getCause());
        assertEquals("recursive variable reference: name", error.getMessage());
        assertEquals(error.getMessage(), cause.getMessage());
        assertTrue(java.util.Arrays.stream(cause.getStackTrace())
                .anyMatch(frame -> StringInterpolator.class.getName().equals(frame.getClassName())));
    }

    @Test
    void retainsTheOriginalUtilityExceptionAndItsCause() throws Exception {
        StringInterpolatorException failure = new StringInterpolatorException("utility callback failed");
        failure.initCause(new IllegalStateException("original failure"));
        java.util.function.UnaryOperator<String> callback = key -> {
            throw failure;
        };
        MavenProperties properties = new MavenProperties(false);
        properties.put("message", "${external}");
        assertSame(
                failure,
                assertThrows(InterpolatorException.class, () -> properties.substitute(callback))
                        .getCause());
        assertSame(
                failure,
                assertThrows(InterpolatorException.class, () -> MavenPropertiesLoader.substitute(properties, callback))
                        .getCause());
        assertSame(
                failure,
                assertThrows(
                                InterpolatorException.class,
                                () -> MavenPropertiesLoader.substVars("${external}", "message", null, callback))
                        .getCause());
        Path file = directory.resolve("callback.properties");
        Files.writeString(file, "message=${external}\n");
        assertSame(
                failure,
                assertThrows(InterpolatorException.class, () -> new MavenProperties(file, callback))
                        .getCause());
        assertSame(
                failure,
                assertThrows(
                                InterpolatorException.class,
                                () -> MavenPropertiesLoader.loadProperties(
                                        new java.util.Properties(), file, callback, false))
                        .getCause());
    }

    @Test
    void preservesExceptionsRaisedByCallbacks() {
        MavenProperties properties = new MavenProperties(false);
        properties.put("message", "${external}");
        IllegalArgumentException failure = new IllegalArgumentException("callback failed");
        assertSame(
                failure,
                assertThrows(
                        IllegalArgumentException.class,
                        () -> properties.substitute(key -> {
                            throw failure;
                        })));
    }
}