Support XML logs in the analyze command
diff --git a/data/expected/repo1-analysis-19700101-000000.html b/data/expected/repo1-analysis-19700101-000000.html
index 0860b16..9d9dd24 100644
--- a/data/expected/repo1-analysis-19700101-000000.html
+++ b/data/expected/repo1-analysis-19700101-000000.html
@@ -20,35 +20,62 @@
   <body>
     <h1>Analysis of ${repo} (1970.01.01 00:00:00)</h1>
     <p>Found 6 POM files</p>
-    <p>Found 20 problems</p>
+    <p>Found 25 problems</p>
     <h2>Table of Contents</h2>
     <ul class='toc'>
       <li>
-        <a href='#toc1'>POMs with same ID but different version (1)</a>
+        <a href='#toc1'>Plug-ins Which Couldn't be Imported (2)</a>
       </li>
       <li>
-        <a href='#toc2'>Problems With Dependencies (1)</a>
+        <a href='#toc2'>Plug-ins Without Manifest (2)</a>
       </li>
       <li>
-        <a href='#toc3'>Dependencies With Different Versions (2)</a>
+        <a href='#toc3'>POMs with same ID but different version (1)</a>
       </li>
       <li>
-        <a href='#toc4'>Missing Dependencies (12)</a>
+        <a href='#toc4'>Problems With Dependencies (1)</a>
       </li>
       <li>
-        <a href='#toc5'>Snapshot Versions (2)</a>
+        <a href='#toc5'>Dependencies With Different Versions (2)</a>
       </li>
       <li>
-        <a href='#toc6'>Path Problems (1)</a>
+        <a href='#toc6'>Missing Dependencies (12)</a>
       </li>
       <li>
-        <a href='#toc7'>Missing Sources (3)</a>
+        <a href='#toc7'>Snapshot Versions (2)</a>
+      </li>
+      <li>
+        <a href='#toc8'>Path Problems (1)</a>
+      </li>
+      <li>
+        <a href='#toc9'>Multiple Nested JARs (1)</a>
+      </li>
+      <li>
+        <a href='#toc10'>Missing Sources (3)</a>
       </li>
       <li>
         <a href='#poms'>6 POMs in the repository</a>
       </li>
     </ul>
-    <h2 id='toc1'>POMs with same ID but different version</h2>
+    <h2 id='toc1'>Plug-ins Which Couldn't be Imported</h2>
+    <p>2 times</p>
+    <div class='problem'>
+      <span class='message'>Error processing ${input}/plugins/file.txt: java.util.zip.ZipException: error in opening zip file</span>
+    </div>
+    <div class='problem'>
+      <span class='message'>Error processing ${input}/plugins/notajar.jar: java.util.zip.ZipException: error in opening zip file</span>
+    </div>
+    <h2 id='toc2'>Plug-ins Without Manifest</h2>
+    <p>2 times</p>
+    <div class='problem'>
+      <div class='ignoreKey'>MissingManifest E0003 nomanifest.jar</div>
+      <span class='message'>Couldn't find MANIFEST.MF in ${input}/plugins/nomanifest.jar</span>
+    </div>
+    <div class='problem'>
+      <div class='ignoreKey'>MissingManifest E0003 unpackedPlugin</div>
+      <span class='message'>Couldn't find MANIFEST.MF in ${input}/plugins/unpackedPlugin</span>
+    </div>
+    <h2 id='toc3'>POMs with same ID but different version</h2>
     <p>1 time</p>
     <div class='problem'>
       <div class='ignoreKey'>ProblemSameKeyDifferentVersion org.eclipse.core:org.eclipse.core.runtime:3.5.0 org.eclipse.core:org.eclipse.core.runtime:3.6.0</div>There are two POMs with the same ID but different version:
@@ -66,14 +93,14 @@
         </li>
       </ul>
     </div>
-    <h2 id='toc2'>Problems With Dependencies</h2>
+    <h2 id='toc4'>Problems With Dependencies</h2>
     <p>1 time</p>
     <div class='problem'>POM 
       <span class='pom'>junit.test:missing-version:1.0</span> 
       <span class='message'>Missing version in dependency</span> 
       <span class='dependency'>org.slf4j:slf4j-api:null</span>
     </div>
-    <h2 id='toc3'>Dependencies With Different Versions</h2>
+    <h2 id='toc5'>Dependencies With Different Versions</h2>
     <p>2 times</p>
     <div class='problem'>
       <div class='ignoreKey'>ProblemDifferentVersions null org.eclipse.equinox:org.eclipse.equinox.preferences</div>The dependency 
@@ -127,7 +154,7 @@
         </li>
       </ul>
     </div>
-    <h2 id='toc4'>Missing Dependencies</h2>
+    <h2 id='toc6'>Missing Dependencies</h2>
     <p>The following dependencies are used in POMs in the repository but they couldn't be found in it.</p>
     <p>12 times</p>
     <div class='problem'>
@@ -265,7 +292,7 @@
         </li>
       </ul>
     </div>
-    <h2 id='toc5'>Snapshot Versions</h2>
+    <h2 id='toc7'>Snapshot Versions</h2>
     <p>Release Repositories should not contain SNAPSHOTs</p>
     <p>2 times</p>
     <div class='problem'>The POM 
@@ -275,7 +302,7 @@
       <span class='dependency'>de.pdark:decentxml:1.4-SNAPSHOT</span> in POM 
       <span class='pom'>junit.test:snapshot-version:0.3.0-SNAPSHOT</span> uses a snapshot version
     </div>
-    <h2 id='toc6'>Path Problems</h2>
+    <h2 id='toc8'>Path Problems</h2>
     <p>These POMs are not where they should be</p>
     <p>1 time</p>
     <div class='problem'>The path for the POM 
@@ -283,7 +310,13 @@
       <span class='file'>${repo}/junit/test/different-version/1.0/different-version-1.0.pom</span> but was 
       <span class='file'>${repo}/junit/test/different-versions/1.0/different-versions-1.0.pom</span>
     </div>
-    <h2 id='toc7'>Missing Sources</h2>
+    <h2 id='toc9'>Multiple Nested JARs</h2>
+    <p>1 time</p>
+    <div class='problem'>
+      <div class='ignoreKey'>MultipleNestedJarsProblem W0004 org/eclipse/birt/org.eclipse.birt.report.data.oda.jdbc.dbprofile/3.7.0.v20110603/org.eclipse.birt.report.data.oda.jdbc.dbprofile-3.7.0.v20110603.jar .,src</div>
+      <span class='message'>Found multiple nested JARs in .../m2repo/org/eclipse/birt/org.eclipse.birt.report.data.oda.jdbc.dbprofile/3.7.0.v20110603/org.eclipse.birt.report.data.oda.jdbc.dbprofile-3.7.0.v20110603.jar</span>
+    </div>
+    <h2 id='toc10'>Missing Sources</h2>
     <p>3 times</p>
     <div class='problem'>
       <p>Missing sources for 3 artifacts</p>
diff --git a/data/input/repo1/.mt4e/logs/install.xml b/data/input/repo1/.mt4e/logs/install.xml
new file mode 100644
index 0000000..0b378ec
--- /dev/null
+++ b/data/input/repo1/.mt4e/logs/install.xml
@@ -0,0 +1,12 @@
+<merged>
+<source file=".../m2repo/.mt4e/logs/install.xml">
+<mt4e-log command='install'>
+<mt4e-log command='install'>
+<error code='E0003' jar='${input}/plugins/nomanifest.jar'>Can't find manifest in ${input}/plugins/nomanifest.jar</error>
+<error code='E0003' jar='${input}/plugins/unpackedPlugin/META-INF/MANIFEST.MF'>Can't find manifest ${input}/plugins/unpackedPlugin/META-INF/MANIFEST.MF</error>
+<error code='E0004' file='${input}/plugins/file.txt' exception='error in opening zip file'>Error processing ${input}/plugins/file.txt: java.util.zip.ZipException: error in opening zip file</error>
+<error code='E0004' file='${input}/plugins/notajar.jar' exception='error in opening zip file'>Error processing ${input}/plugins/notajar.jar: java.util.zip.ZipException: error in opening zip file</error>
+<warning code='W0004' jar='.../m2repo/org/eclipse/birt/org.eclipse.birt.report.data.oda.jdbc.dbprofile/3.7.0.v20110603/org.eclipse.birt.report.data.oda.jdbc.dbprofile-3.7.0.v20110603.jar' nestedJarPath='.,src'>Multiple nested JARs are not supported; just copying the original bundle</warning>
+</mt4e-log></mt4e-log>
+</source>
+</merged>
diff --git a/src/main/groovy/m4e/AnalyzeCmd.groovy b/src/main/groovy/m4e/AnalyzeCmd.groovy
index b3180ad..dbfebc5 100644
--- a/src/main/groovy/m4e/AnalyzeCmd.groovy
+++ b/src/main/groovy/m4e/AnalyzeCmd.groovy
@@ -52,7 +52,7 @@
     }
 }
 
-class Analyzer {
+class Analyzer implements CommonConstants {
     
     static final Logger log = LoggerFactory.getLogger( Analyzer )
     
@@ -100,6 +100,8 @@
             analyzePom( it )
         }
         
+        loadXmlLogs()
+        
         sortEverything()
         
         if( missingSource ) {
@@ -127,6 +129,98 @@
         report()
     }
     
+    void loadXmlLogs() {
+        File folder = new File( repo, MT4E_FOLDER + '/logs' )
+        
+        loadXmlLog( folder )
+    }
+    
+    void loadXmlLog( File file ) {
+        if( file.isDirectory() ) {
+            file.eachFile {
+                loadXmlLog( it )
+            }
+            return
+        }
+
+        try {
+            log.debug( 'Loading {}', file.absolutePath )
+            
+            def root = new XmlSlurper().parse( file )
+            String path = null
+            String command = null
+            
+            for( def node : root.depthFirst() ) {
+                String name = node.name()
+                
+                switch( name ) {
+                case 'source':
+                    path = node.'@file'
+                    break
+                
+                case 'mt4e-log':
+                    command = node.'@command'
+                    break
+                
+                case 'warning':
+                case 'error':
+                    xmlToProblem( command, path, node )
+                    break
+                
+                case 'merged':
+                    break
+                
+                default: log.warn( "Unexpected node '${name}'" )
+                }
+            }
+        } catch( Exception e ) {
+            throw new RuntimeException( "Error loading XML from ${file.absolutePath}", e )
+        }
+    }
+    
+    void xmlToProblem( String command, String path, node ) {
+        String code = node.'@code'
+        
+        def e
+        if( 'W' == code[0] ) {
+            e = Warning.fromCode( code )
+        } else if( 'E' == code[0] ) {
+            e = Error.fromCode( code )
+        } else {
+            throw new RuntimeException( "Unsupported code ${code}" )
+        }
+        
+        def problem
+        
+        switch( e ) {
+        case Error.MISSING_MANIFEST:
+            problem = MissingManifest.create( node )
+            break
+            
+        case Error.IMPORT_ERROR:
+            problem = ImportError.create( node )
+            break
+            
+        case Warning.MULTIPLE_NESTED_JARS:
+            problem = MultipleNestedJarsProblem.create( node )
+            break
+        
+        default:
+            log.warn( "Unsupported code ${code} ${e}" )
+            return 
+        }
+        
+        problem.command = command
+        problem.logFile = path
+        problem.code = code
+        
+        if( ! problem.message ) {
+            problem.message = node.text()
+        }
+        
+        problems << problem
+    }
+    
     void report() {
 //        textReport()
         htmlReport()
@@ -534,7 +628,7 @@
     
     @Override
     public String toString() {
-        return "POM ${pom?.key()}: ${message}";
+        return "POM ${pom?.key()}: ${message}"
     }
     
     void render( MarkupBuilder builder ) {
@@ -559,6 +653,88 @@
     }
 }
 
+class CommandProblem extends Problem {
+    
+    String logFile
+    String command
+    String code
+    String key
+    
+    CommandProblem() {
+        super( null, null )
+    }
+    
+    @Override
+    public String toString() {
+        return message
+    }
+    
+    void render( MarkupBuilder builder ) {
+        builder.div( 'class': 'problem' ) {
+            if( key ) {
+                div( 'class': 'ignoreKey', key() )
+            }
+            span( 'class': 'message',  message )
+        }
+    }
+    
+    String key() {
+        return "${getClass().simpleName} ${key}"
+    }
+    
+    String sortKey() {
+        return key
+    }
+}
+
+class MultipleNestedJarsProblem extends CommandProblem {
+    
+    String jar
+    String nestedJarPath
+    String relPath
+    
+    static MultipleNestedJarsProblem create( node ) {
+        
+        String jar = PathUtils.normalize( node.'@jar'.toString() )
+        String relPath = jar.substringAfterLast( '/m2repo/' )
+        String nestedJarPath = node.'@nestedJarPath'
+        String key = "${node.'@code'} ${relPath} ${nestedJarPath}"
+        
+        String message = "Found multiple nested JARs in ${jar}"
+        
+        def result = new MultipleNestedJarsProblem( jar: jar, nestedJarPath: nestedJarPath, key: key, relPath : relPath, message: message )
+        
+        return result
+    }
+}
+
+class MissingManifest extends CommandProblem {
+    
+    String jar
+    String nestedJarPath
+    String relPath
+    
+    static MissingManifest create( node ) {
+        
+        String jar = PathUtils.normalize( node.'@jar'.toString() )
+        jar = jar.removeEnd( '/META-INF/MANIFEST.MF' )
+        
+        String key = "${node.'@code'} ${jar.substringAfterLast( '/' )}"
+        String message = "Couldn't find MANIFEST.MF in ${jar}"
+
+        def result = new MissingManifest( jar: jar, key: key, message: message )
+        
+        return result
+    }
+}
+
+class ImportError extends CommandProblem {
+    
+    static ImportError create( node ) {
+        return new ImportError()
+    }
+}
+
 class ProblemVersionRange extends Problem {
     
     Dependency dependency
@@ -928,6 +1104,8 @@
 }
 
 enum ProblemType {
+    ImportError( 'Plug-ins Which Couldn\'t be Imported' ),
+    MissingManifest( 'Plug-ins Without Manifest' ),
     Problem( 'Generic Problems', null),
     ProblemSameKeyDifferentVersion( 'POMs with same ID but different version', null),
     DependencyWithoutVersion( 'Problems With Dependencies', null),
@@ -936,16 +1114,13 @@
     ProblemVersionRange( 'Dependencies With Version Ranges', 'Dependencies should not use version ranges.' ),
     ProblemSnaphotVersion( 'Snapshot Versions', 'Release Repositories should not contain SNAPSHOTs' ),
     PathProblem( 'Path Problems', 'These POMs are not where they should be' ),
-    MissingSources( 'Missing Sources', null )
+    MultipleNestedJarsProblem( 'Multiple Nested JARs' ),
+    MissingSources( 'Missing Sources' )
     
     final String title
     final String description
     
-    private ProblemType( String title ) {
-        this( title, null )
-    }
-    
-    private ProblemType( String title, String description ) {
+    private ProblemType( String title, String description = null ) {
         this.title = title
         this.description = description
     }
diff --git a/src/main/groovy/m4e/Error.java b/src/main/groovy/m4e/Error.java
index 05f4085..d023e36 100644
--- a/src/main/groovy/m4e/Error.java
+++ b/src/main/groovy/m4e/Error.java
@@ -11,22 +11,43 @@
 
 package m4e;
 
+import java.util.HashMap;
+import java.util.Map;
+
 public enum Error {
     TWO_VERSIONS( 1 ),
     MAVEN_FAILED( 2 ),
     MISSING_MANIFEST( 3 ),
     IMPORT_ERROR( 4 );
-    
-    private final int id;
+
+    private final static Map<String, Error> map = new HashMap<String, Error>();
+    static {
+        for( Error e : Error.values() ) {
+            Error old = map.put( e.code(), e );
+            if( null != old ) {
+                throw new IllegalStateException( "Duplicate codes: " + e + " and " + old );
+            }
+        }
+    }
+
+    private final String code;
     
     private Error( int id ) {
-        this.id = id;
+        this.code = String.format( "E%04d", id );
     }
     
     public String code() {
-        return String.format( "E%04d", id );
+        return code;
     }
     
+    public static Error fromCode( String code ) {
+        Error result = map.get( code );
+        if( null == result ) {
+            throw new IllegalArgumentException( "Undefined Error " + code );
+        }
+        return result;
+    }
+
     public String url() {
         return Warning.BASE_URL + code();
     }
diff --git a/src/main/groovy/m4e/Warning.java b/src/main/groovy/m4e/Warning.java
index 80009d5..8edc0e4 100644
--- a/src/main/groovy/m4e/Warning.java
+++ b/src/main/groovy/m4e/Warning.java
@@ -11,6 +11,9 @@
 
 package m4e;
 
+import java.util.HashMap;
+import java.util.Map;
+
 public enum Warning {
     MISSING_BINARY_BUNDLE_FOR_SOURCES( 1 ),
     UNEXPECTED_FILE_IN_SOURCE_BUNDLE( 2 ),
@@ -20,14 +23,32 @@
     
     public final static String BASE_URL = "http://wiki.eclipse.org/MT4E_";
     
-    private final int id;
+    private final static Map<String, Warning> map = new HashMap<String, Warning>();
+    static {
+        for( Warning e : Warning.values() ) {
+            Warning old = map.put( e.code(), e );
+            if( null != old ) {
+                throw new IllegalStateException( "Duplicate codes: " + e + " and " + old );
+            }
+        }
+    }
+    
+    private final String code;
     
     private Warning( int id ) {
-        this.id = id;
+        this.code = String.format( "W%04d", id );
     }
     
     public String code() {
-        return String.format( "W%04d", id );
+        return code;
+    }
+    
+    public static Warning fromCode( String code ) {
+        Warning result = map.get( code );
+        if( null == result ) {
+            throw new IllegalArgumentException( "Undefined warning " + code );
+        }
+        return result;
     }
     
     public String url() {