package org.codehaus.mojo.flatten;

/*
 * 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.
 */

import java.util.Queue;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Activation;
import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Profile;
import org.apache.maven.model.Repository;
import org.apache.maven.model.RepositoryPolicy;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingResult;
import org.apache.maven.model.building.ModelProblemCollector;
import org.apache.maven.model.interpolation.ModelInterpolator;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.model.profile.ProfileActivationContext;
import org.apache.maven.model.profile.ProfileInjector;
import org.apache.maven.model.profile.ProfileSelector;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.dependencies.resolve.DependencyResolver;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor;
import org.codehaus.mojo.flatten.cifriendly.CiInterpolator;
import org.codehaus.mojo.flatten.model.resolution.FlattenModelResolver;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.impl.ArtifactDescriptorReader;
import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
import org.eclipse.aether.resolution.ArtifactDescriptorResult;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;

import javax.inject.Inject;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * This MOJO realizes the goal <code>flatten</code> that generates the flattened POM and {@link #isUpdatePomFile()
 * potentially updates the POM file} so that the current {@link MavenProject}'s {@link MavenProject#getFile() file}
 * points to the flattened POM instead of the original <code>pom.xml</code> file. The flattened POM is a reduced version
 * of the original POM with the focus to contain only the important information for consuming it. Therefore information
 * that is only required for maintenance by developers and to build the project artifact(s) are stripped. Starting from
 * here we specify how the flattened POM is created from the original POM and its project:<br>
 * <table border="1" summary="">
 * <tr>
 * <th>Element</th>
 * <th>Transformation</th>
 * <th>Note</th>
 * </tr>
 * <tr>
 * <td>{@link Model#getModelVersion() modelVersion}</td>
 * <td>Fixed to "4.0.0"</td>
 * <td>New maven versions will once be able to evolve the model version without incompatibility to older versions if
 * flattened POMs get deployed.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getGroupId() groupId}<br>
 * {@link Model#getArtifactId() artifactId}<br>
 * {@link Model#getVersion() version}<br>
 * {@link Model#getPackaging() packaging}</td>
 * <td>resolved</td>
 * <td>copied to the flattened POM but with inheritance from {@link Model#getParent() parent} as well as with all
 * variables and defaults resolved. These elements are technically required for consumption.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getLicenses() licenses}</td>
 * <td>resolved</td>
 * <td>copied to the flattened POM but with inheritance from {@link Model#getParent() parent} as well as with all
 * variables and defaults resolved. The licenses would not be required in flattened POM. However, they make sense for
 * publication and deployment and are important for consumers of your artifact.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getDependencies() dependencies}</td>
 * <td>resolved specially</td>
 * <td>flattened POM contains the actual dependencies of the project. Test dependencies are removed. Variables and
 * {@link Model#getDependencyManagement() dependencyManagement} is resolved to get fixed dependency attributes
 * (especially {@link Dependency#getVersion() version}). If {@link #isEmbedBuildProfileDependencies()
 * embedBuildProfileDependencies} is set to <code>true</code>, then also build-time driven {@link Profile}s will be
 * evaluated and may add {@link Dependency dependencies}. For further details see {@link Profile}s below.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getProfiles() profiles}</td>
 * <td>resolved specially</td>
 * <td>only the {@link Activation} and the {@link Dependency dependencies} of a {@link Profile} are copied to the
 * flattened POM. If you set the parameter {@link #isEmbedBuildProfileDependencies() embedBuildProfileDependencies} to
 * <code>true</code> then only profiles {@link Activation activated} by {@link Activation#getJdk() JDK} or
 * {@link Activation#getOs() OS} will be added to the flattened POM while the other profiles are triggered by the
 * current build setup and if activated their impact on dependencies is embedded into the resulting flattened POM.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getName() name}<br>
 * {@link Model#getDescription() description}<br>
 * {@link Model#getUrl() url}<br>
 * {@link Model#getInceptionYear() inceptionYear}<br>
 * {@link Model#getOrganization() organization}<br>
 * {@link Model#getScm() scm}<br>
 * {@link Model#getDevelopers() developers}<br>
 * {@link Model#getContributors() contributors}<br>
 * {@link Model#getMailingLists() mailingLists}<br>
 * {@link Model#getPluginRepositories() pluginRepositories}<br>
 * {@link Model#getIssueManagement() issueManagement}<br>
 * {@link Model#getCiManagement() ciManagement}<br>
 * {@link Model#getDistributionManagement() distributionManagement}</td>
 * <td>configurable</td>
 * <td>Will be stripped from the flattened POM by default. You can configure all of the listed elements inside
 * <code>pomElements</code> that should be kept in the flattened POM (e.g. {@literal
 * <pomElements><name/><description/><developers/><contributors/></pomElements>}). For common use-cases there are
 * predefined modes available via the parameter <code>flattenMode</code> that should be used in preference.</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getPrerequisites() prerequisites}</td>
 * <td>configurable</td>
 * <td>Like above but by default NOT removed if packaging is "maven-plugin".</td>
 * </tr>
 * <tr>
 * <td>{@link Model#getRepositories() repositories}</td>
 * <td>configurable</td>
 * <td>Like two above but by default NOT removed. If you want have it removed, you need to use the parameter
 * <code>pomElements</code> and configure the child element <code>repositories</code> with value <code>flatten</code>.
 * </td>
 * </tr>
 * <tr>
 * <td>{@link Model#getParent() parent}<br>
 * {@link Model#getBuild() build}<br>
 * {@link Model#getDependencyManagement() dependencyManagement}<br>
 * {@link Model#getProperties() properties}<br>
 * {@link Model#getModules() modules}<br>
 * {@link Model#getReporting() reporting}</td>
 * <td>configurable</td>
 * <td>These elements should typically be completely stripped from the flattened POM. However for ultimate flexibility
 * (e.g. if you only want to resolve variables in a POM with packaging pom) you can also configure to keep these
 * elements. We strictly recommend to use this feature with extreme care and only if packaging is pom (for "Bill of
 * Materials"). In the latter case you configure the parameter <code>flattenMode</code> to the value
 * <code>bom</code>.<br>
 * If the <code>build</code> element contains plugins in the <code>build/plugins</code> section which are configured to
 * load <a href="http://maven.apache.org/pom.html#Extensions">extensions</a>, a reduced <code>build</code> element
 * containing these plugins will be kept in the flattened pom.</td>
 * </tr>
 * </table>
 *
 * @author Joerg Hohwiller (hohwille at users.sourceforge.net)
 */
@SuppressWarnings( "deprecation" )
// CHECKSTYLE_OFF: LineLength
@Mojo( name = "flatten", requiresProject = true, requiresDirectInvocation = false, executionStrategy = "once-per-session", requiresDependencyCollection = ResolutionScope.RUNTIME, threadSafe = true )
// CHECKSTYLE_ON: LineLength
public class FlattenMojo
    extends AbstractFlattenMojo
{

    private static final int INITIAL_POM_WRITER_SIZE = 4096;

    /**
     * The Maven Project.
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    private MavenProject project;

    /**
     * The flag to indicate if the generated flattened POM shall be set as POM file to the current project. By default
     * this is only done for projects with packaging other than <code>pom</code>. You may want to also do this for
     * <code>pom</code> packages projects by setting this parameter to <code>true</code> or you can use
     * <code>false</code> in order to only generate the flattened POM but never set it as POM file. If
     * <code>flattenMode</code> is set to bom the default value will be <code>true</code>.
     */
    @Parameter( property = "updatePomFile" )
    private Boolean updatePomFile;

    /** The {@link ArtifactRepository} required to resolve POM using {@link #modelBuilder}. */
    @Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
    private ArtifactRepository localRepository;

    /**
     * Profiles activated by OS or JDK are valid ways to have different dependencies per environment. However, profiles
     * activated by property of file are less clear. When setting this parameter to <code>true</code>, the latter
     * dependencies will be written as direct dependencies of the project. <strong>This is not how Maven2 and Maven3
     * handles dependencies</strong>. When keeping this property <code>false</code>, all profiles will stay in the
     * flattened-pom.
     */
    @Parameter( defaultValue = "false" )
    private Boolean embedBuildProfileDependencies;

    /**
     * The {@link MojoExecution} used to get access to the raw configuration of {@link #pomElements} as empty tags are
     * mapped to null.
     */
    @Parameter( defaultValue = "${mojo}", readonly = true, required = true )
    private MojoExecution mojoExecution;

    /**
     * The {@link Model} that defines how to handle additional POM elements. Please use <code>flattenMode</code> in
     * preference if possible. This parameter is only for ultimate flexibility.
     */
    @Parameter( required = false )
    private FlattenDescriptor pomElements;

    /**
     * The different possible values for flattenMode:
     * <table border="1" summary="">
     * <thead>
     * <tr>
     * <td>Mode</td>
     * <td>Description</td>
     * </tr>
     * </thead> <tbody>
     * <tr>
     * <td>oss</td>
     * <td>For Open-Source-Software projects that want to keep all {@link FlattenDescriptor optional POM elements}
     * except for {@link Model#getRepositories() repositories} and {@link Model#getPluginRepositories()
     * pluginRepositories}.</td>
     * <tr>
     * <td>ossrh</td>
     * <td>Keeps all {@link FlattenDescriptor optional POM elements} that are required for
     * <a href="https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide">OSS
     * Repository-Hosting</a>.</td>
     * </tr>
     * <tr>
     * <td>bom</td>
     * <td>Like {@link #ossrh} but additionally keeps {@link Model#getDependencyManagement() dependencyManagement} and
     * {@link Model#getProperties() properties}. Especially it will keep the {@link Model#getDependencyManagement()
     * dependencyManagement} <em>as-is</em> without resolving parent influences and import-scoped dependencies. This is
     * useful if your POM represents a <a href=
     * "http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Importing_Dependencies"
     * >BOM (Bill Of Material)</a> and you do not want to deploy it as is (to remove parent and resolve version
     * variables, etc.).</td>
     * </tr>
     * <tr>
     * <td>defaults</td>
     * <td>The default mode that removes all {@link FlattenDescriptor optional POM elements} except
     * {@link Model#getRepositories() repositories}.</td>
     * </tr>
     * <tr>
     * <td>clean</td>
     * <td>Removes all {@link FlattenDescriptor optional POM elements}.</td>
     * </tr>
     * <tr>
     * <td>fatjar</td>
     * <td>Removes all {@link FlattenDescriptor optional POM elements} and all {@link Model#getDependencies()
     * dependencies}.</td>
     * </tr>
     * <tr>
     * <td>resolveCiFriendliesOnly</td>
     * <td>Only resolves variables revision, sha1 and changelist. Keeps everything else. 
     * See <a href="https://maven.apache.org/maven-ci-friendly.html">Maven CI Friendly</a> for further details.</td>
     * </tr>
     * </tbody>
     * </table>
     */
    @Parameter( property = "flatten.mode", required = false )
    private FlattenMode flattenMode;

    /**
     * The different possible values for flattenDependencyMode:
     * <table border="1" summary="">
     * <thead>
     * <tr>
     * <td>Mode</td>
     * <td>Description</td>
     * </tr>
     * </thead><tbody>
     * <tr>
     * <td>direct</td>
     * <td>Flatten only the direct dependency versions. This is the default mode and compatible with Flatten Plugin prior to 1.2.0.</td>
     * <tr>
     * <td>all</td>
     * <td><p>Flatten both direct and transitive dependencies. This will examine the full dependency tree, and pull up
     * all transitive dependencies as a direct dependency, and setting their versions appropriately.</p>
     * <p>This is recommended if you are releasing a library that uses dependency management to manage dependency
     * versions.</p>/td>
     * </tr>
     * </tbody>
     * </table>
     */
    @Parameter( property = "flatten.dependency.mode", required = false )
    private FlattenDependencyMode flattenDependencyMode;

    /**
     * The ArtifactFactory required to resolve POM using {@link #modelBuilder}.
     */
    // Neither ArtifactFactory nor DefaultArtifactFactory tells what to use instead
    @Component
    private ArtifactFactory artifactFactory;

    /**
     * The {@link ModelInterpolator} used to resolve variables.
     */
    @Component( role = ModelInterpolator.class )
    private ModelInterpolator modelInterpolator;
    
    /**
     * The {@link ModelInterpolator} used to resolve variables.
     */
    @Component( role = CiInterpolator.class)
    private CiInterpolator modelCiFriendlyInterpolator;

    /**
     * The {@link MavenSession} used to get user properties.
     */
    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    private MavenSession session;

    @Component
    private DependencyResolver dependencyResolver;

    @Component( hint = "default" )
    private DependencyTreeBuilder dependencyTreeBuilder;

    @Component(role = ArtifactDescriptorReader.class)
    private ArtifactDescriptorReader artifactDescriptorReader;

    @Inject
    private ModelBuilderThreadSafetyWorkaround modelBuilderThreadSafetyWorkaround;

    /**
     * The constructor.
     */
    public FlattenMojo()
    {
        super();
    }

    /**
     * {@inheritDoc}
     */
    public void execute()
        throws MojoExecutionException, MojoFailureException
    {

        getLog().info( "Generating flattened POM of project " + this.project.getId() + "..." );

        File originalPomFile = this.project.getFile();
        Model flattenedPom = createFlattenedPom( originalPomFile );
        String headerComment = extractHeaderComment( originalPomFile );

        File flattenedPomFile = getFlattenedPomFile();
        writePom( flattenedPom, flattenedPomFile, headerComment );

        if ( isUpdatePomFile() )
        {
            this.project.setPomFile( flattenedPomFile );
        }
    }

    /**
     * This method extracts the XML header comment if available.
     *
     * @param xmlFile is the XML {@link File} to parse.
     * @return the XML comment between the XML header declaration and the root tag or <code>null</code> if NOT
     * available.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected String extractHeaderComment( File xmlFile )
        throws MojoExecutionException
    {

        try
        {
            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
            SaxHeaderCommentHandler handler = new SaxHeaderCommentHandler();
            parser.setProperty( "http://xml.org/sax/properties/lexical-handler", handler );
            parser.parse( xmlFile, handler );
            return handler.getHeaderComment();
        }
        catch ( Exception e )
        {
            throw new MojoExecutionException( "Failed to parse XML from " + xmlFile, e );
        }
    }

    /**
     * Writes the given POM {@link Model} to the given {@link File}.
     *
     * @param pom           the {@link Model} of the POM to write.
     * @param pomFile       the {@link File} where to write the given POM will be written to. {@link File#getParentFile()
     *                      Parent directories} are {@link File#mkdirs() created} automatically.
     * @param headerComment is the content of a potential XML comment at the top of the XML (after XML declaration and
     *                      before root tag). May be <code>null</code> if not present and to be omitted in target POM.
     * @throws MojoExecutionException if the operation failed (e.g. due to an {@link IOException}).
     */
    protected void writePom( Model pom, File pomFile, String headerComment )
        throws MojoExecutionException
    {

        File parentFile = pomFile.getParentFile();
        if ( !parentFile.exists() )
        {
            boolean success = parentFile.mkdirs();
            if ( !success )
            {
                throw new MojoExecutionException( "Failed to create directory " + pomFile.getParent() );
            }
        }
        // MavenXpp3Writer could internally add the comment but does not expose such feature to API!
        // Instead we have to write POM XML to String and do post processing on that :(
        MavenXpp3Writer pomWriter = new MavenXpp3Writer();
        StringWriter stringWriter = new StringWriter( INITIAL_POM_WRITER_SIZE );
        try
        {
            pomWriter.write( stringWriter, pom );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Internal I/O error!", e );
        }
        StringBuffer buffer = stringWriter.getBuffer();
        if ( !StringUtils.isEmpty( headerComment ) )
        {
            int projectStartIndex = buffer.indexOf( "<project" );
            if ( projectStartIndex >= 0 )
            {
                buffer.insert( projectStartIndex, "<!--" + headerComment + "-->\n" );
            }
            else
            {
                getLog().warn( "POM XML post-processing failed: no project tag found!" );
            }
        }
        writeStringToFile( buffer.toString(), pomFile, pom.getModelEncoding() );
    }

    /**
     * Writes the given <code>data</code> to the given <code>file</code> using the specified <code>encoding</code>.
     *
     * @param data     is the {@link String} to write.
     * @param file     is the {@link File} to write to.
     * @param encoding is the encoding to use for writing the file.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected void writeStringToFile( String data, File file, String encoding )
        throws MojoExecutionException
    {
        if (System.getProperty("os.name").contains("Windows"))
        {
            data = data.replaceAll("\n","\r\n");
        }
        byte[] binaryData;

        try
        {
            binaryData = data.getBytes( encoding );
            if ( file.isFile() && file.canRead() && file.length() == binaryData.length )
            {
                try (InputStream inputStream = new FileInputStream( file ))
                {
                    byte[] buffer = new byte[binaryData.length];
                    inputStream.read( buffer );
                    if ( Arrays.equals( buffer, binaryData ) )
                    {
                        getLog().debug( "Arrays.equals( buffer, binaryData ) " );
                        return;
                    }
                    getLog().debug( "Not Arrays.equals( buffer, binaryData ) " );
                }
                catch ( IOException e )
                {
                    // ignore those exceptions, we will overwrite the file
                    getLog().debug( "Issue reading file: " + file.getPath(), e );
                }
            }
            else
            {
                getLog().debug( "file: " + file + ",file.length(): " + file.length() + ", binaryData.length: " + binaryData.length );
            }
        } catch ( IOException e )
        {
            throw new MojoExecutionException( "cannot read String as bytes" , e );
        }
        try (OutputStream outStream = new FileOutputStream( file ))
        {
            outStream.write( binaryData );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Failed to write to " + file, e );
        }
    }

    /**
     * This method creates the flattened POM what is the main task of this plugin.
     *
     * @param pomFile is the name of the original POM file to read and transform.
     * @return the {@link Model} of the flattened POM.
     * @throws MojoExecutionException if anything goes wrong (e.g. POM can not be processed).
     * @throws MojoFailureException   if anything goes wrong (logical error).
     */
    protected Model createFlattenedPom( File pomFile ) throws MojoExecutionException, MojoFailureException
    {

        ModelBuildingRequest buildingRequest = createModelBuildingRequest( pomFile );
        Model effectivePom = createEffectivePom( buildingRequest, isEmbedBuildProfileDependencies(), this.flattenMode );

        Model flattenedPom = new Model();

        // keep original encoding (we could also normalize to UTF-8 here)
        String modelEncoding = effectivePom.getModelEncoding();
        if ( StringUtils.isEmpty( modelEncoding ) )
        {
            modelEncoding = "UTF-8";
        }
        flattenedPom.setModelEncoding( modelEncoding );

        Model cleanPom = null;
        try {
            cleanPom = createCleanPom( effectivePom );
        }
        catch (Exception e) {
            throw new MojoExecutionException("failed to create a clean pom", e);
        }

        FlattenDescriptor descriptor = getFlattenDescriptor();
        Model originalPom = this.project.getOriginalModel();
        Model resolvedPom = this.project.getModel();
        Model interpolatedPom = createResolvedPom( buildingRequest );

        // copy the configured additional POM elements...

        for ( PomProperty<?> property : PomProperty.getPomProperties() )
        {
            if ( property.isElement() )
            {
                Model sourceModel = getSourceModel( descriptor, property, effectivePom, originalPom, resolvedPom,
                                                    interpolatedPom, cleanPom );
                if ( sourceModel == null )
                {
                    if ( property.isRequired() )
                    {
                        throw new MojoFailureException( "Property " + property.getName()
                            + " is required and can not be removed!" );
                    }
                }
                else
                {
                    property.copy( sourceModel, flattenedPom );
                }
            }
        }

        return flattenedPom;
    }

    private Model createResolvedPom( ModelBuildingRequest buildingRequest )
    {
        LoggingModelProblemCollector problems = new LoggingModelProblemCollector( getLog() );
        Model originalModel = this.project.getOriginalModel().clone();
        if (this.flattenMode == FlattenMode.resolveCiFriendliesOnly) {
            return this.modelCiFriendlyInterpolator.interpolateModel( originalModel, this.project.getModel().getProjectDirectory(),
                                                            buildingRequest, problems );
        }
        return this.modelInterpolator.interpolateModel( originalModel, this.project.getModel().getProjectDirectory(),
                                                        buildingRequest, problems );
    }

    /**
     * This method creates the clean POM as a {@link Model} where to copy elements from that shall be
     * {@link ElementHandling#flatten flattened}. Will be mainly empty but contains some the minimum elements that have
     * to be kept in flattened POM.
     *
     * @param effectivePom is the effective POM.
     * @return the clean POM.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected Model createCleanPom( Model effectivePom ) throws MojoExecutionException
    {
        Model cleanPom = new Model();

        cleanPom.setGroupId( effectivePom.getGroupId() );
        cleanPom.setArtifactId( effectivePom.getArtifactId() );
        cleanPom.setVersion( effectivePom.getVersion() );
        cleanPom.setPackaging( effectivePom.getPackaging() );
        cleanPom.setLicenses( effectivePom.getLicenses() );
        // fixed to 4.0.0 forever :)
        cleanPom.setModelVersion( "4.0.0" );

        // plugins with extensions must stay
        Build build = effectivePom.getBuild();
        if ( build != null )
        {
            for ( Plugin plugin : build.getPlugins() )
            {
                if ( plugin.isExtensions() )
                {
                    Build cleanBuild = cleanPom.getBuild();
                    if ( cleanBuild == null )
                    {
                        cleanBuild = new Build();
                        cleanPom.setBuild( cleanBuild );
                    }
                    Plugin cleanPlugin = new Plugin();
                    cleanPlugin.setGroupId( plugin.getGroupId() );
                    cleanPlugin.setArtifactId( plugin.getArtifactId() );
                    cleanPlugin.setVersion( plugin.getVersion() );
                    cleanPlugin.setExtensions( true );
                    cleanBuild.addPlugin( cleanPlugin );
                }
            }
        }

        // transform profiles...
        Dependencies managedDependencies = new Dependencies();
        if ( effectivePom.getDependencyManagement() != null && effectivePom.getDependencyManagement().getDependencies() != null )
        {
            managedDependencies.addAll( effectivePom.getDependencyManagement().getDependencies() );
        }

        for ( Profile profile : effectivePom.getProfiles() )
        {
            if ( !isEmbedBuildProfileDependencies() || !isBuildTimeDriven( profile.getActivation() ) )
            {
                if ( !isEmpty( profile.getDependencies() ) || !isEmpty( profile.getRepositories() ) )
                {
                    List<Dependency> strippedDependencies = new ArrayList<>();
                    for ( Dependency dep : profile.getDependencies() )
                    {
                        Dependency parsedDep = dep.clone();
                        if ( managedDependencies.contains( parsedDep ) )
                        {
                            parsedDep.setVersion( managedDependencies.resolve( parsedDep ).getVersion() );
                            parsedDep.setScope( managedDependencies.resolve( parsedDep ).getScope() );
                            if ( parsedDep.getScope() == null )
                            {
                                parsedDep.setScope( "compile" );
                            }
                            parsedDep.setOptional( managedDependencies.resolve( parsedDep ).getOptional() );
                            if ( parsedDep.getOptional() == null )
                            {
                                parsedDep.setOptional( "false" );
                            }
                        }
                        Dependency flattenedDep = createFlattenedDependency( parsedDep );
                        if ( flattenedDep != null )
                        {
                            strippedDependencies.add( flattenedDep );
                        }
                    }
                    if ( !strippedDependencies.isEmpty() || !isEmpty( profile.getRepositories() ) )
                    {
                        Profile strippedProfile = new Profile();
                        strippedProfile.setId( profile.getId() );
                        strippedProfile.setActivation( profile.getActivation() );
                        strippedProfile.setDependencies( strippedDependencies.isEmpty() ? null : strippedDependencies );
                        strippedProfile.setRepositories( profile.getRepositories() );
                        cleanPom.addProfile( strippedProfile );
                    }
                }
            }
        }

        // transform dependencies...
        List<Dependency> dependencies = createFlattenedDependencies( effectivePom );
        cleanPom.setDependencies( dependencies );
        return cleanPom;
    }

    private Model getSourceModel( FlattenDescriptor descriptor, PomProperty<?> property, Model effectivePom,
                                  Model originalPom, Model resolvedPom, Model interpolatedPom, Model cleanPom )
    {

        ElementHandling handling = descriptor.getHandling( property );
        getLog().debug( "Property " + property.getName() + " will be handled using " + handling
            + " in flattened POM." );
        switch ( handling )
        {
            case expand:
                return effectivePom;
            case keep:
                return originalPom;
            case resolve:
                return resolvedPom;
            case interpolate:
                return interpolatedPom;
            case flatten:
                return cleanPom;
            case remove:
                return null;
            default:
                throw new IllegalStateException( handling.toString() );
        }
    }

    /**
     * Creates a flattened {@link List} of {@link Repository} elements where those from super-POM are omitted.
     *
     * @param repositories is the {@link List} of {@link Repository} elements. May be <code>null</code>.
     * @return the flattened {@link List} of {@link Repository} elements or <code>null</code> if <code>null</code> was
     *         given.
     */
    protected static List<Repository> createFlattenedRepositories( List<Repository> repositories )
    {
        if ( repositories != null )
        {
            List<Repository> flattenedRepositories = new ArrayList<Repository>( repositories.size() );
            for ( Repository repo : repositories )
            {
                // filter inherited repository section from super POM (see MOJO-2042)...
                if ( !isCentralRepositoryFromSuperPom( repo ) )
                {
                    flattenedRepositories.add( repo );
                }
            }
            return flattenedRepositories;
        }
        return repositories;
    }

    private FlattenDescriptor getFlattenDescriptor()
        throws MojoFailureException
    {
        FlattenDescriptor descriptor = this.pomElements;
        if ( descriptor == null )
        {
            FlattenMode mode = this.flattenMode;
            if ( mode == null )
            {
                mode = FlattenMode.defaults;
            }
            else if ( this.flattenMode == FlattenMode.minimum )
            {
                getLog().warn( "FlattenMode " + FlattenMode.minimum + " is deprecated!" );
            }
            descriptor = mode.getDescriptor();
            if ( "maven-plugin".equals( this.project.getPackaging() ) )
            {
                descriptor.setPrerequisites( ElementHandling.expand );
            }
        }
        else
        {
            if ( descriptor.isEmpty() )
            {
                // legacy approach...
                // Can't use Model itself as empty elements are never null, so you can't recognize if it was set or not
                Xpp3Dom rawDescriptor = this.mojoExecution.getConfiguration().getChild( "pomElements" );
                descriptor = new FlattenDescriptor( rawDescriptor );
            }
            if ( this.flattenMode != null )
            {
                descriptor = descriptor.merge( this.flattenMode.getDescriptor() );
            }
        }
        return descriptor;
    }

    /**
     * This method determines if the given {@link Repository} section is identical to what is defined from the super
     * POM.
     *
     * @param repo is the {@link Repository} section to check.
     * @return <code>true</code> if maven central default configuration, <code>false</code> otherwise.
     */
    private static boolean isCentralRepositoryFromSuperPom( Repository repo )
    {
        if ( repo != null )
        {
            if ( "central".equals( repo.getId() ) )
            {
                RepositoryPolicy snapshots = repo.getSnapshots();
                if ( snapshots != null && !snapshots.isEnabled() )
                {
                    return true;
                }
            }
        }
        return false;
    }

    private ModelBuildingRequest createModelBuildingRequest( File pomFile )
    {

        FlattenModelResolver resolver = new FlattenModelResolver( this.localRepository, this.artifactFactory,
            this.dependencyResolver, this.session.getProjectBuildingRequest(), getReactorModelsFromSession() );
        Properties userProperties = this.session.getUserProperties();
        List<String> activeProfiles = this.session.getRequest().getActiveProfiles();

        // @formatter:off
        ModelBuildingRequest buildingRequest =
            new DefaultModelBuildingRequest().setUserProperties( userProperties ).setSystemProperties( System.getProperties() ).setPomFile( pomFile ).setModelResolver( resolver ).setActiveProfileIds( activeProfiles );
        // @formatter:on
        return buildingRequest;
    }

    private List<MavenProject> getReactorModelsFromSession()
    {
        // robust approach for 'special' environments like m2e (Eclipse plugin) which don't provide allProjects
        List<MavenProject> models = this.session.getAllProjects();
        if ( models == null )
        {
            models = this.session.getProjects();
        }
        if ( models == null )
        {
            models = Collections.emptyList();
        }
        return models;
    }

    /**
     * Creates the effective POM for the given <code>pomFile</code> trying its best to match the core maven behaviour.
     *
     * @param buildingRequest {@link ModelBuildingRequest}
     * @param embedBuildProfileDependencies embed build profiles yes/no.
     * @param flattenMode the flattening mode
     * @return the parsed and calculated effective POM.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected Model createEffectivePom( ModelBuildingRequest buildingRequest,
                                               final boolean embedBuildProfileDependencies, final FlattenMode flattenMode )
        throws MojoExecutionException
    {
        ModelBuildingResult buildingResult;
        try
        {
            ProfileInjector customInjector = new ProfileInjector()
            {
                public void injectProfile( Model model, Profile profile, ModelBuildingRequest request,
                                           ModelProblemCollector problems )
                {
                    List<String> activeProfileIds = request.getActiveProfileIds();
                    if ( activeProfileIds.contains( profile.getId() ) )
                    {
                        Properties merged = new Properties();
                        merged.putAll( model.getProperties() );
                        merged.putAll( profile.getProperties() );
                        model.setProperties( merged );
                    }
                }
            };
            ProfileSelector customSelector = new ProfileSelector()
            {
                public List<Profile> getActiveProfiles( Collection<Profile> profiles, ProfileActivationContext context,
                                                        ModelProblemCollector problems )
                {
                    List<Profile> activeProfiles = new ArrayList<Profile>( profiles.size() );

                    for ( Profile profile : profiles )
                    {
                        Activation activation = profile.getActivation();
                        if ( !embedBuildProfileDependencies || isBuildTimeDriven( activation ) )
                        {
                            activeProfiles.add( profile );
                        }
                    }

                    return activeProfiles;
                }
            };

            buildingResult =  modelBuilderThreadSafetyWorkaround.build( buildingRequest, customInjector, customSelector );
        }
        catch ( ModelBuildingException e )
        {
            throw new MojoExecutionException( e.getMessage(), e );
        }

        Model effectivePom = buildingResult.getEffectiveModel();

        // LoggingModelProblemCollector problems = new LoggingModelProblemCollector( getLog() );
        // Model interpolatedModel =
        // this.modelInterpolator.interpolateModel( this.project.getOriginalModel(),
        // effectivePom.getProjectDirectory(), buildingRequest, problems );

        // remove Repositories from super POM (central)
        effectivePom.setRepositories( createFlattenedRepositories( effectivePom.getRepositories() ) );
        return effectivePom;
    }

    /**
     * Null-safe check for {@link Collection#isEmpty()}.
     *
     * @param collection is the {@link Collection} to test. May be <code>null</code>.
     * @return <code>true</code> if <code>null</code> or {@link Collection#isEmpty() empty}, <code>false</code>
     *         otherwise.
     */
    private boolean isEmpty( Collection<?> collection )
    {
        if ( collection == null )
        {
            return true;
        }
        return collection.isEmpty();
    }

    /**
     * @return <code>true</code> if build-dependent profiles (triggered by OS or JDK) should be evaluated and their
     *         effect (variables and dependencies) are resolved and embedded into the flattened POM while the profile
     *         itself is stripped. Otherwise if <code>false</code> the profiles will remain untouched.
     */
    public boolean isEmbedBuildProfileDependencies()
    {

        return this.embedBuildProfileDependencies.booleanValue();
    }

    /**
     * @param activation is the {@link Activation} of a {@link Profile}.
     * @return <code>true</code> if the given {@link Activation} is build-time driven, <code>false</code> otherwise (if
     *         it is triggered by OS or JDK).
     */
    protected static boolean isBuildTimeDriven( Activation activation )
    {

        if ( activation == null )
        {
            return true;
        }
        if ( StringUtils.isEmpty( activation.getJdk() ) && activation.getOs() == null )
        {
            return true;
        }
        return false;
    }

    /**
     * Creates the {@link List} of {@link Dependency dependencies} for the flattened POM. These are all resolved
     * {@link Dependency dependencies} except for those added from {@link Profile profiles}.
     *
     * @param effectiveModel is the effective POM {@link Model} to process.
     * @return the {@link List} of {@link Dependency dependencies}.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected List<Dependency> createFlattenedDependencies( Model effectiveModel )
            throws MojoExecutionException {
        List<Dependency> flattenedDependencies = new ArrayList<Dependency>();
        // resolve all direct and inherited dependencies...
        try {
            createFlattenedDependencies( effectiveModel, flattenedDependencies );
        }
        catch (Exception e) {
            throw new MojoExecutionException("unable to create flattened dependencies", e);
        }
        if ( isEmbedBuildProfileDependencies() )
        {
            Model projectModel = this.project.getModel();
            Dependencies modelDependencies = new Dependencies();
            modelDependencies.addAll( projectModel.getDependencies() );
            for ( Profile profile : projectModel.getProfiles() )
            {
                // build-time driven activation (by property or file)?
                if ( isBuildTimeDriven( profile.getActivation() ) )
                {
                    List<Dependency> profileDependencies = profile.getDependencies();
                    for ( Dependency profileDependency : profileDependencies )
                    {
                        if ( modelDependencies.contains( profileDependency ) )
                        {
                            // our assumption here is that the profileDependency has been added to model because of
                            // this build-time driven profile. Therefore we need to add it to the flattened POM.
                            // Non build-time driven profiles will remain in the flattened POM with their dependencies
                            // and
                            // allow dynamic dependencies due to OS or JDK.
                            flattenedDependencies.add( modelDependencies.resolve(profileDependency) );
                        }
                    }
                }
            }
            getLog().debug( "Resolved " + flattenedDependencies.size() + " dependency/-ies for flattened POM." );
        }
        return flattenedDependencies;
    }

    /**
     * Collects the resolved {@link Dependency dependencies} from the given <code>effectiveModel</code>.
     *
     * @param projectDependencies is the effective POM {@link Model}'s current dependencies
     * @param flattenedDependencies is the {@link List} where to add the collected {@link Dependency dependencies}.
     */
    private void createFlattenedDependenciesDirect( List<Dependency> projectDependencies, List<Dependency> flattenedDependencies )
    {
        for ( Dependency projectDependency : projectDependencies )
        {
            Dependency flattenedDependency = createFlattenedDependency( projectDependency );
            if ( flattenedDependency != null )
            {
                flattenedDependencies.add( flattenedDependency );
            }
        }
    }

    /**
     * Collects the resolved direct and transitive {@link Dependency dependencies} from the given <code>effectiveModel</code>.
     * The collected dependencies are stored in order, so that the leaf dependencies are prioritized in front of direct dependencies.
     * In addition, every non-leaf dependencies will exclude its own direct dependency, since all transitive dependencies
     * will be collected.
	 *
     * Transitive dependencies are all going to be collected and become a direct dependency. Maven should already resolve
     * versions properly because now the transitive dependencies are closer to the artifact. However, when this artifact is
     * being consumed, Maven Enforcer Convergence rule will fail because there may be multiple versions for the same transitive dependency.
	 *
     * Typically, exclusion can be done by using the wildcard. However, a known Maven issue prevents convergence enforcer from
     * working properly w/ wildcard exclusions. Thus, this will exclude each dependencies explicitly rather than using the wildcard.
     *
     * @param projectDependencies is the effective POM {@link Model}'s current dependencies
     * @param flattenedDependencies is the {@link List} where to add the collected {@link Dependency dependencies}.
     * @throws DependencyTreeBuilderException
     * @throws ArtifactDescriptorException
     */
    private void createFlattenedDependenciesAll( List<Dependency> projectDependencies, List<Dependency> flattenedDependencies )
            throws DependencyTreeBuilderException, ArtifactDescriptorException {
        final Queue<DependencyNode> dependencyNodeLinkedList = new LinkedList<DependencyNode>() {};
        final Set<String> processedDependencies = new HashSet<>();

        final Artifact projectArtifact = this.project.getArtifact();

        final DependencyNode dependencyNode = this.dependencyTreeBuilder.buildDependencyTree(this.project,
                this.localRepository, null);

        dependencyNode.accept(new DependencyNodeVisitor() {
            @Override public boolean visit(DependencyNode node) {
                if (node.getArtifact().getGroupId().equals(projectArtifact.getGroupId()) && node.getArtifact().getArtifactId().equals(projectArtifact.getArtifactId())) {
                    return true;
                }
                if (node.getState() != DependencyNode.INCLUDED) return true;
                dependencyNodeLinkedList.add(node);
                return true;
            }

            @Override public boolean endVisit(DependencyNode node) {
                return true;
            }
        });

        while (!dependencyNodeLinkedList.isEmpty()) {
            DependencyNode node = dependencyNodeLinkedList.poll();

            Artifact artifact = node.getArtifact();

            Dependency dependency = new Dependency();
            dependency.setGroupId(artifact.getGroupId());
            dependency.setArtifactId(artifact.getArtifactId());
            dependency.setVersion(artifact.getVersion());
            dependency.setClassifier(artifact.getClassifier());
            dependency.setOptional(artifact.isOptional());
            dependency.setScope(artifact.getScope());
            dependency.setType(artifact.getType());

            List<Exclusion> exclusions = new LinkedList<>();

            org.eclipse.aether.artifact.Artifact aetherArtifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), null, artifact.getVersion());
            ArtifactDescriptorRequest request = new ArtifactDescriptorRequest(aetherArtifact, null, null);
            ArtifactDescriptorResult artifactDescriptorResult = this.artifactDescriptorReader
                    .readArtifactDescriptor(this.session.getRepositorySession(), request);

            for (org.eclipse.aether.graph.Dependency artifactDependency: artifactDescriptorResult.getDependencies()) {
                if ("test".equals(artifactDependency.getScope())) {
                    continue;
                }
                Exclusion exclusion = new Exclusion();
                exclusion.setGroupId(artifactDependency.getArtifact().getGroupId());
                exclusion.setArtifactId(artifactDependency.getArtifact().getArtifactId());
                exclusions.add(exclusion);
            }

            dependency.setExclusions(exclusions);

            // convert dependency to string for the set, since Dependency doesn't implement equals, etc.
            String dependencyString = dependency.getManagementKey();

            if (!processedDependencies.add(dependencyString)) {
                continue;
            }

            Dependency flattenedDependency = createFlattenedDependency( dependency );
            if (flattenedDependency != null) {
                flattenedDependencies.add(flattenedDependency);
            }
        }
    }

    /**
     * Collects the resolved {@link Dependency dependencies} from the given <code>effectiveModel</code>.
     *
     * @param effectiveModel is the effective POM {@link Model} to process.
     * @param flattenedDependencies is the {@link List} where to add the collected {@link Dependency dependencies}.
     * @throws MojoExecutionException if anything goes wrong.
     */
    protected void createFlattenedDependencies( Model effectiveModel, List<Dependency> flattenedDependencies )
            throws MojoExecutionException
    {
        getLog().debug( "Resolving dependencies of " + effectiveModel.getId() );
        // this.project.getDependencies() already contains the inherited dependencies but also those from profiles
        // List<Dependency> projectDependencies = currentProject.getOriginalModel().getDependencies();
        List<Dependency> projectDependencies = effectiveModel.getDependencies();

        if (flattenDependencyMode == null | flattenDependencyMode == FlattenDependencyMode.direct) {
            createFlattenedDependenciesDirect(projectDependencies, flattenedDependencies);
        } else if (flattenDependencyMode == FlattenDependencyMode.all) {
            try {
                createFlattenedDependenciesAll(projectDependencies, flattenedDependencies);
            } catch (Exception e) {
                throw new MojoExecutionException("caught exception when flattening dependencies", e);
            }
        }
    }

    /**
     * @param projectDependency is the project {@link Dependency}.
     * @return the flattened {@link Dependency} or <code>null</code> if the given {@link Dependency} is NOT relevant for
     *         flattened POM.
     */
    protected Dependency createFlattenedDependency( Dependency projectDependency )
    {
        return "test".equals( projectDependency.getScope() ) ? null : projectDependency;
    }

    /**
     * @return <code>true</code> if the generated flattened POM shall be {@link MavenProject#setFile(java.io.File) set}
     *         as POM artifact of the {@link MavenProject}, <code>false</code> otherwise.
     */
    public boolean isUpdatePomFile()
    {

        if ( this.updatePomFile == null )
        {
            if ( this.flattenMode == FlattenMode.bom )
            {
                return true;
            }
            return !this.project.getPackaging().equals( "pom" );
        }
        else
        {
            return this.updatePomFile.booleanValue();
        }
    }

    /**
     * This class is a simple SAX handler that extracts the first comment located before the root tag in an XML
     * document.
     */
    private class SaxHeaderCommentHandler
        extends DefaultHandler2
    {

        /** <code>true</code> if root tag has already been visited, <code>false</code> otherwise. */
        private boolean rootTagSeen;

        /** @see #getHeaderComment() */
        private String headerComment;

        /**
         * The constructor.
         */
        public SaxHeaderCommentHandler()
        {

            super();
            this.rootTagSeen = false;
        }

        /**
         * @return the XML comment from the header of the document or <code>null</code> if not present.
         */
        public String getHeaderComment()
        {

            return this.headerComment;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void comment( char[] ch, int start, int length )
            throws SAXException
        {

            if ( !this.rootTagSeen )
            {
                if ( this.headerComment == null )
                {
                    this.headerComment = new String( ch, start, length );
                }
                else
                {
                    getLog().warn( "Ignoring multiple XML header comment!" );
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void startElement( String uri, String localName, String qName, Attributes atts )
            throws SAXException
        {

            this.rootTagSeen = true;
        }
    }

}