From eb89d88c85343447b5a40ca9bb22373e10e03aa8 Mon Sep 17 00:00:00 2001 From: melloware Date: Tue, 8 Oct 2024 15:55:39 -0400 Subject: [PATCH] Native work in progress --- .../deployment/JasperReportsProcessor.java | 130 ++++++++++++++++-- .../deployment/XalanProcessor.java | 8 +- integration-tests/pom.xml | 50 +++---- .../it/JasperReportsResource.java | 20 +-- .../it/JasperReportsResourceIT.java | 8 ++ .../XalanTransformerFactory.java | 5 +- .../graal/JRClassLoaderSubstitution.java | 34 +++++ .../graal/TemplatesImplSubstitution.java | 8 ++ 8 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 integration-tests/src/test/java/io/quarkiverse/jasperreports/it/JasperReportsResourceIT.java create mode 100644 runtime/src/main/java/io/quarkiverse/jasperreports/graal/JRClassLoaderSubstitution.java diff --git a/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/JasperReportsProcessor.java b/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/JasperReportsProcessor.java index 102e039..6db1104 100644 --- a/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/JasperReportsProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/JasperReportsProcessor.java @@ -27,6 +27,7 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; @@ -41,6 +42,12 @@ import net.sf.jasperreports.engine.design.JRReportCompileData; import net.sf.jasperreports.engine.util.JRLoader; +/** + * Processor class for JasperReports integration in Quarkus. + * This class handles various build steps required for JasperReports functionality, + * including feature registration, resource merging, dependency indexing, + * and reflection configuration. + */ class JasperReportsProcessor { private static final String FEATURE = "jasperreports"; @@ -52,11 +59,29 @@ FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } + /** + * Merges the JasperReports extension properties file into the Uber JAR. + * + * This build step ensures that the JasperReports extension properties file + * is included in the final Uber JAR, allowing JasperReports to properly + * load its extensions at runtime. + * + * @return A {@link UberJarMergedResourceBuildItem} representing the merged resource + */ @BuildStep UberJarMergedResourceBuildItem mergeResource() { return new UberJarMergedResourceBuildItem(EXTENSIONS_FILE); } + /** + * Indexes transitive dependencies required for JasperReports functionality. + * + * This method adds various JasperReports modules and related dependencies + * to the build index, ensuring they are properly processed during the build. + * + * @param index The BuildProducer for IndexDependencyBuildItem, used to add + * dependencies to the build index. + */ @BuildStep void indexTransitiveDependencies(BuildProducer index) { index.produce(new IndexDependencyBuildItem("net.sf.jasperreports", "jasperreports")); @@ -70,6 +95,16 @@ void indexTransitiveDependencies(BuildProducer index) index.produce(new IndexDependencyBuildItem("org.apache.xmlgraphics", "batik-gvt")); } + /** + * Registers classes for reflection in the native image. + * + * This method collects various classes used by JasperReports and registers them + * for reflection. This is necessary for proper functioning in a native image context. + * + * @param reflectiveClass The BuildProducer for ReflectiveClassBuildItem, used to register + * classes for reflection. + * @param combinedIndex The CombinedIndexBuildItem containing information about indexed classes. + */ @BuildStep void registerForReflection(BuildProducer reflectiveClass, CombinedIndexBuildItem combinedIndex) { @@ -94,36 +129,41 @@ void registerForReflection(BuildProducer reflectiveCla classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.components.ComponentsExtensionsRegistryFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.data.xmla.XmlaDataAdapterImpl.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.JasperReport.class.getPackageName())); + classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.base.ElementStore.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.design.JRDesignQuery.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.export.DefaultExporterFilterFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.export.ExporterNature.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.fill.JRFillVariable.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.fonts.SimpleFontFace.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.part.DefaultPartComponentsBundle.class.getPackageName())); + classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.type.ColorEnum.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.util.ImageUtil.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.util.xml.JRXPathExecuter.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.engine.xml.ReportLoader.class.getPackageName())); + classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.export.CsvExporterConfiguration.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.extensions.DefaultExtensionsRegistryFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.governors.GovernorExtensionsRegistryFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.jackson.util.JacksonUtil.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.parts.PartComponentsExtensionsRegistryFactory.class.getPackageName())); + classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.pdf.classic.ClassicPdfProducerFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.renderers.AbstractSvgDataToGraphics2DRenderer.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.renderers.util.SvgFontProcessor.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.repo.DefaultRepositoryExtensionsRegistryFactory.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, net.sf.jasperreports.util.JsonLoader.class.getPackageName())); classNames.addAll(collectClassesInPackage(combinedIndex, org.apache.batik.gvt.font.AWTGVTFont.class.getPackageName())); - // basic Java classes found in reports classNames.addAll(collectImplementors(combinedIndex, java.util.Collection.class.getName())); classNames.addAll(collectImplementors(combinedIndex, java.time.temporal.TemporalAccessor.class.getName())); classNames.addAll(collectImplementors(combinedIndex, java.util.Map.Entry.class.getName())); - + classNames.add(byte.class.getName()); + classNames.add(byte[].class.getName()); classNames.add(java.awt.color.ColorSpace.class.getName()); classNames.add(java.awt.Color.class.getName()); classNames.add(java.io.Serializable.class.getName()); classNames.add(java.lang.Boolean.class.getName()); classNames.add(java.lang.Byte.class.getName()); + classNames.add(java.lang.Enum.class.getName()); classNames.add(java.lang.Byte.class.getName()); classNames.add(java.lang.Character.class.getName()); classNames.add(java.lang.Double.class.getName()); @@ -165,6 +205,12 @@ void registerForReflection(BuildProducer reflectiveCla .serialization().build()); } + /** + * Registers classes and packages that need to be initialized at runtime. + * + * @param runtimeInitializedPackages Producer for runtime initialized packages + * @param combinedIndex Combined index of classes in the application + */ @BuildStep void runtimeInitializedClasses(BuildProducer runtimeInitializedPackages, CombinedIndexBuildItem combinedIndex) { @@ -212,9 +258,18 @@ void runtimeInitializedClasses(BuildProducer } + /** + * Registers various resource build items for native image compilation. + * + * @param nativeImageResourcePatterns Producer for native image resource patterns + * @param nativeImageResourceProducer Producer for native image resources + * @param resourceBundleBuildItem Producer for resource bundles + */ @BuildStep - void registerResourceBuildItems(BuildProducer nativeImageResourceProducer, + void registerResourceBuildItems(BuildProducer nativeImageResourcePatterns, + BuildProducer nativeImageResourceProducer, BuildProducer resourceBundleBuildItem) { + // Register individual resource files nativeImageResourceProducer.produce(new NativeImageResourceBuildItem( "jasperreports.properties", EXTENSIONS_FILE, @@ -224,14 +279,31 @@ void registerResourceBuildItems(BuildProducer nati "metadata_messages-defaults.properties", "properties-metadata.json")); + // Register resource bundles resourceBundleBuildItem.produce(new NativeImageResourceBundleBuildItem("jasperreports_messages")); resourceBundleBuildItem.produce(new NativeImageResourceBundleBuildItem("metadata_messages")); resourceBundleBuildItem.produce(new NativeImageResourceBundleBuildItem("metadata_messages-defaults")); + + // Register resource patterns for OOXML export + final NativeImageResourcePatternsBuildItem.Builder builder = NativeImageResourcePatternsBuildItem.builder(); + builder.includeGlob("**/export/ooxml/docx/**"); + builder.includeGlob("**/export/ooxml/pptx/**"); + builder.includeGlob("**/export/ooxml/xlsx/**"); + nativeImageResourcePatterns.produce(builder.build()); } + /** + * Registers report files for native image compilation and processes compiled report files. + * + * @param nativeImageResourcePatterns Producer for native image resource patterns + * @param additionalClasses Producer for additional generated classes + * @param reflectiveClassProducer Producer for reflective classes + * @param outputTarget The output target build item + */ @BuildStep void registerReports(BuildProducer nativeImageResourcePatterns, BuildProducer additionalClasses, + BuildProducer reflectiveClassProducer, OutputTargetBuildItem outputTarget) { final NativeImageResourcePatternsBuildItem.Builder builder = NativeImageResourcePatternsBuildItem.builder(); builder.includeGlob("*." + ReportFileBuildItem.EXT_REPORT); @@ -272,7 +344,11 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (StringUtils.isNotBlank(reportDataClass)) { byte[] bytes = (byte[]) mainData.getCompileData(); Log.infof("Report Data Class: %s Size: %d", reportDataClass, bytes.length); - additionalClasses.produce(new GeneratedClassBuildItem(false, reportDataClass, bytes)); + + reflectiveClassProducer + .produce(ReflectiveClassBuildItem.builder(reportDataClass).constructors().methods().serialization() + .build()); + additionalClasses.produce(new GeneratedClassBuildItem(true, reportDataClass, bytes)); } } catch (JRException e) { @@ -281,17 +357,36 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { }); } - static Path findProjectRoot(Path outputDirectory) { - Path currentPath = outputDirectory.getParent(); - Log.tracef("Current Directory: %s", currentPath); - Path root = Paths.get(DEFAULT_ROOT_PATH); - if (Files.exists(currentPath.resolve(root))) { - return currentPath.resolve(root).normalize(); - } else { - return outputDirectory; + /** + * Registers JasperReports proxy classes for native image compilation. + * + * This method collects classes from the CsvExporterConfiguration and PdfExporterConfiguration + * packages and registers them as proxy definitions for the native image build process. + * + * @param proxyDefinitions The producer for NativeImageProxyDefinitionBuildItem + * @param combinedIndex The combined index of classes for the build + */ + @BuildStep + void registerJasperReportsProxies(BuildProducer proxyDefinitions, + CombinedIndexBuildItem combinedIndex) { + // Register the proxy for exporters + List classes = new ArrayList<>(collectClassesInPackage(combinedIndex, + net.sf.jasperreports.export.CsvExporterConfiguration.class.getPackageName())); + classes.addAll(collectClassesInPackage(combinedIndex, + net.sf.jasperreports.pdf.PdfExporterConfiguration.class.getPackageName())); + for (String proxyClassName : classes) { + proxyDefinitions.produce(new NativeImageProxyDefinitionBuildItem(proxyClassName)); } } + /** + * Registers font resources for inclusion in the native image. + * + * This method adds a glob pattern to include DejaVu fonts in the native image. + * These fonts are commonly used by JasperReports for PDF generation. + * + * @param nativeImageResourcePatterns The producer for NativeImageResourcePatternsBuildItem + */ @BuildStep void registerFonts(BuildProducer nativeImageResourcePatterns) { final NativeImageResourcePatternsBuildItem.Builder builder = NativeImageResourcePatternsBuildItem.builder(); @@ -384,4 +479,15 @@ public List collectImplementors(CombinedIndexBuildItem combinedIndex, St Log.tracef("Implementors: %s", classes); return new ArrayList<>(classes); } + + static Path findProjectRoot(Path outputDirectory) { + Path currentPath = outputDirectory.getParent(); + Log.tracef("Current Directory: %s", currentPath); + Path root = Paths.get(DEFAULT_ROOT_PATH); + if (Files.exists(currentPath.resolve(root))) { + return currentPath.resolve(root).normalize(); + } else { + return outputDirectory; + } + } } \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/XalanProcessor.java b/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/XalanProcessor.java index 4b23302..0c077ce 100644 --- a/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/XalanProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/jasperreports/deployment/XalanProcessor.java @@ -13,7 +13,13 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; -class XalanNativeImageProcessor { +/** + * Xalan native processor from Camel. + * + * @see XalanNativeImageProcessor.java + */ +class XalanProcessor { private static final String TRANSFORMER_FACTORY_SERVICE_FILE_PATH = "META-INF/services/javax.xml.transform.TransformerFactory"; @BuildStep diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 50cf29e..4e3901f 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -57,12 +57,29 @@ build + generate-code + generate-code-tests + native-image-agent - org.apache.maven.plugins + maven-compiler-plugin + + true + + + + maven-surefire-plugin + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + maven-failsafe-plugin @@ -70,44 +87,29 @@ integration-test verify - - - ${project.build.directory}/${project.build.finalName}-runner - - org.jboss.logmanager.LogManager - ${maven.home} - - + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + - native-image + native native - - - - org.apache.maven.plugins - maven-surefire-plugin - - ${native.surefire.skip} - - - - false true - - --trace-object-instantiation=org.openxmlformats.schemas.wordprocessingml.x2006.main.STZoom$Enum - diff --git a/integration-tests/src/main/java/io/quarkiverse/jasperreports/it/JasperReportsResource.java b/integration-tests/src/main/java/io/quarkiverse/jasperreports/it/JasperReportsResource.java index f29c5bb..e179fc0 100644 --- a/integration-tests/src/main/java/io/quarkiverse/jasperreports/it/JasperReportsResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/jasperreports/it/JasperReportsResource.java @@ -163,18 +163,22 @@ public static boolean isRunningInContainer() { @GET @Path("csv") @APIResponse(responseCode = "200", description = "Document downloaded", content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM, schema = @Schema(type = SchemaType.STRING, format = "binary"))) - public Response csv() throws JRException { + public Response csv() { long start = System.currentTimeMillis(); - JasperPrint jasperPrint = fill(); - - JRCsvExporter exporter = new JRCsvExporter(); - - exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - exporter.setExporterOutput(new SimpleWriterExporterOutput(outputStream)); + try { - exporter.exportReport(); + JasperPrint jasperPrint = fill(); + JRCsvExporter exporter = new JRCsvExporter(); + exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); + exporter.setExporterOutput(new SimpleWriterExporterOutput(outputStream)); + exporter.exportReport(); + + } catch (JRException e) { + Log.error("CSV Error", e); + throw new RuntimeException(e); + } Log.infof("CSV creation time : %s", (System.currentTimeMillis() - start)); final Response.ResponseBuilder response = Response.ok(outputStream.toByteArray()); response.header("Content-Disposition", "attachment;filename=jasper.csv"); diff --git a/integration-tests/src/test/java/io/quarkiverse/jasperreports/it/JasperReportsResourceIT.java b/integration-tests/src/test/java/io/quarkiverse/jasperreports/it/JasperReportsResourceIT.java new file mode 100644 index 0000000..aefb648 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/jasperreports/it/JasperReportsResourceIT.java @@ -0,0 +1,8 @@ +package io.quarkiverse.jasperreports.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class JasperReportsResourceIT extends JasperReportsResourceTest { + // Execute the same tests but in packaged mode. +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/jasperreports/XalanTransformerFactory.java b/runtime/src/main/java/io/quarkiverse/jasperreports/XalanTransformerFactory.java index c555d23..89bef5f 100644 --- a/runtime/src/main/java/io/quarkiverse/jasperreports/XalanTransformerFactory.java +++ b/runtime/src/main/java/io/quarkiverse/jasperreports/XalanTransformerFactory.java @@ -19,6 +19,9 @@ /** * A {@link TransformerFactory} delegating to a {@link TransformerFactory} created via * {@code TransformerFactory.newInstance("org.apache.xalan.xsltc.trax.TransformerFactoryImpl", Thread.currentThread().getContextClassLoader())} + * + * @see XalanTransformerFactory.java */ public final class XalanTransformerFactory extends SAXTransformerFactory { private static final Logger LOGGER = LoggerFactory.getLogger(XalanTransformerFactory.class); @@ -129,4 +132,4 @@ public XMLFilter newXMLFilter(Templates templates) throws TransformerConfigurati return delegate.newXMLFilter(templates); } -} +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/jasperreports/graal/JRClassLoaderSubstitution.java b/runtime/src/main/java/io/quarkiverse/jasperreports/graal/JRClassLoaderSubstitution.java new file mode 100644 index 0000000..f7a1f8a --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/jasperreports/graal/JRClassLoaderSubstitution.java @@ -0,0 +1,34 @@ +package io.quarkiverse.jasperreports.graal; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +/** + * Substitution class for net.sf.jasperreports.engine.util.JRClassLoader. + * JasperReports loads classes by bytes out of .jasper files which is not supported by native images. + * This substitution class will load the classes using the current thread's context class loader. + */ +@TargetClass(className = "net.sf.jasperreports.engine.util.JRClassLoader") +final class JRClassLoaderSubstitution { + + /** + * Loads a class with the specified name using the current thread's context class loader. + * + * @param className The fully qualified name of the class to load. + * @param bytecodes The bytecodes of the class (not used in this implementation). + * @return The loaded Class object. + * @throws RuntimeException if the class cannot be found. + */ + @Substitute + protected Class loadClass(String className, byte[] bytecodes) { + Class clazz = null; + + try { + clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + return clazz; + } +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/jasperreports/graal/TemplatesImplSubstitution.java b/runtime/src/main/java/io/quarkiverse/jasperreports/graal/TemplatesImplSubstitution.java index 56ff5e6..8cc9c9d 100644 --- a/runtime/src/main/java/io/quarkiverse/jasperreports/graal/TemplatesImplSubstitution.java +++ b/runtime/src/main/java/io/quarkiverse/jasperreports/graal/TemplatesImplSubstitution.java @@ -3,6 +3,14 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; +/** + * Substitution class for org.apache.xalan.xsltc.trax.TemplatesImpl. + * This class is used to load classes by bytes out of .jasper files which is not supported by native images. + * This substitution class will throw an UnsupportedOperationException when trying to define a class. + * + * @see XalanTransformerFactory.java + */ @TargetClass(className = "org.apache.xalan.xsltc.trax.TemplatesImpl") final class TemplatesImplSubstitution {