JCRVLT-649 improve logging of errors in ActivityLog and ProgressTracker (#242)

diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
index fd67aa4..f9c05dd 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
@@ -194,7 +194,7 @@
                 try {
                     session.save();
                 } catch (RepositoryException e) {
-                    log.error("Error during auto save: {} - retrying after refresh...", e.getMessage());
+                    log.error("Error during auto save, retrying after refresh: {}", Importer.getExtendedThrowableMessage(e));
                     session.refresh(true);
                     session.save();
                 }
@@ -202,7 +202,7 @@
             }
         } catch (RepositoryException e) {
             if (isPotentiallyTransientException(e) && isIntermediate) {
-                log.warn("Could not auto-save even after refresh due to potentially transient exception: {}", e.getMessage());
+                log.warn("Could not auto-save even after refresh due to potentially transient exception: {}", Importer.getExtendedThrowableMessage(e));
                 log.debug("Auto save exception", e);
                 return false;
             } else {
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java
index 3925091..1d066e2 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java
@@ -24,6 +24,7 @@
 import java.io.OutputStream;
 import java.io.Reader;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -45,6 +46,7 @@
 import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping;
 import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
 import org.apache.jackrabbit.spi.commons.namespace.SessionNamespaceResolver;
+import org.apache.jackrabbit.util.Text;
 import org.apache.jackrabbit.vault.fs.api.Artifact;
 import org.apache.jackrabbit.vault.fs.api.ArtifactType;
 import org.apache.jackrabbit.vault.fs.api.ImportInfo;
@@ -83,7 +85,6 @@
 import org.apache.jackrabbit.vault.packaging.registry.impl.JcrPackageRegistry;
 import org.apache.jackrabbit.vault.util.Constants;
 import org.apache.jackrabbit.vault.util.PlatformNameFormat;
-import org.apache.jackrabbit.util.Text;
 import org.apache.jackrabbit.vault.util.Tree;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -477,8 +478,8 @@
                     log.error("Error while committing changes. Aborting.");
                     throw e;
                 } else {
-                    log.warn("Error while committing changes. Retrying import from checkpoint at {}. Retries {}/10",
-                            cpTxInfo == null ? "/" : cpTxInfo.path, recoveryRetryCounter);
+                    log.warn("Error while committing changes: Retrying import from checkpoint at {}. Retries {}/10. {}",
+                            cpTxInfo == null ? "/" : cpTxInfo.path, recoveryRetryCounter, getExtendedThrowableMessage(e));
                     autoSave = cpAutosave.copy();
                     // build skip list
                     skipList.clear();
@@ -508,6 +509,7 @@
             if (hasErrors) {
                 track("Package import simulation finished. (with errors, check logs!)", "");
                 log.error("There were errors during package install simulation. Please check the logs for details.");
+                track("First error was " + getExtendedThrowableMessage(firstException), "");
             } else {
                 track("Package import simulation finished.", "");
             }
@@ -518,12 +520,46 @@
                     throw new RepositoryException("Some errors occurred while installing packages. Please check the logs for details. First exception is logged as cause.", firstException);
                 }
                 log.error("There were errors during package install. Please check the logs for details.");
+                track("First error was " + getExtendedThrowableMessage(firstException), "");
             } else {
                 track("Package imported.", "");
             }
         }
     }
 
+    /**
+     * Returns a human-readable error message from the throwable including all its causes till the root.
+     * Also the throwable class names are included in the message
+     * @param throwable from which to construct an error message
+     * @return the enhanced error message derived from the throwable
+     */
+    static String getExtendedThrowableMessage(Throwable throwable) {
+        StringBuilder messageBuilder = new StringBuilder();
+        if (throwable == null) {
+            return "";
+        }
+        messageBuilder.append(throwable.getClass().getName()).append(": ");
+        messageBuilder.append(throwable.getMessage());
+        Throwable cause = throwable.getCause();
+        while (cause != null) {
+            if (!isDelimiter(messageBuilder.charAt(messageBuilder.length()-1))) {
+                messageBuilder.append(".");
+            }
+            messageBuilder.append(" Caused by ")
+            .append(cause.getClass().getName())
+            .append(": ")
+            .append(cause.getMessage());
+            cause = cause.getCause();
+        }
+        return messageBuilder.toString();
+    }
+
+    /** all punctuation delimiters between sentences in English */
+    private static final List<Character> DELIMITERS = Arrays.asList('.', '?', '!');
+    static boolean isDelimiter(char character) {
+        return DELIMITERS.contains(character);
+    }
+
     private TxInfo postFilter(TxInfo root) {
         TxInfo modifierRoot = root;
         if (filter.contains(modifierRoot.path)){
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/io/ImporterTest.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/io/ImporterTest.java
new file mode 100644
index 0000000..4a05597
--- /dev/null
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/io/ImporterTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.jackrabbit.vault.fs.io;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class ImporterTest {
+
+    @Test
+    public void testGetExtendedThrowableMessage() {
+        Throwable testThrowable = 
+            new IOException("my exception", 
+                new IllegalArgumentException("intermediate cause!", 
+                    new IllegalStateException("root cause")));
+        String expectedMessage = "java.io.IOException: my exception. " + 
+                    "Caused by java.lang.IllegalArgumentException: intermediate cause! " +
+                    "Caused by java.lang.IllegalStateException: root cause";
+        assertEquals(expectedMessage, Importer.getExtendedThrowableMessage(testThrowable));
+        assertEquals("", Importer.getExtendedThrowableMessage(null));
+    }
+}