 ------
  Using Container Descriptor Handlers
 ------
  Guillaume Boué
 ------
  2017-07-15
 ------

~~ 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.

~~ NOTE: For help with the syntax of this file, see:
~~ https://maven.apache.org/doxia/references/apt-format.html

Using Container Descriptor Handlers

* Introduction

  Container descriptor handlers can be used to filter dynamically the content
  of files configured in a descriptor, for example by aggregating multiple files 
  into a single file, or customizing the content of specific files.

  This example demonstrate the use of <<<\<containerDescriptorHandlers\>>>> in
  the assembly {{{../../assembly.html}descriptor format}}.

* Built-in container descriptor handlers

  The plugin comes with several handlers already defined.
  
    [<<<file-aggregator>>>] This handler matches the files according to the given regular
    expression <<<filePattern>>>, aggregates their content, and stores the output 
    in the assembly at the given <<<outputPath>>>. A sample descriptor which matches
    all <<<file.txt>>> files configured in the assembly and aggregates them, by 
    appending their content, into a single <<<file.txt>>> located under the base directory
    of the assembly, is:

+-----
<assembly xmlns="http://maven.apache.org/ASSEMBLY/${mdoVersion}"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/${mdoVersion} http://maven.apache.org/xsd/assembly-${mdoVersion}.xsd">
  ....
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>file-aggregator</handlerName>
      <configuration>
        <filePattern>.*/file.txt</filePattern>
        <outputPath>file.txt</outputPath>
      </configuration>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
</assembly>
+-----

    [<<<metaInf-services>>>] This handler matches every <<<META-INF/services>>> file and
    aggregates them into a single <<<META-INF/services>>>. The content of the files
    are appended together.

+-----
<assembly xmlns="http://maven.apache.org/ASSEMBLY/${mdoVersion}"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/${mdoVersion} http://maven.apache.org/xsd/assembly-${mdoVersion}.xsd">
  ....
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>metaInf-services</handlerName>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
</assembly>
+-----

    [<<<metaInf-spring>>>] This handler is similar to <<<metaInf-services>>>. It matches
    every file with a name starting with <<<META-INF/spring.>>>.

+-----
<assembly xmlns="http://maven.apache.org/ASSEMBLY/${mdoVersion}"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/${mdoVersion} http://maven.apache.org/xsd/assembly-${mdoVersion}.xsd">
  ....
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>metaInf-spring</handlerName>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
</assembly>
+-----

    [<<<plexus>>>] This handler matches every <<<META-INF/plexus/components.xml>>> file and
    aggregates them into a single valid <<<META-INF/plexus/components.xml>>>.

+-----
<assembly xmlns="http://maven.apache.org/ASSEMBLY/${mdoVersion}"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/${mdoVersion} http://maven.apache.org/xsd/assembly-${mdoVersion}.xsd">
  ....
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>plexus</handlerName>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
</assembly>
+-----

    []

* Custom container descriptor handlers

  You can create your own container descriptor handler by creating a class implementing 
  <<<ContainerDescriptorHandler>>>. As an example, let's create a handler that will prepend 
  a configured comment to every properties file configured in an assembly descriptor. 

  We start by creating a new Maven project named <<<custom-container-descriptor-handler>>>
  with the following POM:

+-----
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>custom-container-descriptor-handler</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-assembly-plugin</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.plexus</groupId>
        <artifactId>plexus-component-metadata</artifactId>
        <version>1.7.1</version>
        <executions>
          <execution>
            <goals>
              <goal>generate-metadata</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
+-----

  This POM declares a dependency on the Assembly Plugin so that we can create
  our handler, and generates a Plexus configuration file so that it can be
  found through dependency injection during assembling.

  Implementing <<<ContainerDescriptorHandler>>> requires defining a couple of 
  methods:
  
    [<<<isSelected>>>] Tells whether a given file or directory, configured in the
    assembly descriptor, should be added to the final assembly. A typical
    set-up would be to prevent the addition of certain files, and let the
    handler do its work on them.
    
    [<<<getVirtualFiles>>>] Returns the list of file paths, from the root of the assembly,
    of each file this handler will add.
    
    [<<<finalizeArchiveCreation>>>] Callback that is invoked when an assembly is going
    to be created. This method can be used to add files that resulted from the
    handler's work on each selected file. <<Cave-at:>> Because of the way archive
    finalization is performed, we need to loop through each resource in the archive
    before doing anything.
    
    [<<<finalizeArchiveExtraction>>>] Callback that is invoked when an assembly has been
    extracted into a directory. This method can be used to process files that resulted
    from the handler's work on each selected file.

    []
    
  Our handler that prepends a comment to each properties file could look like the following
  (using here Java 8 features):

+-----
package com.test;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.maven.plugins.assembly.filter.ContainerDescriptorHandler;
import org.apache.maven.plugins.assembly.utils.AssemblyFileUtils;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.components.io.fileselectors.FileInfo;

@Component(role = ContainerDescriptorHandler.class, hint = "custom")
public class MyCustomDescriptorHandler implements ContainerDescriptorHandler {

    private String comment;

    private Map<String, List<String>> catalog = new HashMap<>();

    private boolean excludeOverride = false;

    @Override
    public void finalizeArchiveCreation(Archiver archiver) throws ArchiverException {
        archiver.getResources().forEachRemaining(a -> {}); // necessary to prompt the isSelected() call

        for (Map.Entry<String, List<String>> entry : catalog.entrySet())
        {
            String name = entry.getKey();
            String fname = new File(name).getName();

            Path p;
            try {
                p = Files.createTempFile("assembly-" + fname, ".tmp");
            } catch (IOException e) {
                throw new ArchiverException("Cannot create temporary file to finalize archive creation", e);
            }

            try (BufferedWriter writer = Files.newBufferedWriter(p, StandardCharsets.ISO_8859_1)) {
                writer.write("# " + comment);
                for (String line : entry.getValue()) {
                    writer.newLine();
                    writer.write(line);
                }
            } catch (IOException e) {
                throw new ArchiverException("Error adding content of " + fname + " to finalize archive creation", e);
            }

            File file = p.toFile();
            file.deleteOnExit();
            excludeOverride = true;
            archiver.addFile(file, name);
            excludeOverride = false;
        }
    }

    @Override
    public void finalizeArchiveExtraction(UnArchiver unarchiver) throws ArchiverException { }

    @Override
    public List<String> getVirtualFiles() {
        return new ArrayList<>(catalog.keySet());
    }

    @Override
    public boolean isSelected(FileInfo fileInfo) throws IOException {
        if (excludeOverride) {
            return true;
        }
        String name = AssemblyFileUtils.normalizeFileInfo(fileInfo);
        if (fileInfo.isFile() && AssemblyFileUtils.isPropertyFile(name)) {
            catalog.put(name, readLines(fileInfo));
            return false;
        }
        return true;
    }

    private List<String> readLines(FileInfo fileInfo) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileInfo.getContents(), StandardCharsets.ISO_8859_1))) {
            return reader.lines().collect(Collectors.toList());
        }
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

}
+-----

  It is a Plexus component, having the <<<ContainerDescriptorHandler>>> role,
  that is distinguished from the other handlers with its <<<hint>>> of <<<custom>>>.

  It selects each properties file and stores their content into a catalog map,
  where the key is the name of the file and the value is a list of its lines.
  Those matched files are not added to the assembly, because the handler needs
  to process them first. During assembly creation, it creates temporary files
  whose content are the previously read lines, prepended by a custom comment.
  They are then added back into the archive with their previous name. Note that
  this simple handler does not aggregate files with the same name - it could be
  enhanced to do it. When the temporary files are added to the archive, the 
  <<<isSelected>>> method is automatically called, hence we need to set a boolean 
  <<<excludeOverride>>> to <<<true>>> to make sure the catalog processing part is not done.

  The last ingredient is using our custom handler in an assembly descriptor of
  some Maven project. Suppose there is a <<<src/samples>>> directory in this project,
  containing an XML file named <<<test.xml>>> and a properties file named 
  <<<test.properties>>>. With the following descriptor format

+-----
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
  <id>dist</id>
  <formats>
    <format>zip</format>
  </formats>
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>custom</handlerName>
      <configuration>
        <comment>A comment</comment>
      </configuration>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
  <fileSets>
    <fileSet>
      <directory>src/samples</directory>
      <outputDirectory></outputDirectory>
    </fileSet>
  </fileSets>
</assembly>
+-----

  and the following Assembly plugin configuration

+-----
<project>
  [...]
  <build>
    [...]
    <plugins>
      [...]
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>${project.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <descriptors>
                <descriptor>src/assemble/assembly.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>com.test</groupId>
            <artifactId>custom-container-descriptor-handler</artifactId>
            <version>0.0.1-SNAPSHOT</version>
          </dependency>
        </dependencies>
      </plugin>
  [...]
</project>
+-----

  the resulting assembly would contain both <<<test.xml>>> and <<<test.properties>>>
  under the base directory, with only the latter starting with <<<# A comment>>>.
