Search code examples
scalasbtdroolsexecutable-jarkie

Run Drools Kie project from fat jar


I am trying to run a drools based (and KieServices based) project from a fat jar that has been generated using sbt assembly.

[main] INFO org.drools.compiler.kie.builder.impl.ClasspathKieProject - Found kmodule: jar:file:/.../myJar.jar!/META-INF/kmodule.xml
[main] ERROR org.drools.compiler.kie.builder.impl.ClasspathKieProject - Unable to build index of kmodule.xml url=jar:file:/.../myJar.jar/META-INF/kmodule.xml
You're trying to perform a xml related operation without the necessary xml support for drools. Please add the module org.drools:drools-xml-support to your classpath.
[main] ERROR org.drools.compiler.kie.builder.impl.KieContainerImpl - Unknown KieSession name: DroolDummyKS

This error is triggered by the following code, last line to be specific:

val kieServices: KieServices = KieServices.Factory.get
val kieContainer: KieContainer = kieServices.getKieClasspathContainer

// Apply the correct kie session from the ./resources/META-INF/kmodule.xml configuration
val kieSession: KieSession = kieContainer.newKieSession("DroolDummyKS")

Project is a Scala SBT project (Java 11 as compiler)


Solution

  • I couldn't reproduce Unable to build index of kmodule.xml. Just kieContainer.newKieSession("DroolDummyKS") returns null for assembly jar (java -jar myJar.jar), on contrary to sbt run, where it returns KieSession[0].

    Notice that it's written in your error:

    Please add the module org.drools:drools-xml-support to your classpath

    Do two things in build.sbt:

    • add to libraryDependencies

      "org.drools" % "drools-xml-support" % "8.31.1.Final" 
      

    and

    • unignore kmodule.xml in assembly strategy (so that kmodule.xml is included into assembly jar), for example with singleOrError (not sure that concatenation makes sense for xml on contrary to service files, we're making sure that it's your kmodule.xml being included, otherwise it throws)

      assembly / assemblyMergeStrategy := {
        case PathList("META-INF", "services", xs@_*) => MergeStrategy.concat
        case PathList("META-INF", "kmodule.xml") => MergeStrategy.singleOrError
        case PathList("META-INF", xs@_*) => MergeStrategy.discard
        case _ => MergeStrategy.first
      }
      

    Update. With

    assembly / assemblyMergeStrategy := {
      case _ => MergeStrategy.singleOrError
    }
    

    you can check what files have duplicates.

    You should add the dependency "org.drools" % "drools-xml-support" % "8.31.1.Final" as I adviced earlier. Otherwise there is NPE even for sbt run. I see that you added it in update.

    You should remove the file src/main/resources/META-INF/services/org.kie.api.KieServices. Anyway it's present in the dependency drools-compiler-8.31.1.Final.jar.

    Try the strategy ignoring as less as possible

    assembly / assemblyMergeStrategy := {
      case xs if xs.endsWith("LICENSE") => MergeStrategy.discard
      case xs if xs.endsWith("LICENSE.txt") => MergeStrategy.discard
      case xs if xs.endsWith("INDEX.LIST") => MergeStrategy.discard
      case xs if xs.endsWith("MANIFEST.MF") => MergeStrategy.discard
      case xs if xs.endsWith("NOTICE") => MergeStrategy.discard
      case xs if xs.endsWith("NOTICE.txt") => MergeStrategy.discard
      case xs if xs.endsWith("module-info.class") => MergeStrategy.discard
      case PathList("META-INF", "services", "org.apache.poi.sl.draw.ImageRenderer") => MergeStrategy.filterDistinctLines
      case PathList("META-INF", "services", "org.apache.poi.ss.usermodel.WorkbookProvider") => MergeStrategy.filterDistinctLines
      case PathList("META-INF", "services", "org.apache.poi.extractor.ExtractorProvider") => MergeStrategy.filterDistinctLines
      case PathList("META-INF", "services", "org.drools.wiring.api.ComponentsSupplier") => MergeStrategy.filterDistinctLines
      case _ => MergeStrategy.singleOrError
    }
    

    Duplicates should be resolved then.

    filterDistinctLines is similar to concat, just not adding the same lines.

    The strategy can be simplified

    assembly / assemblyMergeStrategy := {
      case xs if Seq(
        "LICENSE",
        "LICENSE.txt",
        "INDEX.LIST",
        "MANIFEST.MF",
        "NOTICE",
        "NOTICE.txt",
        "module-info.class"
      ).exists(xs.endsWith) => MergeStrategy.discard
      case PathList("META-INF", "services", xs@_*) => MergeStrategy.filterDistinctLines
      case _ => MergeStrategy.singleOrError
    }
    

    Update 2. I looked for reasons of the latest NPE

    Exception in thread "main" java.lang.NullPointerException: 
    Cannot invoke 
    "org.drools.compiler.compiler.Dialect.getId()"
    because the return value of
    "org.drools.compiler.rule.builder.RuleBuildContext.getDialect()"
    is null
    

    It turns out that the thing was in the file META-INF/kie.default.properties.conf of one of dependencies. So it was enough to additionally unignore it

    assembly / assemblyMergeStrategy := {
      case x if x.endsWith("module-info.class") => MergeStrategy.discard
      case PathList("META-INF", "services", xs@_*) => MergeStrategy.concat
      case PathList("META-INF", "kmodule.xml") => MergeStrategy.singleOrError
      case PathList("META-INF", "kie.default.properties.conf") => MergeStrategy.singleOrError
      case PathList("META-INF", xs@_*) => MergeStrategy.discard
      case _ => MergeStrategy.first
    }
    

    But the main conclusion for us should be that ignoring the whole META-INF can be dangerous. There can be some other files there that are important for some of dependencies. Maybe now there is no NPE but some issues can be later.

    It turns out that the default assembly strategy

    assembly / assemblyMergeStrategy := MergeStrategy.defaultMergeStrategy
    

    or

    assembly / assemblyMergeStrategy := {
      case x =>
        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
        oldStrategy(x)
    }
    

    or (see here)

    val defaultMergeStrategy: String => MergeStrategy = {
      case x if Assembly.isConfigFile(x) =>
        MergeStrategy.concat
      case PathList(ps @ _*) if Assembly.isReadme(ps.last) || Assembly.isLicenseFile(ps.last) =>
        MergeStrategy.rename
      case PathList("META-INF", xs @ _*) =>
        (xs map {_.toLowerCase}) match {
          case ("manifest.mf" :: Nil) | ("index.list" :: Nil) | ("dependencies" :: Nil) =>
            MergeStrategy.discard
          case ps @ (x :: xs) if ps.last.endsWith(".sf") || ps.last.endsWith(".dsa") =>
            MergeStrategy.discard
          case "plexus" :: xs =>
            MergeStrategy.discard
          case "services" :: xs =>
            MergeStrategy.filterDistinctLines
          case ("spring.schemas" :: Nil) | ("spring.handlers" :: Nil) =>
            MergeStrategy.filterDistinctLines
          case _ => MergeStrategy.deduplicate
        }
      case _ => MergeStrategy.deduplicate
    }
    

    does the work pretty well. You just had to ignore additionally files module-info.class. So you can prefer

    assembly / assemblyMergeStrategy := {
      case x if x.endsWith("module-info.class") => MergeStrategy.discard
      case x =>
        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
        oldStrategy(x)
    }
    

    https://github.com/ThijmenL98/DroolsMCVE/pull/1