/*
 * The MIT License
 * 
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jene Jasper, Stephen Connolly, Tom Huybrechts, Yahoo! Inc.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.tasks;

import hudson.Extension;
import jenkins.MasterToSlaveFileCallable;
import hudson.Launcher;
import hudson.Functions;
import hudson.EnvVars;
import hudson.Util;
import hudson.CopyOnWrite;
import hudson.Launcher.LocalLauncher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.EnvironmentSpecific;
import hudson.model.Node;
import jenkins.model.Jenkins;
import jenkins.mvn.GlobalMavenConfig;
import jenkins.mvn.GlobalSettingsProvider;
import jenkins.mvn.SettingsProvider;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.slaves.NodeSpecific;
import hudson.tasks._maven.MavenConsoleAnnotator;
import hudson.tools.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.tools.DownloadFromUrlInstaller;
import hudson.tools.ToolInstaller;
import hudson.tools.ToolProperty;
import hudson.util.ArgumentListBuilder;
import hudson.util.NullStream;
import hudson.util.StreamTaskListener;
import hudson.util.VariableResolver;
import hudson.util.VariableResolver.ByMap;
import hudson.util.VariableResolver.Union;
import hudson.util.FormValidation;
import hudson.util.XStream2;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.List;
import java.util.Collections;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;

/**
 * Build by using Maven.
 *
 * @author Kohsuke Kawaguchi
 */
public class Maven extends Builder {
    /**
     * The targets and other maven options.
     * Can be separated by SP or NL.
     */
    public final String targets;

    /**
     * Identifies {@link MavenInstallation} to be used.
     */
    public final String mavenName;

    /**
     * MAVEN_OPTS if not null.
     */
    public final String jvmOptions;

    /**
     * Optional POM file path relative to the workspace.
     * Used for the Maven '-f' option.
     */
    public final String pom;

    /**
     * Optional properties to be passed to Maven. Follows {@link Properties} syntax.
     */
    public final String properties;

    /**
     * If true, the build will use its own local Maven repository
     * via "-Dmaven.repo.local=...".
     * <p>
     * This would consume additional disk space, but provides isolation with other builds on the same machine,
     * such as mixing SNAPSHOTS. Maven also doesn't try to coordinate the concurrent access to Maven repositories
     * from multiple Maven process, so this helps there too.
     *
     * Identical to logic used in maven-plugin.
     *
     * @since 1.322
     */
    public boolean usePrivateRepository = false;
    
    /**
     * Provides access to the settings.xml to be used for a build.
     * @since 1.491
     */
    private SettingsProvider settings;
    
    /**
     * Provides access to the global settings.xml to be used for a build.
     * @since 1.491
     */
    private GlobalSettingsProvider globalSettings;

    /**
     * Skip injecting build variables as properties into maven process.
     *
     * Defaults to false unless user requests otherwise. Old configurations are set to true to mimic the legacy behaviour.
     *
     * @since 2.12
     */
    private @Nonnull Boolean injectBuildVariables;

    private final static String MAVEN_1_INSTALLATION_COMMON_FILE = "bin/maven";
    private final static String MAVEN_2_INSTALLATION_COMMON_FILE = "bin/mvn";
    
    private static final Pattern S_PATTERN = Pattern.compile("(^| )-s ");
    private static final Pattern GS_PATTERN = Pattern.compile("(^| )-gs ");

    public Maven(String targets,String name) {
        this(targets,name,null,null,null,false, null, null);
    }

    public Maven(String targets, String name, String pom, String properties, String jvmOptions) {
        this(targets, name, pom, properties, jvmOptions, false, null, null);
    }
    
    public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository) {
        this(targets, name, pom, properties, jvmOptions, usePrivateRepository, null, null);
    }
    
    public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository, SettingsProvider settings, GlobalSettingsProvider globalSettings) {
        this(targets, name, pom, properties, jvmOptions, usePrivateRepository, settings, globalSettings, false);
    }

    @DataBoundConstructor
    public Maven(String targets,String name, String pom, String properties, String jvmOptions, boolean usePrivateRepository, SettingsProvider settings, GlobalSettingsProvider globalSettings, boolean injectBuildVariables) {
        this.targets = targets;
        this.mavenName = name;
        this.pom = Util.fixEmptyAndTrim(pom);
        this.properties = Util.fixEmptyAndTrim(properties);
        this.jvmOptions = Util.fixEmptyAndTrim(jvmOptions);
        this.usePrivateRepository = usePrivateRepository;
        this.settings = settings != null ? settings : GlobalMavenConfig.get().getSettingsProvider();
        this.globalSettings = globalSettings != null ? globalSettings : GlobalMavenConfig.get().getGlobalSettingsProvider();
        this.injectBuildVariables = injectBuildVariables;
    }

    public String getTargets() {
        return targets;
    }

    /**
     * @since 1.491
     */
    public SettingsProvider getSettings() {
        return settings != null ? settings : GlobalMavenConfig.get().getSettingsProvider();
    }
    
    protected void setSettings(SettingsProvider settings) {
        this.settings = settings;
    }
    
    /**
     * @since 1.491
     */
    public GlobalSettingsProvider getGlobalSettings() {
        return globalSettings != null ? globalSettings : GlobalMavenConfig.get().getGlobalSettingsProvider();
    }
    
    protected void setGlobalSettings(GlobalSettingsProvider globalSettings) {
        this.globalSettings = globalSettings;
    }

    public void setUsePrivateRepository(boolean usePrivateRepository) {
        this.usePrivateRepository = usePrivateRepository;
    }

    public boolean usesPrivateRepository() {
        return usePrivateRepository;
    }

    @Restricted(NoExternalUse.class) // Exposed for view
    public boolean isInjectBuildVariables() {
        return injectBuildVariables;
    }

    /**
     * Gets the Maven to invoke,
     * or null to invoke the default one.
     */
    public MavenInstallation getMaven() {
        for( MavenInstallation i : getDescriptor().getInstallations() ) {
            if(mavenName !=null && mavenName.equals(i.getName()))
                return i;
        }
        return null;
    }

    private Object readResolve() throws ObjectStreamException {
        if (injectBuildVariables == null) {
            injectBuildVariables = true;
        }
        return this;
    }

    /**
     * Looks for <tt>pom.xlm</tt> or <tt>project.xml</tt> to determine the maven executable
     * name.
     */
    private static final class DecideDefaultMavenCommand extends MasterToSlaveFileCallable<String> {
        private static final long serialVersionUID = -2327576423452215146L;
        // command line arguments.
        private final String arguments;

        public DecideDefaultMavenCommand(String arguments) {
            this.arguments = arguments;
        }

        public String invoke(File ws, VirtualChannel channel) throws IOException {
            String seed=null;

            // check for the -f option
            StringTokenizer tokens = new StringTokenizer(arguments);
            while(tokens.hasMoreTokens()) {
                String t = tokens.nextToken();
                if(t.equals("-f") && tokens.hasMoreTokens()) {
                    File file = new File(ws,tokens.nextToken());
                    if(!file.exists())
                        continue;   // looks like an error, but let the execution fail later
                    seed = file.isDirectory() ?
                        /* in M1, you specify a directory in -f */ "maven"
                        /* in M2, you specify a POM file name.  */ : "mvn";
                    break;
                }
            }

            if(seed==null) {
                // as of 1.212 (2008 April), I think Maven2 mostly replaced Maven1, so
                // switching to err on M2 side.
                seed = new File(ws,"project.xml").exists() ? "maven" : "mvn";
            }

            return seed;
        }
    }

    @Override
    public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
        VariableResolver<String> vr = build.getBuildVariableResolver();

        EnvVars env = build.getEnvironment(listener);

        String targets = Util.replaceMacro(this.targets,vr);
        targets = env.expand(targets);
        String pom = env.expand(this.pom);

        int startIndex = 0;
        int endIndex;
        do {
            // split targets into multiple invocations of maven separated by |
            endIndex = targets.indexOf('|', startIndex);
            if (-1 == endIndex) {
                endIndex = targets.length();
            }

            String normalizedTarget = targets
                    .substring(startIndex, endIndex)
                    .replaceAll("[\t\r\n]+"," ");

            ArgumentListBuilder args = new ArgumentListBuilder();
            MavenInstallation mi = getMaven();
            if(mi==null) {
                String execName = build.getWorkspace().act(new DecideDefaultMavenCommand(normalizedTarget));
                args.add(execName);
            } else {
                mi = mi.forNode(Computer.currentComputer().getNode(), listener);
            	mi = mi.forEnvironment(env);
                String exec = mi.getExecutable(launcher);
                if(exec==null) {
                    listener.fatalError(Messages.Maven_NoExecutable(mi.getHome()));
                    return false;
                }
                args.add(exec);
            }
            if(pom!=null)
                args.add("-f",pom);
            
            
            if(!S_PATTERN.matcher(targets).find()){ // check the given target/goals do not contain settings parameter already
                String settingsPath = SettingsProvider.getSettingsRemotePath(getSettings(), build, listener);
                if(StringUtils.isNotBlank(settingsPath)){
                    args.add("-s", settingsPath);
                }
            }
            if(!GS_PATTERN.matcher(targets).find()){
                String settingsPath = GlobalSettingsProvider.getSettingsRemotePath(getGlobalSettings(), build, listener);
                if(StringUtils.isNotBlank(settingsPath)){
                    args.add("-gs", settingsPath);
                }
            }

            Set<String> sensitiveVars = build.getSensitiveBuildVariables();

            // Inject environment variables only if chosen to do so
            if (isInjectBuildVariables()) {
                args.addKeyValuePairs("-D", build.getBuildVariables(), sensitiveVars);
            }

            // Add properties from builder configuration, AFTER the injected build variables.
            final VariableResolver<String> resolver = new Union<String>(new ByMap<String>(env), vr);
            args.addKeyValuePairsFromPropertyString("-D", this.properties, resolver, sensitiveVars);

            if (usesPrivateRepository())
                args.add("-Dmaven.repo.local=" + build.getWorkspace().child(".repository"));
            args.addTokenized(normalizedTarget);
            wrapUpArguments(args,normalizedTarget,build,launcher,listener);

            buildEnvVars(env, mi);
            
            if (!launcher.isUnix()) {
                args = args.toWindowsCommand();
            }

            try {
                MavenConsoleAnnotator mca = new MavenConsoleAnnotator(listener.getLogger(),build.getCharset());
                int r = launcher.launch().cmds(args).envs(env).stdout(mca).pwd(build.getModuleRoot()).join();
                if (0 != r) {
                    return false;
                }
            } catch (IOException e) {
                Util.displayIOException(e,listener);
                Functions.printStackTrace(e, listener.fatalError(Messages.Maven_ExecFailed()));
                return false;
            }
            startIndex = endIndex + 1;
        } while (startIndex < targets.length());
        return true;
    }

    /**
     * Allows the derived type to make additional modifications to the arguments list.
     *
     * @since 1.344
     */
    protected void wrapUpArguments(ArgumentListBuilder args, String normalizedTarget, AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
    }

    /**
     * Build up the environment variables toward the Maven launch.
     */
    protected void buildEnvVars(EnvVars env, MavenInstallation mi) throws IOException, InterruptedException {
        if(mi!=null) {
            // if somebody has use M2_HOME they will get a classloading error
            // when M2_HOME points to a different version of Maven2 from
            // MAVEN_HOME (as Maven 2 gives M2_HOME priority.)
            //
            // The other solution would be to set M2_HOME if we are calling Maven2
            // and MAVEN_HOME for Maven1 (only of use for strange people that
            // are calling Maven2 from Maven1)
            mi.buildEnvVars(env);
        }
        // just as a precaution
        // see http://maven.apache.org/continuum/faqs.html#how-does-continuum-detect-a-successful-build
        env.put("MAVEN_TERMINATE_CMD","on");

        String jvmOptions = env.expand(this.jvmOptions);
        if(jvmOptions!=null)
            env.put("MAVEN_OPTS",jvmOptions.replaceAll("[\t\r\n]+"," "));
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl)super.getDescriptor();
    }

    /**
     * @deprecated as of 1.286
     *      Use {@link jenkins.model.Jenkins#getDescriptorByType(Class)} to obtain the current instance.
     *      For compatibility, this field retains the last created {@link DescriptorImpl}.
     *      TODO: fix sonar plugin that depends on this. That's the only plugin that depends on this field.
     */
    @Deprecated
    public static DescriptorImpl DESCRIPTOR;

    @Extension @Symbol("maven")
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        @CopyOnWrite
        private volatile MavenInstallation[] installations = new MavenInstallation[0];

        public DescriptorImpl() {
            DESCRIPTOR = this;
            load();
        }

        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }

        @Override
        public String getHelpFile(String fieldName) {
            if (fieldName != null && fieldName.equals("globalSettings")) fieldName = "settings"; // same help file
            return super.getHelpFile(fieldName);
        }

        public String getDisplayName() {
            return Messages.Maven_DisplayName();
        }
        
        public GlobalSettingsProvider getDefaultGlobalSettingsProvider() {
            return GlobalMavenConfig.get().getGlobalSettingsProvider();
        }
        
        public SettingsProvider getDefaultSettingsProvider() {
            return GlobalMavenConfig.get().getSettingsProvider();
        }

        public MavenInstallation[] getInstallations() {
            return installations;
        }

		public void setInstallations(MavenInstallation... installations) {
			List<MavenInstallation> tmpList = new ArrayList<Maven.MavenInstallation>();
			// remote empty Maven installation : 
			if(installations != null) {
				Collections.addAll(tmpList, installations);
				for(MavenInstallation installation : installations) {
					if(Util.fixEmptyAndTrim(installation.getName()) == null) {
						tmpList.remove(installation);
					}
				}
			}
            this.installations = tmpList.toArray(new MavenInstallation[tmpList.size()]);
            save();
        }

        @Override
        public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
            if (req == null) {
                // This state is prohibited according to the Javadoc of the super method.
                throw new FormException("Maven Build Step new instance method is called for null Stapler request. "
                        + "Such call is prohibited.", "req");
            }
            return req.bindJSON(Maven.class,formData);
        }
    }

    /**
     * Represents a Maven installation in a system.
     */
    public static final class MavenInstallation extends ToolInstallation implements EnvironmentSpecific<MavenInstallation>, NodeSpecific<MavenInstallation> {
        /**
         * Constants for describing Maven versions for comparison.
         */
        public static final int MAVEN_20 = 0;
        public static final int MAVEN_21 = 1;
        public static final int MAVEN_30 = 2;
        
    
        /**
         * @deprecated since 2009-02-25.
         */
        @Deprecated // kept for backward compatibility - use getHome()
        private transient String mavenHome;

        /**
         * @deprecated as of 1.308.
         *      Use {@link #MavenInstallation(String, String, List)}
         */
        @Deprecated
        public MavenInstallation(String name, String home) {
            super(name, home);
        }

        @DataBoundConstructor
        public MavenInstallation(String name, String home, List<? extends ToolProperty<?>> properties) {
            super(Util.fixEmptyAndTrim(name), Util.fixEmptyAndTrim(home), properties);
        }

        /**
         * install directory.
         *
         * @deprecated as of 1.308. Use {@link #getHome()}.
         */
        @Deprecated
        public String getMavenHome() {
            return getHome();
        }

        public File getHomeDir() {
            return new File(getHome());
        }

        @Override
        public void buildEnvVars(EnvVars env) {
            String home = getHome();
            if (home == null) {
                return;
            }
            env.put("M2_HOME", home);
            env.put("MAVEN_HOME", home);
            env.put("PATH+MAVEN", home + "/bin");
        }

        /**
         * Compares the version of this Maven installation to the minimum required version specified.
         *
         * @param launcher
         *      Represents the node on which we evaluate the path.
         * @param mavenReqVersion
         *      Represents the minimum required Maven version - constants defined above.
         */
        public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException {
            // FIXME using similar stuff as in the maven plugin could be better 
            // olamy : but will add a dependency on maven in core -> so not so good 
            String mavenVersion = launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() {
                    private static final long serialVersionUID = -4143159957567745621L;

                    public String call() throws IOException {
                        File[] jars = new File(getHomeDir(),"lib").listFiles();
                        if(jars!=null) { // be defensive
                            for (File jar : jars) {
                                if (jar.getName().startsWith("maven-")) {
                                    JarFile jf = null;
                                    try {
                                        jf = new JarFile(jar);
                                        Manifest manifest = jf.getManifest();
                                        String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
                                        if(version != null) return version;
                                    } finally {
                                        if(jf != null) jf.close();
                                    }
                                }
                            }
                        }
                        return "";
                    }
                });

            if (!mavenVersion.equals("")) {
                if (mavenReqVersion == MAVEN_20) {
                    if(mavenVersion.startsWith("2."))
                        return true;
                }
                else if (mavenReqVersion == MAVEN_21) {
                    if(mavenVersion.startsWith("2.") && !mavenVersion.startsWith("2.0"))
                        return true;
                }
                else if (mavenReqVersion == MAVEN_30) {
                    if(mavenVersion.startsWith("3."))
                        return true;
                }                
            }
            return false;
            
        }
        
        /**
         * Is this Maven 2.1.x or 2.2.x - but not Maven 3.x?
         *
         * @param launcher
         *      Represents the node on which we evaluate the path.
         */
        public boolean isMaven2_1(Launcher launcher) throws IOException, InterruptedException {
            return meetsMavenReqVersion(launcher, MAVEN_21);
        }

        /**
         * Gets the executable path of this maven on the given target system.
         */
        public String getExecutable(Launcher launcher) throws IOException, InterruptedException {
            return launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() {
                private static final long serialVersionUID = 2373163112639943768L;

                public String call() throws IOException {
                    File exe = getExeFile("mvn");
                    if(exe.exists())
                        return exe.getPath();
                    exe = getExeFile("maven");
                    if(exe.exists())
                        return exe.getPath();
                    return null;
                }
            });
        }

        private File getExeFile(String execName) {
            String m2Home = Util.replaceMacro(getHome(),EnvVars.masterEnvVars);

            if(Functions.isWindows()) {
                File exeFile = new File(m2Home, "bin/" + execName + ".bat");

                // since Maven 3.3 .bat files are replaced with .cmd
                if (!exeFile.exists()) {
                    return new File(m2Home, "bin/" + execName + ".cmd");
                }

                return exeFile;
            } else {
                return new File(m2Home, "bin/" + execName);
            }
        }

        /**
         * Returns true if the executable exists.
         */
        public boolean getExists() {
            try {
                return getExecutable(new LocalLauncher(new StreamTaskListener(new NullStream())))!=null;
            } catch (IOException | InterruptedException e) {
                return false;
            }
        }

        private static final long serialVersionUID = 1L;

        public MavenInstallation forEnvironment(EnvVars environment) {
            return new MavenInstallation(getName(), environment.expand(getHome()), getProperties().toList());
        }

        public MavenInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException {
            return new MavenInstallation(getName(), translateFor(node, log), getProperties().toList());
        }

        @Extension @Symbol("maven")
        public static class DescriptorImpl extends ToolDescriptor<MavenInstallation> {
            @Override
            public String getDisplayName() {
                return "Maven";
            }

            @Override
            public List<? extends ToolInstaller> getDefaultInstallers() {
                return Collections.singletonList(new MavenInstaller(null));
            }

            // overriding them for backward compatibility.
            // newer code need not do this
            @Override
            public MavenInstallation[] getInstallations() {
                return Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).getInstallations();
            }

            // overriding them for backward compatibility.
            // newer code need not do this
            @Override
            public void setInstallations(MavenInstallation... installations) {
                Jenkins.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(installations);
            }

            /**
             * Checks if the MAVEN_HOME is valid.
             */
            @Override protected FormValidation checkHomeDirectory(File value) {
                File maven1File = new File(value,MAVEN_1_INSTALLATION_COMMON_FILE);
                File maven2File = new File(value,MAVEN_2_INSTALLATION_COMMON_FILE);

                if(!maven1File.exists() && !maven2File.exists())
                    return FormValidation.error(Messages.Maven_NotMavenDirectory(value));

                return FormValidation.ok();
            }

        }

        public static class ConverterImpl extends ToolConverter {
            public ConverterImpl(XStream2 xstream) { super(xstream); }
            @Override protected String oldHomeField(ToolInstallation obj) {
                return ((MavenInstallation)obj).mavenHome;
            }
        }
    }

    /**
     * Automatic Maven installer from apache.org.
     */
    public static class MavenInstaller extends DownloadFromUrlInstaller {
        @DataBoundConstructor
        public MavenInstaller(String id) {
            super(id);
        }

        @Extension @Symbol("maven")
        public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl<MavenInstaller> {
            public String getDisplayName() {
                return Messages.InstallFromApache();
            }

            @Override
            public boolean isApplicable(Class<? extends ToolInstallation> toolType) {
                return toolType==MavenInstallation.class;
            }
        }
    }

    /**
     * Optional interface that can be implemented by {@link AbstractProject}
     * that has "contextual" {@link MavenInstallation} associated with it.
     *
     * <p>
     * Code like RedeployPublisher uses this interface in an attempt
     * to use the consistent Maven installation attached to the project.
     *
     * @since 1.235
     */
    public interface ProjectWithMaven {
        /**
         * Gets the {@link MavenInstallation} associated with the project.
         * Can be null.
         *
         * <p>
         * If the Maven installation can not be uniquely determined,
         * it's often better to return just one of them, rather than returning
         * null, since this method is currently ultimately only used to
         * decide where to parse <tt>conf/settings.xml</tt> from.
         */
        MavenInstallation inferMavenInstallation();
    }

}
