Add new tunable (errorOnUnsupportedJavaVersion) to allow running with older versions of Java

Currently we intentionally throw an exception if Daffodil is not run
with Java 8 due to bugs in how Java 7 handles decoding errors. However,
there are some cases where a user has no choice except to run under Java
7.

This patch adds a new tunable "errorOnUnsupportedJavaVersion" that causes
Java 8 check failures to be logged as a warning rather than thrown as an
exception. This allows the above use case to be possible, with the explicit
understanding that it is not fully tested and is know to have unexpected
behavior in some circumstances.

Additionally, the file "/daffodil-config.xml" can be put on the
classpath to override tunables globally, regardless of how Daffodil is
run (e.g. CLI, TDMLRunner, API, etc.). This allows one to easily run all
tests and processing with the errorOnUnsupportedJavaVersion check
disable, or other tunables modified, if desired.

DAFFODIL-1921
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/compiler/Compiler.scala b/daffodil-core/src/main/scala/org/apache/daffodil/compiler/Compiler.scala
index 9f39bbc..bc018ba 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/compiler/Compiler.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/compiler/Compiler.scala
@@ -39,6 +39,7 @@
 import org.apache.daffodil.externalvars.ExternalVariablesLoader
 import org.apache.daffodil.processors.SchemaSetRuntimeData
 import org.apache.daffodil.util.CheckJavaVersion
+import org.apache.daffodil.util.InvalidJavaVersionException
 import org.apache.daffodil.api.ValidationMode
 import org.apache.daffodil.processors.VariableMap
 import java.util.zip.GZIPInputStream
@@ -162,7 +163,14 @@
         rootERD,
         variables,
         validationMode)
-      CheckJavaVersion.checkJavaVersion(ssrd)
+      val versionErrorOpt = CheckJavaVersion.checkJavaVersion()
+      if (versionErrorOpt.isDefined) {
+        if (tunable.errorOnUnsupportedJavaVersion) {
+          throw new InvalidJavaVersionException(versionErrorOpt.get)
+        } else {
+          log(LogLevel.Warning, versionErrorOpt.get + " " + CheckJavaVersion.allowUnsupportedJavaMessage)
+        }
+      }
       val dataProc = new DataProcessor(ssrd)
       if (dataProc.isError) {
         // NO longer printing anything here. Callers must do this.
@@ -284,7 +292,14 @@
       val dpObj = objInput.readObject()
       objInput.close()
       val dp = dpObj.asInstanceOf[SerializableDataProcessor]
-      CheckJavaVersion.checkJavaVersion(dp.ssrd)
+      val versionErrorOpt = CheckJavaVersion.checkJavaVersion()
+      if (versionErrorOpt.isDefined) {
+        if (dp.getTunables.errorOnUnsupportedJavaVersion) {
+          throw new InvalidJavaVersionException(versionErrorOpt.get)
+        } else {
+          log(LogLevel.Warning, versionErrorOpt.get + " " + CheckJavaVersion.allowUnsupportedJavaMessage)
+        }
+      }
       dp
     } catch {
       case ex: ZipException => {
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/util/CheckJavaVersion.scala b/daffodil-core/src/main/scala/org/apache/daffodil/util/CheckJavaVersion.scala
index 0a93da1..1a8f1d6 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/util/CheckJavaVersion.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/util/CheckJavaVersion.scala
@@ -17,24 +17,27 @@
 
 package org.apache.daffodil.util
 
-import org.apache.daffodil.exceptions.ThrowsSDE
 import org.apache.daffodil.processors.charset.CharsetUtils
 
 class InvalidJavaVersionException(msg: String, cause: Throwable = null) extends Exception(msg, cause)
 
 object CheckJavaVersion {
 
-  def checkJavaVersion(context: ThrowsSDE) = {
+  def checkJavaVersion(): Option[String] = {
     val jVersion = scala.util.Properties.javaVersion
-    if (!scala.util.Properties.isJavaAtLeast("1.8")) {
-      throw new InvalidJavaVersionException("Daffodil requires Java 8 (1.8) or higher. You are currently running %s".format(jVersion))
-    }
-    //
-    // Test specifically for this particular decoder bug
-    // 
-    if (CharsetUtils.hasJava7DecoderBug) {
-      throw new InvalidJavaVersionException("This Java JVM has the Java 7 Decoder Bug. Daffodil requires Java 8 or higher.")
-    }
+    val errorStringOpt =
+      if (!scala.util.Properties.isJavaAtLeast("1.8")) {
+        Some("Daffodil requires Java 8 (1.8) or higher. You are currently running %s.".format(jVersion))
+      } else if (CharsetUtils.hasJava7DecoderBug) {
+        Some("This Java JVM has the Java 7 Decoder Bug. Daffodil requires Java 8 or higher.")
+      } else {
+        None
+      }
+    errorStringOpt
   }
 
+  val allowUnsupportedJavaMessage =
+    "Due to the tunable value of errorOnUnsupportedJavaVersion, " +
+    "processing will continue with the understanding that this is not " +
+    "fully tested and may have unexpected behavior in some circumstances."
 }
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/api/DaffodilTunables.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/api/DaffodilTunables.scala
index c4850b0..27b090f 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/api/DaffodilTunables.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/api/DaffodilTunables.scala
@@ -21,18 +21,41 @@
 import org.apache.daffodil.schema.annotation.props.gen.ParseUnparsePolicy
 import org.apache.daffodil.util.LogLevel
 import org.apache.daffodil.util.Logging
+import org.apache.daffodil.util.Misc
+import org.apache.daffodil.xml.DaffodilXMLLoader
 
 object DaffodilTunables {
 
   def apply(tunables: Map[String, String]): DaffodilTunables = {
-    new DaffodilTunables().setTunables(tunables)
+    apply().setTunables(tunables)
   }
 
   def apply(tunable: String, value: String): DaffodilTunables = {
-    new DaffodilTunables().setTunable(tunable, value)
+    apply().setTunable(tunable, value)
   }
 
-  def apply(): DaffodilTunables = new DaffodilTunables()
+  def apply(): DaffodilTunables = {
+    // override tunables from the global configuration file on the class path, if it exists
+    val (configOpt, _) = Misc.getResourceOption("/daffodil-config.xml")
+    val configTunables: Map[String, String] =
+      if (configOpt.isDefined) {
+        val loader = new DaffodilXMLLoader()
+        val node = loader.load(new URISchemaSource(configOpt.get))
+        val trimmed = scala.xml.Utility.trim(node)
+        val tunablesNode = (trimmed \ "tunables").headOption
+        val tunablesMap: Map[String, String] = tunablesNode match {
+          case None => Map.empty
+          case Some(tunableNode) => {
+            tunableNode.child.map { n => (n.label, n.text) }.toMap
+          }
+        }
+        tunablesMap
+      } else {
+        Map.empty
+      }
+
+    new DaffodilTunables().setTunables(configTunables)
+  }
 }
 
 case class DaffodilTunables(
@@ -105,7 +128,19 @@
   // This may cause a degredation of performance in path expression evaluation,
   // so this should be avoided when in production. This flag is automatically
   // enabled when debugging is enabled.
-  val allowExternalPathExpressions: Boolean = false)
+  val allowExternalPathExpressions: Boolean = false,
+
+  // A bug exists in Java 7 that causes unexpected behavior when decode errors
+  // occur in the specific ways that Daffodil decodes data. For this reason,
+  // Daffodil throws an exception when it detects that Daffodil is not running
+  // under Java 8 or has this decoder bug. However, there are some cases where
+  // a user has no choice but to run on Java 7. Setting this tunable to false
+  // will cause Daffodil to log a warning rather than throw an exception so
+  // that a user can run Daffodil on unsupported Java versions, with the
+  // understanding that it is not fully tested and behavior may not be well
+  // defined. This boolean is experimental and should only be used by those
+  // that fully understand the risks.
+  val errorOnUnsupportedJavaVersion: Boolean = true)
   extends Serializable
   with Logging
   with DataStreamLimits {
@@ -206,6 +241,7 @@
         this.copy(suppressSchemaDefinitionWarnings = warningsList)
       }
       case "allowexternalpathexpressions" => this.copy(allowExternalPathExpressions = java.lang.Boolean.valueOf(value))
+      case "erroronunsupportedjavaversion" => this.copy(errorOnUnsupportedJavaVersion = java.lang.Boolean.valueOf(value))
       case _ => {
         log(LogLevel.Warning, "Ignoring unknown tunable: %s", tunable)
         this