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