@Nullable
    public static String replaceMacro(@CheckForNull String s, @NonNull Map<String, String> properties) {
        return replaceMacro(s, new VariableResolver.ByMap<>(properties));
    }
@Test
    public void testReplaceMacro() {
        Map<String, String> m = new HashMap<>();
        m.put("A", "a");
        m.put("A.B", "a-b");
        m.put("AA", "aa");
        m.put("B", "B");
        m.put("DOLLAR", "$");
        m.put("ENCLOSED", "a${A}");

        // longest match
        assertEquals("aa", Util.replaceMacro("$AA", m));

        // invalid keys are ignored
        assertEquals("$AAB", Util.replaceMacro("$AAB", m));

        assertEquals("aaB", Util.replaceMacro("${AA}B", m));
        assertEquals("${AAB}", Util.replaceMacro("${AAB}", m));

        // $ escaping
        assertEquals("asd$${AA}dd", Util.replaceMacro("asd$$$${AA}dd", m));
        assertEquals("$", Util.replaceMacro("$$", m));
        assertEquals("$$", Util.replaceMacro("$$$$", m));

        // dots
        assertEquals("a.B", Util.replaceMacro("$A.B", m));
        assertEquals("a-b", Util.replaceMacro("${A.B}", m));

        // test that more complex scenarios work
        assertEquals("/a/B/aa", Util.replaceMacro("/$A/$B/$AA", m));
        assertEquals("a-aa", Util.replaceMacro("$A-$AA", m));
        assertEquals("/a/foo/can/B/you-believe_aa~it?", Util.replaceMacro("/$A/foo/can/$B/you-believe_$AA~it?", m));
        assertEquals("$$aa$Ba${A}$it", Util.replaceMacro("$$$DOLLAR${AA}$$B${ENCLOSED}$it", m));
    }