Write errors and warnings to an XML log file
Merge the XML log files when M2 repos are merged
diff --git a/src/main/groovy/m4e/AbstractCommand.groovy b/src/main/groovy/m4e/AbstractCommand.groovy
index d85ef12..21c53a4 100644
--- a/src/main/groovy/m4e/AbstractCommand.groovy
+++ b/src/main/groovy/m4e/AbstractCommand.groovy
@@ -12,14 +12,16 @@
 package m4e
 
 import java.io.File;
+import m4e.maven.ErrorLog
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-abstract class AbstractCommand {
+abstract class AbstractCommand implements CommonConstants {
 
     Logger log = LoggerFactory.getLogger( getClass() )
     
     File workDir
+    ErrorLog errorLog
     
     void run( List<String> args ) {
         run( args as String[] )
@@ -38,7 +40,17 @@
     }
     
     void destroy() {
+        if( errorLog ) {
+            errorLog.close()
+        }
+    }
+    
+    void prepareErrorLog( File repo, String command ) {
+        if( errorLog ) {
+            errorLog.close()
+        }
         
+        errorLog = new ErrorLog( repo: repo, command: command )
     }
     
     abstract void doRun( String... args );
@@ -46,14 +58,32 @@
     int errorCount = 0
     int warningCount = 0
     
-    void warn( Warning warning, String msg ) {
+    void warn( Warning warning, String msg, Map xml = null ) {
         warningCount ++
         log.warn( msg + '\nFor details, see ' + warning.url() )
+        
+        if( errorLog && xml ) {
+            Map map = new LinkedHashMap()
+            
+            map['code'] = warning.code()
+            map.putAll( xml )
+            
+            errorLog.write().invokeMethod( 'warning', [ map, msg ] ) 
+        }
     }
     
-    void error( Error error, String msg ) {
+    void error( Error error, String msg, Map xml = null ) {
         errorCount ++
         log.error( msg + '\nFor details, see ' + error.url() )
+        
+        if( errorLog && xml ) {
+            Map map = new LinkedHashMap()
+            
+            map['code'] = error.code()
+            map.putAll( xml )
+            
+            errorLog.write().invokeMethod( 'error', [ map, msg ] ) 
+        }
     }
     
     void mergeCounters( AbstractCommand other ) {
diff --git a/src/main/groovy/m4e/CommonConstants.java b/src/main/groovy/m4e/CommonConstants.java
new file mode 100644
index 0000000..bc175d9
--- /dev/null
+++ b/src/main/groovy/m4e/CommonConstants.java
@@ -0,0 +1,13 @@
+package m4e;
+
+public interface CommonConstants {
+
+    /** Folder name inside of a Maven 2 repository where MT4E will put its files */
+    static final String MT4E_FOLDER = ".mt4e";
+    
+    /** File name of the import/export DB in the MT4E folder */
+    static final String IMPORT_EXPORT_DB_FILE = "importExportDB";
+    
+    /** UTF-8 encoding/charset */
+    static final String UTF_8 = "UTF-8";
+}
diff --git a/src/main/groovy/m4e/Error.java b/src/main/groovy/m4e/Error.java
index e7e0c6b..60865f0 100644
--- a/src/main/groovy/m4e/Error.java
+++ b/src/main/groovy/m4e/Error.java
@@ -22,7 +22,11 @@
         this.id = id;
     }
     
+    public String code() {
+        return String.format( "E%04d", id );
+    }
+    
     public String url() {
-        return Warning.BASE_URL + String.format( "E%04d", id );
+        return Warning.BASE_URL + code();
     }
 }
diff --git a/src/main/groovy/m4e/InstallCmd.groovy b/src/main/groovy/m4e/InstallCmd.groovy
index 20934c9..5c0f07c 100644
--- a/src/main/groovy/m4e/InstallCmd.groovy
+++ b/src/main/groovy/m4e/InstallCmd.groovy
@@ -35,6 +35,15 @@
         }
         
         log.info( "Import complete. Imported ${statistics.bundleCount} Eclipse bundles into ${statistics.pomCount} POMs. There are ${statistics.jarCount} new JARs of which ${statistics.sourceCount} have sources" )
+        
+        if( m2repos.size() == 1 ) {
+            log.info( "The new Maven 2 repository is here: ${m2repos[0].absolutePath}" )
+        } else {
+            log.info( "${m2repos.size()} Maven 2 repositories were created:" )
+            m2repos.each {
+                log.info( "    ${it.absolutePath}" )
+            }
+        }
     }
     
     List<File> m2repos = []
@@ -188,6 +197,8 @@
         m2repo = new File( tmpHome, 'm2repo' )
         failure = new File( tmpHome, FAILURE_FILE_NAME )
         
+        installCmd.prepareErrorLog( m2repo, 'install' )
+        
         log.debug( "Importing plug-ins from ${eclipseFolder} into repo ${m2repo}" )
         clean()
 
@@ -339,7 +350,9 @@
     void unpackNestedJar( String nestedJarPath, File jarFile ) {
         
         if( nestedJarPath.contains( ',' ) ) {
-            installCmd.warn( Warning.MULTIPLE_NESTED_JARS, "Multiple nested JARs are not supported; just copying the original bundle" )
+            String msg = "Multiple nested JARs are not supported; just copying the original bundle"
+            Map xml = [ jar: jarFile.absolutePath, nestedJarPath: nestedJarPath ]
+            installCmd.warn( Warning.MULTIPLE_NESTED_JARS, msg, xml )
             bundle.copy( jarFile )
             return
         }
@@ -621,7 +634,8 @@
         
         def entry = archive[ 'META-INF/MANIFEST.MF' ]
         if( !entry ) {
-            installCmd.error( Error.MISSING_MANIFEST, "Can't find manifest in ${file.absolutePath}" )
+            String msg = "Can't find manifest in ${file.absolutePath}"
+            installCmd.error( Error.MISSING_MANIFEST, msg, [ jar: file.absolutePath ] )
             return null
         }
         
@@ -696,7 +710,8 @@
     
     Manifest loadManifestFromFile( File file ) {
         if( !file.exists() ) {
-            installCmd.error( Error.MISSING_MANIFEST, "Can't find manifest ${file.absolutePath}" )
+            String msg = "Can't find manifest ${file.absolutePath}"
+            installCmd.error( Error.MISSING_MANIFEST, msg, [ jar: file.absolutePath ] )
             return null
         }
         
diff --git a/src/main/groovy/m4e/MergeCmd.groovy b/src/main/groovy/m4e/MergeCmd.groovy
index b33ce4d..2992b9c 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 de.pdark.decentxml.XMLUtils;
 
 class MergeCmd extends AbstractCommand {
     
@@ -32,15 +33,44 @@
             throw new UserError( "Target repository ${target} already exists. Cowardly refusing to continue." )
         }
         
+        prepareErrorLog( target, 'merge' )
+        
+        mt4eFolder = new File( target, '.mt4e' )
+        
         log.debug( "Sources: ${sources}" )
         log.debug( "Target: ${target}" )
         
         for( source in sources ) {
             log.info( 'Merging {}', source )
-            merge( new File( source ).absoluteFile, target )
+            File file = new File( source ).absoluteFile
+            
+            merge( file, target )
+        }
+        
+        closeXmlFiles()
+    }
+    
+    void closeXmlFiles() {
+        def exc = null
+        
+        xmlFiles.values().each {
+            try {
+                it << '</merged>\n'
+                it.close()
+            } catch( Exception e ) {
+                if( !exc ) {
+                    exc = e
+                }
+            }
+        }
+        
+        if( exc ) {
+            throw e
         }
     }
     
+    File mt4eFolder
+    
     void merge( File source, File target ) {
         
         target.makedirs()
@@ -48,6 +78,11 @@
         source.eachFile { File srcPath ->
             File targetPath = new File( target, srcPath.name )
             
+            if( targetPath.equals( mt4eFolder ) ) {
+                mergeMt4eFiles( srcPath, mt4eFolder )
+                return
+            }
+            
             if( srcPath.isDirectory() ) {
                 if( targetPath.exists() && !targetPath.isDirectory() ) {
                     throw new RuntimeException( "${srcPath} is a directory but ${targetPath} is a file" )
@@ -70,6 +105,54 @@
         }
     }
     
+    void mergeMt4eFiles( File source, File target ) {
+        source.eachFile { File srcPath ->
+            String name = srcPath.name
+            if( IMPORT_EXPORT_DB_FILE == name ) {
+                return
+            }
+            
+            File targetPath = new File( target, name )
+            
+            if( srcPath.isDirectory() ) {
+                mergeMt4eFiles( srcPath, targetPath )
+                return
+            }
+
+            if( name.endsWith( '.xml' ) ) {
+                mergeXml( srcPath, targetPath )
+            } else {
+                warn( Warning.UNABLE_TO_MERGE_MT4E_FILE, "Unable to merge ${srcPath.absolutePath}", [ file: srcPath.absolutePath ] )
+            }
+        }
+    }
+    
+    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 ] )
+            }
+            
+            target.parentFile?.makedirs()
+            
+            writer = target.newWriter( UTF_8 )
+            xmlFiles[ target.name ] = writer
+            
+            writer << '<merged>\n'
+        }
+        
+        writer << '<source file="' << XMLUtils.escapeXMLText( source.absolutePath ).replace( '"', '&quot;' ) << '">\n'
+        
+        source.withReader( UTF_8 ) {
+            writer << it
+        }
+        
+        writer << '\n</source>\n'
+    }
+    
     boolean filesAreEqual( File source, File target ) {
         if( source.size() != target.size() ) {
             return false
diff --git a/src/main/groovy/m4e/UpdateImportExportDatabaseCmd.groovy b/src/main/groovy/m4e/UpdateImportExportDatabaseCmd.groovy
index 6047258..b03633d 100644
--- a/src/main/groovy/m4e/UpdateImportExportDatabaseCmd.groovy
+++ b/src/main/groovy/m4e/UpdateImportExportDatabaseCmd.groovy
@@ -24,7 +24,7 @@
     public void doRun( String... args ) {
         File repo = repoOption( args, 1 )
         
-        File dbPath = new File( repo, '.mt4e/importExportDB' )
+        File dbPath = new File( repo, MT4E_FOLDER + '/' + IMPORT_EXPORT_DB_FILE )
         
         def db = new ImportExportDB( file: dbPath )
         
diff --git a/src/main/groovy/m4e/Warning.java b/src/main/groovy/m4e/Warning.java
index e63345e..80009d5 100644
--- a/src/main/groovy/m4e/Warning.java
+++ b/src/main/groovy/m4e/Warning.java
@@ -15,7 +15,8 @@
     MISSING_BINARY_BUNDLE_FOR_SOURCES( 1 ),
     UNEXPECTED_FILE_IN_SOURCE_BUNDLE( 2 ),
     BINARY_DIFFERENCE( 3 ),
-    MULTIPLE_NESTED_JARS( 4 );
+    MULTIPLE_NESTED_JARS( 4 ),
+    UNABLE_TO_MERGE_MT4E_FILE( 5 );
     
     public final static String BASE_URL = "http://wiki.eclipse.org/MT4E_";
     
@@ -25,7 +26,11 @@
         this.id = id;
     }
     
+    public String code() {
+        return String.format( "W%04d", id );
+    }
+    
     public String url() {
-        return BASE_URL + String.format( "W%04d", id );
+        return BASE_URL + code();
     }
 }
diff --git a/src/main/groovy/m4e/maven/ErrorLog.groovy b/src/main/groovy/m4e/maven/ErrorLog.groovy
new file mode 100644
index 0000000..8a8b985
--- /dev/null
+++ b/src/main/groovy/m4e/maven/ErrorLog.groovy
@@ -0,0 +1,36 @@
+package m4e.maven;
+
+import groovy.xml.MarkupBuilder
+
+public class ErrorLog {
+
+    File repo
+    String command
+    
+    Writer writer
+    MarkupBuilder builder
+    
+    MarkupBuilder write() {
+        if( ! builder ) {
+            File file = new File( repo, ".mt4e/logs/${command}.xml" )
+            
+            file.parentFile?.makedirs()
+            
+            writer = file.newWriter( 'UTF-8' )
+            builder = new MarkupBuilder( writer )
+            
+            writer.write( "<mt4e-log command='${command}'>\n" )
+        }
+        
+        return builder
+    }
+    
+    void close() {
+        if( writer ) {
+            builder.yield( "\n</mt4e-log>", false )
+            
+            writer.close()
+            writer = null
+        }
+    }
+}