001/*
002 * Copyright 2017-2021 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.testresources;
017
018import org.apache.maven.AbstractMavenLifecycleParticipant;
019import org.apache.maven.execution.MavenSession;
020import org.apache.maven.model.Build;
021import org.apache.maven.model.Plugin;
022import org.apache.maven.plugin.MojoExecution;
023import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
024import org.apache.maven.project.MavenProject;
025import org.codehaus.plexus.component.annotations.Component;
026import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
027import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
028import org.codehaus.plexus.logging.Logger;
029import org.codehaus.plexus.util.xml.Xpp3Dom;
030import org.codehaus.plexus.util.xml.Xpp3DomWriter;
031import org.twdata.maven.mojoexecutor.MojoExecutor;
032
033import javax.inject.Inject;
034import java.io.File;
035import java.io.StringWriter;
036import java.util.List;
037import java.util.Map;
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.function.Consumer;
040
041import static io.micronaut.maven.MojoUtils.THIS_PLUGIN;
042import static io.micronaut.maven.testresources.AbstractTestResourcesMojo.CONFIG_PROPERTY_PREFIX;
043import static io.micronaut.maven.testresources.StopTestResourcesServerMojo.MICRONAUT_TEST_RESOURCES_KEEPALIVE;
044
045/**
046 * A lifecycle extension which determines if the test resources server should
047 * be stopped when the build is complete.
048 */
049@Component(role = AbstractMavenLifecycleParticipant.class, hint = "test-resources")
050public class TestResourcesLifecycleExtension extends AbstractMavenLifecycleParticipant {
051
052    private static final String EXPLICIT_START_SERVICE_GOAL_NAME = "mn:" + StartTestResourcesServerMojo.NAME;
053    private static final String EXPLICIT_STOP_SERVICE_GOAL_NAME = "mn:" + StopTestResourcesServerMojo.NAME;
054
055    private final Map<MavenProject, ExpressionEvaluator> perProjectEvaluator = new ConcurrentHashMap<>();
056    private final Map<MavenProject, TestResourcesConfiguration> perProjectConfiguration = new ConcurrentHashMap<>();
057
058    private final Logger logger;
059
060    @Inject
061    @SuppressWarnings("CdiInjectionPointsInspection")
062    public TestResourcesLifecycleExtension(Logger logger) {
063        this.logger = logger;
064    }
065
066    @Override
067    public void afterProjectsRead(MavenSession session) {
068        session.getAllProjects().forEach(currentProject -> {
069            Build build = currentProject.getBuild();
070            withPlugin(build, plugin -> {
071                ExpressionEvaluator evaluator = perProjectEvaluator.computeIfAbsent(currentProject, p -> initEvaluator(p, session));
072                TestResourcesConfiguration configuration = perProjectConfiguration.computeIfAbsent(currentProject, mavenProject -> initConfiguration(plugin));
073
074                boolean enabled = isEnabled(evaluator, configuration);
075                if (enabled) {
076                    List<String> goals = session.getGoals();
077                    if (goals.stream().anyMatch(EXPLICIT_START_SERVICE_GOAL_NAME::equals)) {
078                        // we need to keep the server alive at the end of the build
079
080                        Xpp3Dom flag = new Xpp3Dom(MICRONAUT_TEST_RESOURCES_KEEPALIVE);
081                        Xpp3Dom pluginConfiguration = (Xpp3Dom) plugin.getConfiguration();
082                        pluginConfiguration.addChild(flag);
083                        flag.setValue("true");
084                    }
085                }
086            });
087        });
088    }
089
090    @Override
091    public void afterSessionEnd(MavenSession session) {
092        if (session.getGoals().stream().noneMatch(s -> s.equals(EXPLICIT_START_SERVICE_GOAL_NAME) || s.equals(EXPLICIT_STOP_SERVICE_GOAL_NAME))) {
093            session.getAllProjects().forEach(currentProject -> {
094                Build build = currentProject.getBuild();
095                withPlugin(build, plugin -> {
096                    ExpressionEvaluator evaluator = perProjectEvaluator.computeIfAbsent(currentProject, p -> initEvaluator(p, session));
097                    TestResourcesConfiguration configuration = perProjectConfiguration.computeIfAbsent(currentProject, mavenProject -> initConfiguration(plugin));
098                    boolean enabled = isEnabled(evaluator, configuration);
099                    boolean shared = isShared(evaluator, configuration);
100                    File buildDirectory = new File(build.getDirectory());
101
102                    var helper = new TestResourcesHelper(session, enabled, shared, buildDirectory);
103                    if (shared) {
104                        String sharedServerNamespace = findSharedServerNamespace(evaluator, configuration);
105                        helper.setSharedServerNamespace(sharedServerNamespace);
106                    }
107                    try {
108                        helper.stop(true);
109                    } catch (Exception e) {
110                        logger.error(e.getMessage(), e);
111                    }
112                });
113            });
114        }
115    }
116
117    private String findSharedServerNamespace(ExpressionEvaluator evaluator, TestResourcesConfiguration configuration) {
118        try {
119            String result = (String) evaluator.evaluate("${" + CONFIG_PROPERTY_PREFIX + "namespace" + "}");
120            if (result != null) {
121                return result;
122            } else if (configuration != null) {
123                return configuration.getSharedServerNamespace();
124            }
125        } catch (ExpressionEvaluationException e) {
126            return null;
127        }
128        return null;
129    }
130
131    private boolean isShared(ExpressionEvaluator evaluator, TestResourcesConfiguration configuration) {
132        Boolean result = evaluateBooleanProperty(evaluator, CONFIG_PROPERTY_PREFIX + "shared");
133        if (result != null) {
134            return result;
135        } else if (configuration != null) {
136            return configuration.isShared();
137        }
138        return false;
139    }
140
141    private boolean isEnabled(ExpressionEvaluator evaluator, TestResourcesConfiguration configuration) {
142        Boolean result = evaluateBooleanProperty(evaluator, CONFIG_PROPERTY_PREFIX + "enabled");
143        if (result != null) {
144            return result;
145        } else if (configuration != null) {
146            return configuration.isTestResourcesEnabled();
147        }
148        return false;
149    }
150
151    private TestResourcesConfiguration initConfiguration(Plugin plugin) {
152        var configuration = (Xpp3Dom) plugin.getConfiguration();
153        if (configuration == null) {
154            configuration = MojoExecutor.configuration();
155            plugin.setConfiguration(configuration);
156        }
157        var writer = new StringWriter();
158        Xpp3DomWriter.write(writer, configuration);
159        return parseConfiguration(configuration);
160    }
161
162    private TestResourcesConfiguration parseConfiguration(Xpp3Dom dom) {
163        TestResourcesConfiguration config = null;
164        if (dom != null) {
165            config = new TestResourcesConfiguration();
166
167            Xpp3Dom testResourcesEnabled = dom.getChild("testResourcesEnabled");
168            if (testResourcesEnabled != null) {
169                config.setTestResourcesEnabled(Boolean.parseBoolean(testResourcesEnabled.getValue()));
170            }
171
172            Xpp3Dom shared = dom.getChild("shared");
173            if (shared != null) {
174                config.setShared(Boolean.parseBoolean(shared.getValue()));
175            }
176
177            Xpp3Dom sharedServerNamespace = dom.getChild("sharedServerNamespace");
178            if (sharedServerNamespace != null) {
179                config.setSharedServerNamespace(sharedServerNamespace.getValue());
180            }
181
182            Xpp3Dom debugServer = dom.getChild("debugServer");
183            if (debugServer != null) {
184                config.setShared(Boolean.parseBoolean(debugServer.getValue()));
185            }
186        }
187        return config;
188    }
189
190    private Boolean evaluateBooleanProperty(ExpressionEvaluator evaluator, String property) {
191        try {
192            Object result = evaluator.evaluate("${" + property + "}");
193            if (result instanceof Boolean b) {
194                return b;
195            }
196            if (result instanceof String s && (s.equals(Boolean.TRUE.toString()) || s.equals(Boolean.FALSE.toString()))) {
197                return Boolean.parseBoolean(s);
198            }
199        } catch (ExpressionEvaluationException e) {
200            return false;
201        }
202        return null;
203    }
204
205    private ExpressionEvaluator initEvaluator(MavenProject currentProject, MavenSession session) {
206        Plugin thisPlugin = currentProject.getPlugin(THIS_PLUGIN);
207        var execution = new MojoExecution(thisPlugin, null, null);
208        MavenProject actualCurrentProject = session.getCurrentProject();
209        ExpressionEvaluator evaluator;
210
211        // Maven 3: PluginParameterExpressionEvaluator gets the current project from the session:
212        // synchronize in case another thread wants to fetch the real current project in between
213        synchronized (perProjectEvaluator) {
214            session.setCurrentProject(currentProject);
215            evaluator = new PluginParameterExpressionEvaluator(session, execution);
216            session.setCurrentProject(actualCurrentProject);
217        }
218
219        return evaluator;
220    }
221
222    private static void withPlugin(Build build, Consumer<? super Plugin> consumer) {
223        build.getPlugins()
224            .stream()
225            .filter(p -> THIS_PLUGIN.equals(p.getGroupId() + ":" + p.getArtifactId()))
226            .findFirst()
227            .ifPresent(consumer);
228    }
229}