Create a mapping file for snapshot versions
Support to merge these mapping files
diff --git a/data/input/mergeTest/repo1/.mt4e/snapshotVersionMapping b/data/input/mergeTest/repo1/.mt4e/snapshotVersionMapping
new file mode 100644
index 0000000..666b99c
--- /dev/null
+++ b/data/input/mergeTest/repo1/.mt4e/snapshotVersionMapping
@@ -0,0 +1,3 @@
+b:y 1.0 1.0-SNAPSHOT
+c:x 1.0 1.0-SNAPSHOT
+d:x 1.1 1.1-SNAPSHOT
diff --git a/data/input/mergeTest/repo2/.mt4e/snapshotVersionMapping b/data/input/mergeTest/repo2/.mt4e/snapshotVersionMapping
new file mode 100644
index 0000000..4b85dcf
--- /dev/null
+++ b/data/input/mergeTest/repo2/.mt4e/snapshotVersionMapping
@@ -0,0 +1,3 @@
+b:x 2.0 2.0-SNAPSHOT
+c:x 1.0 1.0-SNAPSHOT
+d:x 1.2 1.1-SNAPSHOT
diff --git a/src/main/groovy/m4e/CommonConstants.java b/src/main/groovy/m4e/CommonConstants.java
index bc175d9..6a08b56 100644
--- a/src/main/groovy/m4e/CommonConstants.java
+++ b/src/main/groovy/m4e/CommonConstants.java
@@ -8,6 +8,12 @@
     /** File name of the import/export DB in the MT4E folder */
     static final String IMPORT_EXPORT_DB_FILE = "importExportDB";
     
+    /** File name of the snapshot version mapping file in the MT4E folder */
+    static final String SNAPSHOT_VERSION_MAPPING_FILE = "snapshotVersionMapping";
+    
     /** UTF-8 encoding/charset */
     static final String UTF_8 = "UTF-8";
+    
+    static final String SNAPSHOT = "SNAPSHOT";
+    static final String MINUS_SNAPSHOT = "-" + SNAPSHOT;
 }
diff --git a/src/main/groovy/m4e/InstallCmd.groovy b/src/main/groovy/m4e/InstallCmd.groovy
index ad68a4d..19fb73b 100644
--- a/src/main/groovy/m4e/InstallCmd.groovy
+++ b/src/main/groovy/m4e/InstallCmd.groovy
@@ -177,6 +177,42 @@
      * 
      * <p>Eclipse key (value of Eclipse-SourceBundle) -> Maven path of source JAR */
     Map<String, File> unknownMap = [:]
+    
+    void setM2repo( File m2repo ) {
+        snapshotVersionMappingFile = new File( m2repo, CommonConstants.MT4E_FOLDER + '/' + CommonConstants.SNAPSHOT_VERSION_MAPPING_FILE )
+    }
+    
+    File snapshotVersionMappingFile
+    Writer snapshotVersionWriter
+    Map<String, String> existingEclipseMappings = [:]
+    Map<String, String> existingMavenMappings = [:]
+    
+    void appendSnapshotVersion( String shortKey, String eclipseVersion, String mavenVersion ) {
+        if( !snapshotVersionWriter ) {
+            snapshotVersionMappingFile.parentFile?.makedirs()
+            
+            snapshotVersionWriter = snapshotVersionMappingFile.newWriter( CommonConstants.UTF_8 )
+        }
+        
+        String key = "${shortKey}:${mavenVersion}"
+        String old = existingEclipseMappings.put( key, eclipseVersion )
+        assert old == null, "There is an existing mapping ${key}:${old} -> ${eclipseVersion}"
+        
+        key = "${shortKey}:${eclipseVersion}"
+        old = existingMavenMappings.put( key, mavenVersion )
+        assert old == null, "There is an existing mapping ${key}:${old} -> ${mavenVersion}"
+        
+        String line = "${shortKey} ${eclipseVersion.replaceAll( '\\s+', '' )} ${mavenVersion.replaceAll( '\\s+', '' )}\n"
+        snapshotVersionWriter << line
+    }
+    
+    void close() {
+        println 'close'
+        if( snapshotVersionWriter ) {
+            snapshotVersionWriter.close()
+            snapshotVersionWriter = null
+        }
+    }
 }
 
 class ImportTool {
@@ -217,9 +253,15 @@
         log.debug( "Importing plug-ins from ${eclipseFolder} into repo ${m2repo}" )
         clean()
 
-        doImport()
+        versionMap.m2repo = m2repo
         
-        moveSourceBundles()
+        try {
+            doImport()
+        
+            moveSourceBundles()
+        } finally {
+            versionMap.close()
+        }
         
         log.info( 'OK' )
         
@@ -243,15 +285,8 @@
     }
     
     void doImport( File bundle ) {
-        def tool = new BundleConverter( installCmd: installCmd, m2repo: m2repo, statistics: installCmd.statistics, versionMap: versionMap )
-        
-        if( bundle.isDirectory() ) {
-            tool.importExplodedBundle( bundle )
-        } else {
-            tool.importBundle( bundle )
-        }
-        
-        tool.close()
+        def tool = new BundleConverter( installCmd: installCmd, m2repo: m2repo, versionMap: versionMap )
+        tool.importBundle( bundle )
     }
     
     /** Make sure we don't have any leftovers from previous attempts. */
@@ -314,10 +349,23 @@
     
     File bundle
     
-    ImportStatistics statistics
     SourceSnapshotVersionMap versionMap
     
-    void importBundle( File bundleJar ) {
+    void importBundle( File bundle ) {
+        
+        try {
+            if( bundle.isDirectory() ) {
+                importExplodedBundle( bundle )
+            } else {
+                importJarBundle( bundle )
+            }
+        } finally {
+            close()
+        }
+    }
+    
+    void importJarBundle( File bundleJar ) {
+        
         this.bundle = bundleJar
         
         manifest = loadManifestFromJar( bundleJar )
@@ -325,7 +373,7 @@
             return
         }
         
-        statistics.bundleCount ++
+        installCmd.statistics.bundleCount ++
         
         log.debug( 'Importing {}', bundleJar )
         
@@ -349,7 +397,7 @@
             bundleJar.copy( jarFile )
         }
         
-        statistics.jarCount ++
+        installCmd.statistics.jarCount ++
         
         File pomFile = MavenRepositoryTools.buildPath( m2repo, key, 'pom' )
         pomFile.withWriter( 'UTF-8' ) {
@@ -370,6 +418,10 @@
                 
                 String pomVersion = p.getProperty( 'version' )
                 if( pomVersion ) {
+                    if( pomVersion.endsWith( CommonConstants.MINUS_SNAPSHOT ) ) {
+                        versionMap.appendSnapshotVersion( "${groupId}:${artifactId}", version, pomVersion )
+                    }
+                    
                     version = pomVersion
                 }
                 
@@ -455,7 +507,7 @@
     
     void createPom( Writer writer ) {
         
-        statistics.pomCount ++
+        installCmd.statistics.pomCount ++
         
         writer << '<?xml version="1.0" encoding="UTF-8"?>\n'
         writer << '<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"\n'
@@ -639,7 +691,7 @@
     
     void importSourceBundle( Manifest manifest, ManifestElement[] sourceBundleFor ) {
         
-        statistics.sourceCount ++
+        installCmd.statistics.sourceCount ++
         
         if( sourceBundleFor.size() != 1 ) {
             throw new RuntimeException( "Expected exactly one element in ${sourceBundleFor}" )
@@ -755,7 +807,7 @@
             return
         }
         
-        statistics.bundleCount ++
+        installCmd.statistics.bundleCount ++
         
         log.debug( 'Importing {}', bundleFolder )
         bundle = bundleFolder
@@ -779,7 +831,7 @@
             packBundle( bundleFolder, jarFile )
         }
         
-        statistics.jarCount ++
+        installCmd.statistics.jarCount ++
         
         File pomFile = MavenRepositoryTools.buildPath( m2repo, key, 'pom' )
         pomFile.withWriter( 'UTF-8' ) {
diff --git a/src/main/groovy/m4e/MergeCmd.groovy b/src/main/groovy/m4e/MergeCmd.groovy
index 1e7315a..f7fa746 100644
--- a/src/main/groovy/m4e/MergeCmd.groovy
+++ b/src/main/groovy/m4e/MergeCmd.groovy
@@ -12,6 +12,7 @@
 package m4e
 
 import java.io.File;
+import java.util.Map;
 import de.pdark.decentxml.XMLUtils;
 
 class MergeCmd extends AbstractCommand {
@@ -47,6 +48,16 @@
             merge( file, target )
         }
         
+        close()
+    }
+    
+    void close() {
+        
+        if( snapshotVersionWriter ) {
+            snapshotVersionWriter.close()
+            snapshotVersionWriter = null
+        }
+        
         closeXmlFiles()
     }
     
@@ -119,6 +130,11 @@
                 return
             }
 
+            if( name == SNAPSHOT_VERSION_MAPPING_FILE ) {
+                mergeSnapshotVersions( srcPath, targetPath )
+                return
+            }
+            
             if( name.endsWith( '.xml' ) ) {
                 mergeXml( srcPath, targetPath )
             } else {
@@ -127,13 +143,53 @@
         }
     }
     
+    Writer snapshotVersionWriter
+    Map<String, String> existingEclipseMappings = [:]
+    Map<String, String> existingMavenMappings = [:]
+    
+    void mergeSnapshotVersions( File source, File target ) {
+        if( ! snapshotVersionWriter ) {
+            if( target.exists() ) {
+                warn( Warning.UNABLE_TO_MERGE_MT4E_FILE, "Target ${target.absolutePath} already exists and will be overwritten", [ file: source.absolutePath ] )
+            }
+            
+            target.parentFile?.makedirs()
+            
+            snapshotVersionWriter = target.newWriter( UTF_8 )
+        }
+        
+        source.eachLine( UTF_8 ) {
+            def (shortKey, eclipseVersion, mavenVersion) = it.split( ' ' )
+            
+            String key = "${shortKey}:${mavenVersion}"
+            String old = existingEclipseMappings.put( key, eclipseVersion )
+            if( old ) {
+                if( old != eclipseVersion ) {
+                    warn( Warning.DUPLICATE_VERSION_MAPPING, "There is an existing mapping ${key}:${old} -> ${eclipseVersion}")
+                }
+                
+                return
+            }
+            
+            key = "${shortKey}:${eclipseVersion}"
+            old = existingMavenMappings.put( key, mavenVersion )
+            if( old ) {
+                warn( Warning.DUPLICATE_VERSION_MAPPING, "There is an existing mapping ${key}:${old} -> ${mavenVersion}" )
+                return
+            }
+            
+            String line = "${shortKey} ${eclipseVersion} ${mavenVersion}\n"
+            snapshotVersionWriter << line
+        }
+    }
+    
     Map<String, Writer> xmlFiles = [:]
     
     void mergeXml( File source, File target ) {
         def writer = xmlFiles[ target.name ]
         if( !writer ) {
             if( target.exists() ) {
-                warn( Warning.UNABLE_TO_MERGE_MT4E_FILE, "Unable to merge ${source.absolutePath}", [ file: source.absolutePath ] )
+                warn( Warning.UNABLE_TO_MERGE_MT4E_FILE, "Target ${target.absolutePath} already exists and will be overwritten", [ file: source.absolutePath ] )
             }
             
             target.parentFile?.makedirs()
diff --git a/src/main/groovy/m4e/Warning.java b/src/main/groovy/m4e/Warning.java
index 8edc0e4..32bd119 100644
--- a/src/main/groovy/m4e/Warning.java
+++ b/src/main/groovy/m4e/Warning.java
@@ -19,7 +19,8 @@
     UNEXPECTED_FILE_IN_SOURCE_BUNDLE( 2 ),
     BINARY_DIFFERENCE( 3 ),
     MULTIPLE_NESTED_JARS( 4 ),
-    UNABLE_TO_MERGE_MT4E_FILE( 5 );
+    UNABLE_TO_MERGE_MT4E_FILE( 5 ),
+    DUPLICATE_VERSION_MAPPING( 6 );
     
     public final static String BASE_URL = "http://wiki.eclipse.org/MT4E_";
     
diff --git a/src/test/groovy/m4e/BundleConverterTest.groovy b/src/test/groovy/m4e/BundleConverterTest.groovy
index 346dcf8..546d1cb 100644
--- a/src/test/groovy/m4e/BundleConverterTest.groovy
+++ b/src/test/groovy/m4e/BundleConverterTest.groovy
@@ -110,13 +110,18 @@
         InstallCmd cmd = new InstallCmd( workDir: workDir )
         
         ImportTool tool = new ImportTool( m2repo: repo, installCmd: cmd )
+        tool.versionMap.m2repo = repo
+        
         tool.doImport( new File( plugins, 'de.itemis.xtext.typesystem.source_2.0.5.201205161310.jar' ) )
         tool.doImport( new File( plugins, 'de.itemis.xtext.typesystem_2.0.5.201205161310.jar' ) )
         tool.doImport( new File( plugins, 'org.apache.commons.lang3_3.1.0.jar' ) )
         tool.doImport( new File( plugins, 'org.apache.commons.lang3.sources_3.1.0.jar' ) )
         
+        tool.versionMap.close()
+        
         // Sources not yet in the right place
         assertEquals( '''\
+.mt4e/snapshotVersionMapping
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.jar
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.pom
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5.201205161310/de.itemis.xtext.typesystem-2.0.5.201205161310-sources.jar
@@ -128,6 +133,7 @@
         tool.moveSourceBundles()
         
         assertEquals( '''\
+.mt4e/snapshotVersionMapping
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT-sources.jar
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.jar
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.pom
@@ -137,6 +143,9 @@
             CommonTestCode.listFiles( repo ) )
         
         CommonTestCode.fileEquals( '''\
+de.itemis.xtext:de.itemis.xtext.typesystem 2.0.5.201205161310 2.0.5-SNAPSHOT''', new File( repo, '.mt4e/snapshotVersionMapping' ) )
+        
+        CommonTestCode.fileEquals( '''\
 <?xml version="1.0" encoding="UTF-8"?>
 <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
@@ -230,12 +239,15 @@
         InstallCmd cmd = new InstallCmd( workDir: workDir )
         
         ImportTool tool = new ImportTool( m2repo: repo, installCmd: cmd )
+        tool.versionMap.m2repo = repo
+        
         tool.doImport( new File( plugins, 'de.itemis.xtext.typesystem_2.0.5.201205161310.jar' ) )
         tool.doImport( new File( plugins, 'de.itemis.xtext.typesystem.source_2.0.5.201205161310.jar' ) )
         tool.doImport( new File( plugins, 'org.apache.commons.lang3.sources_3.1.0.jar' ) )
         tool.doImport( new File( plugins, 'org.apache.commons.lang3_3.1.0.jar' ) )
 
         String expected = '''\
+.mt4e/snapshotVersionMapping
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT-sources.jar
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.jar
 de/itemis/xtext/de.itemis.xtext.typesystem/2.0.5-SNAPSHOT/de.itemis.xtext.typesystem-2.0.5-SNAPSHOT.pom
@@ -256,9 +268,9 @@
     private String convert( String manifest ) {
         def m = new Manifest( new ByteArrayInputStream( manifest.getBytes( 'UTF-8' ) ) )
         
-        def statistics = new ImportStatistics()
+        def cmd = new InstallCmd()
         
-        def tool = new BundleConverter( manifest: m, statistics: statistics )
+        def tool = new BundleConverter( manifest: m, installCmd: cmd )
         tool.examineManifest()
         
         def buffer = new StringWriter()
diff --git a/src/test/groovy/m4e/MergeCmdTest.groovy b/src/test/groovy/m4e/MergeCmdTest.groovy
index f819a68..8c7df9b 100644
--- a/src/test/groovy/m4e/MergeCmdTest.groovy
+++ b/src/test/groovy/m4e/MergeCmdTest.groovy
@@ -3,12 +3,12 @@
 import static org.junit.Assert.*;
 import org.junit.Test;
 
-class MergeCmdTest implements CommonConstants {
+class MergeCmdTest extends CommonTestCode implements CommonConstants {
 
     @Test
     public void testMerge() throws Exception {
         
-        File target = CommonTestCode.newFile( 'testMerge' )
+        File target = newFile( 'testMerge' )
         target.parentFile?.makedirs()
         
         def tool = new MergeCmd()
@@ -38,13 +38,27 @@
         
         assert target.exists()
         
-        File logFolder = new File( target, MT4E_FOLDER + '/logs' )
+        File mt4eFolder = new File( target, MT4E_FOLDER )
+        
+        assertEquals( '''\
+logs/install.xml
+logs/merge.xml
+logs/only_repo1.xml
+logs/only_repo2.xml
+snapshotVersionMapping'''
+            , listFiles( mt4eFolder ) )
+        
+        File logFolder = new File( mt4eFolder, 'logs' )
+        
+        def lines = read( new File( logFolder, 'merge.xml' ) ).split( '\n' )
+        lines = [lines[0]] + lines[1..-2].sort() + [lines[-1]]
         
         assertEquals( '''\
 <mt4e-log command='merge'>
 <warning code='W0003' source='${input2}/org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.jar' target='${m2repo}/org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.jar'>File ${input2}/org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.jar differs from ${m2repo}/org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.jar</warning>
+<warning code='W0006'>There is an existing mapping d:x:1.1-SNAPSHOT:1.1 -&gt; 1.2</warning>
 </mt4e-log>'''
-            , read( new File( logFolder, 'merge.xml' ) ) )
+            , lines.join( '\n' ) )
         
         assertEquals( '''\
 <merged>
@@ -84,6 +98,13 @@
 '''
             , read( new File( logFolder, 'only_repo2.xml' ) ) )
         
+        fileEquals( '''\
+b:y 1.0 1.0-SNAPSHOT
+c:x 1.0 1.0-SNAPSHOT
+d:x 1.1 1.1-SNAPSHOT
+b:x 2.0 2.0-SNAPSHOT'''
+            , new File( mt4eFolder, SNAPSHOT_VERSION_MAPPING_FILE ) )
+        
         assertEquals( 'xxx\n', read( new File( target, 'org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.jar' ) ) )
         assertEquals( 'xxx\n', read( new File( target, 'org/eclipse/core/org.eclipse.core.resources/3.7.101/org.eclipse.core.resources-3.7.101.pom' ) ) )
     }