TIBCOSoftware / jasperreports

JasperReports® - Free Java Reporting Library
https://community.jaspersoft.com/downloads/community-edition/
GNU Lesser General Public License v3.0
1.02k stars 394 forks source link

Module path problem, package net.sf.jasperreports.fonts exists in two separate modules #463

Closed bjorndarri closed 1 week ago

bjorndarri commented 1 month ago

Hi,

I'm using JasperReports 7.0.0 in a modular application, built with jlink so I'm using the module system as much as possible.

The problem I'm running into is the net.sf.jasperreports.fonts package, which exists in both the jasperreports-core and jasperreports-fonts jars.

When running the application you get something along the lines of:

java.lang.LayerInstantiationException: Package net.sf.jasperreports.fonts in both module X and module jasperreports.fonts

The package contains only resources in the core jar, no classes, and exists only in the resources folder.

After having looked around the project a bit I found the package net.sf.jasperreports.engine.fonts, which seems like it should be the "correct" location for these default font resources, moving the resources there would solve this issue.

teodord commented 1 month ago

Can you provide us a minimal setup to reproduce this issue? Since we did not declare modules in our own JARs, it would be useful to see the command you use when you get the above error. Maybe you can skip code from your app and provide an example with just our own JARs.

Thank you, Teodor

bjorndarri commented 1 month ago

Sure, I have a reproducer project, just have to tidy it up a bit, give me a couple of days.

teodord commented 1 month ago

The project would be useful to asses the situation with all artifacts. The issue you mentioned about fonts is probably an easy one that can be fixed, but there are other package duplications among our JARs which might not be as easy.

Thank you, Teodor

bjorndarri commented 1 month ago

Here you go, reduced to its absolute minimum:

https://github.com/bjorndarri/jasper-jlink-test

Let me know if you want me to explain anything.

teodord commented 1 month ago

Thank you for providing this module testing tool. I was able to run it, but unfortunately, I was not able to extend its reach to investigate additional JARs from our project, as you have limited module use to core and fonts in your minimal setup.

The problem is that package duplication is know to exist in a handful of other cases across the multitude of JARs that now make up the JasperReports Library functionality. For example, jasperreports-castor, jasperreports-sort and especially jasperreports-data-adapters come with package duplication against the core JAR or against each other. The good news about this is that the Castor optional extension is deprecated and you probably don't need it anyway. Same for the jasperreports-sort.jar.

Things are a little more complicated between core JAR and jasperreports-data-adapters jar, which share a net.sf.jasperreports.data package. We are going to investigate feasibility of renaming some of these packages, but can't say when or if this would be fixed soon.

Thank you, Teodor

bjorndarri commented 1 month ago

You can fork the repository and modify it to test other modules.

I created an example branch jasperreports-pdf where I have switched out the fonts module with the pdf module, which works fine, since there are no split packages between core and pdf.

You can create other branches to test other modules or to test module combinations.

You do have to fiddle a bit with the module descriptors, to get things working properly, but the heavy lifting is being done by the extra-java-module-info plugin, which is an absolute life-saver, by the way.

I'm more than willing to help out, I dream of a future with a fully modularized JasperReports library :smile:

teodord commented 1 month ago

My attempt was to add more JRL JARs to your project and not replace the fonts one with another, just one JAR at a time. This is because package duplication can exist between optional JARs and not only against core. When adding the jasperreports-data-adapters JAR, I started to have many errors, mostly because I'm not very familiar with modules in general. Ideally, using your project, we should be able to see all package duplications across all JRL JARs together. I have identified package duplication by inspecting our source code and just wanted to confirm them using your project, but had to give up due to my limited knowledge in building using modules.

Thank you, Teodor

teodord commented 1 month ago

Maybe I'm missing something, but my understanding is that JasperReports Library artifacts cannot be modularized as long as they depend on non-modularized JARs such as Apache Batik. Having said that I'm not sure how you can eventually use them with jlink since automatic modules cannot be used with jlink.

Thank you, Teodor

bjorndarri commented 1 month ago

That's where the badass-jlink-plugin and extra-java-module-info Gradle plugins come into play. For maven there is the moditect plugin, but I haven't looked into exactly what it can do.

plugins {
    id "application"
    id "org.beryx.jlink" version "3.0.1"
    id "org.gradlex.extra-java-module-info" version "1.8"
}

The extra-java-module-info plugin provides a way to transform non-modular dependencies into either automatic modules or full-fledged modules (with a bit more work and trial & error).

Here I transform the jasperreports-core and jasperreports-fonts jars and into modules named jasperreports and jasperreports-fonts as well as the commons-beanutils and commons-collections jars into automatic modules.

extraJavaModuleInfo {
    module("net.sf.jasperreports:jasperreports", "jasperreports") {
        exportAllPackages()
        requires("java.sql")
        requires("java.desktop")
        requires("org.apache.commons.logging")
        requires("org.apache.commons.collections4")
    }

    module("net.sf.jasperreports:jasperreports-fonts", "jasperreports.fonts") {
        exportAllPackages()
    }

    automaticModule("commons-beanutils:commons-beanutils", "commons.beanutils")
    automaticModule("commons-collections:commons-collections", "commons.collections")
}

This means that I can now refer to these modules in my module-info.java file:

module is.test.jasper {
  requires jasperreports;
  requires jasperreports.fonts;
}

When creating the jlink image the badass-jlink-plugin starts by combining all non-modular and automatic-module jars into a single full-fledged module. That means that all dependencies are now either full modules via extra-java-module-info or become part of the combined module and voila, now jlink has all the info it needs to create an image.

The reason I exclude all the batik and xml-apis dependencies is because otherwise I would have to add automaticModule clauses for all of those jars in the extraJavaModuleInfo section, but since my reports don't rely on those, I can safely exclude them.

dependencies {
    implementation("net.sf.jasperreports:jasperreports:7.0.0") {
        exclude group: "org.apache.xmlgraphics"
        exclude group: "xml-apis"
    }
    implementation("net.sf.jasperreports:jasperreports-fonts:7.0.0")
}

I'm skipping over a few details here, but this is basically how I've been using the JasperReports library in my Swing applications for a while now.

Now, regarding your question as to whether you can modularize JasperReports while depending on non-modular jars, the answer is definitely yes, the users of your library have to do a bit of work handling those non-modular dependencies, when creating jlink images (see above), but that's a fair trade for small runtime images.

I've actually developed a fully modularized Swing/CRUD framework, with a JasperReports plugin module depending on the JasperReports library, here's the build file jasperreports-plugin build file. I'm relying on the extra-java-module-info plugin to do most of the heavy lifting for me.

Now that I have introduced all the required concepts, here's my workaround for the problem this issue is about:

Transform the jasperreports-core jar into an automatic module:

extraJavaModuleInfo {
    automaticModule("net.sf.jasperreports:jasperreports", "jasperreports")
}

which means it becomes part of the combined (so-called merged) module, when creating the jlink image.

Then delete the offending split package after the merged module contents have been prepared:

jlink {
  prepareMergedJarsDir {
        doLast {
            project.delete(files("${mergedJarsDir}/net/sf/jasperreports/fonts"))
        }
    }
}

Now only the jasperreports-fonts module contains the net/sf/jasperreports/fonts package.

teodord commented 1 month ago

OK, I understand. You are modularizing the non-modular third party dependencies that you need.

Now, back to JasperReports JAR files. I think for the time being, we cannot modularize them using module-info.java declarations because a public library like ours cannot depend on automatic modules derived from JAR file names.

The only thing we can do for now is to declare automatic module names in Manifest file, making the first step towards future modularization, when that will be possible.

The last paragraph in this article explains exactly the case of JasperReports library: https://dev.java/learn/modules/automatic-module/

Thank you, Teodor

bjorndarri commented 1 month ago

Absolutely, module names derived from jar files are not usable.

Automatic module names in the manifest file would be a great first step, and fixing split packages.

I did take a stab at fixing split packages in three of the jasperreports modules you mentioned earlier (jasperreports-data-adapters, jasperreports-data and jasperreports-sort), and managed to fix these pretty easily, you can take a look at the commits if you are curious (I was just experimenting so I'm not planning a pull request or anything).

https://github.com/bjorndarri/jasperreports

branch split-packages.

There are other jasperreports modules which turned out to be more difficult and require more knowledge about the codebase so I left it at that.

teodord commented 1 month ago

I have pushed here on master branch a change that adds automatic module names to the manifest files and fixes the split packages. It is a first attempt at fixing the split packages because there are still some of them left. I did not fix those remaining because they only contain resources and no Java classes and hence building a meta module with all of them did not raise any error for them. For example, as you indicated, the split net.sf.jasperreports.fonts package is only about resource files and has no Java class in it. In my modules tests, such resources packages never pose a problem, so this is why I did not touch them yet. It is not clear to me if packages containing only resource files pose a real split package problem or not. In my tests, they did not. So maybe you can check your test project and see if indeed the way you modify the JAR to make them modularized is not somehow doing more than needed with respect to split package validation.

I published new master snapshots for all our artifacts at https://jaspersoft.jfrog.io/artifactory/jr-ce-snapshots/.

Thank you, Teodor

bjorndarri commented 1 month ago

I have experimented more and it sure looks like split packages containing only resources in automatic modules do not trigger a failure.

I'm no module expert and I can certainly confuse myself when playing with these things, but I'm pretty sure a split package containing only resources will pose the same problem as other packages, when used in a fully modular context.

It's quite tempting to try to find an actual module expert to ask :smile:.

teodord commented 1 month ago

It would not be very difficult to eliminate the split packages containing resources as well, but I first wanted to know if they do pose a problem or not. And usually I'm trying to do minimum changes/disruption, if possible. I searched the internet, but could not find a revealing answer to this dilemma. Maybe you have better luck.

Thanks, Teodor

bjorndarri commented 2 weeks ago

Hey,

While reading through the jigsaw-dev mailing list for an unrelated matter, I ran into this quote from Alan Bateman (at Oracle):

Two modules with resources in the same "package" is the same as split 
package issue.

https://mail.openjdk.org/pipermail/jigsaw-dev/2021-June/014677.html

I think this mailing list would be the correct place to ask the question, if you want a definitive answer :-).

teodord commented 1 week ago

Thanks for providing this link. At this point it is enough for me and I think it makes sense to avoid split packages in any case, so I decided to move files around and fix the ones having resources only.

Closing this as fixed: https://github.com/TIBCOSoftware/jasperreports/commit/d736357b5d6d08294e7604ad511f68a9798689bf

Thank you, Teodor

bjorndarri commented 1 week ago

Excellent, just tested this version and can confirm that I can create a jlink image without the delete hack mentioned here.

Thanks!