diff --git a/build.sbt b/build.sbt index 991d09a0bfa4..e3c94279adc8 100644 --- a/build.sbt +++ b/build.sbt @@ -363,6 +363,7 @@ lazy val enso = (project in file(".")) `persistance`, `persistance-dsl`, pkg, + `poi-wrapper`, `polyglot-api`, `polyglot-api-macros`, `process-utils`, @@ -618,7 +619,7 @@ val bouncyCastle = Seq( val jlineVersion = "3.26.3" val jline = Seq( "org.jline" % "jline-terminal" % jlineVersion, - "org.jline" % "jline-terminal-jna" % jlineVersion, + "org.jline" % "jline-terminal-jni" % jlineVersion, // The terminal provider jna has been deprecated, check your configuration. "org.jline" % "jline-reader" % jlineVersion, "org.jline" % "jline-native" % jlineVersion ) @@ -1384,6 +1385,20 @@ lazy val `jna-wrapper` = project } ) +lazy val `poi-wrapper` = project + .in(file("lib/java/poi-wrapper")) + .settings( + frgaalJavaCompilerSetting, + version := "0.1", + autoScalaLibrary := false, + libraryDependencies ++= Seq( + "org.apache.poi" % "poi-ooxml" % poiOoxmlVersion + ), + assemblyMergeStrategy := { case _ => + MergeStrategy.preferProject + } + ) + lazy val `runtime-utils` = project .in(file("lib/java/runtime-utils")) .enablePlugins(JPMSPlugin) @@ -1435,6 +1450,7 @@ lazy val `directory-watcher-wrapper` = project ) } ) + .dependsOn(`jna-wrapper` % "provided") lazy val `fansi-wrapper` = project .in(file("lib/java/fansi-wrapper")) @@ -1495,7 +1511,6 @@ lazy val `akka-wrapper` = project "com.google.protobuf" % "protobuf-java" % googleProtobufVersion, "io.github.java-diff-utils" % "java-diff-utils" % javaDiffVersion, "org.reactivestreams" % "reactive-streams" % reactiveStreamsVersion, - "net.java.dev.jna" % "jna" % jnaVersion, "io.spray" %% "spray-json" % sprayJsonVersion ), javaModuleName := "org.enso.akka.wrapper", @@ -1503,6 +1518,9 @@ lazy val `akka-wrapper` = project "com.google.protobuf" % "protobuf-java" % googleProtobufVersion, "org.reactivestreams" % "reactive-streams" % reactiveStreamsVersion ), + Compile / internalModuleDependencies := Seq( + (`jna-wrapper` / Compile / exportedModule).value + ), assembly / assemblyExcludedJars := { val excludedJars = JPMSUtils.filterModulesFromUpdate( update.value, @@ -1511,8 +1529,7 @@ lazy val `akka-wrapper` = project "com.typesafe" % "config" % typesafeConfigVersion, "io.github.java-diff-utils" % "java-diff-utils" % javaDiffVersion, "com.google.protobuf" % "protobuf-java" % googleProtobufVersion, - "org.reactivestreams" % "reactive-streams" % reactiveStreamsVersion, - "net.java.dev.jna" % "jna" % jnaVersion + "org.reactivestreams" % "reactive-streams" % reactiveStreamsVersion ), streams.value.log, moduleName.value, @@ -1549,6 +1566,7 @@ lazy val `akka-wrapper` = project ) } ) + .dependsOn(`jna-wrapper` % "provided") lazy val `zio-wrapper` = project .in(file("lib/java/zio-wrapper")) @@ -3902,7 +3920,7 @@ lazy val `engine-runner` = project val NI_MODULES = "org.graalvm.nativeimage,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.base,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.librarysupport,org.graalvm.nativeimage.objectfile,org.graalvm.nativeimage.pointsto,com.oracle.graal.graal_enterprise,com.oracle.svm.svm_enterprise" val JDK_MODULES = - "java.desktop,java.naming,java.net.http,jdk.charsets,jdk.crypto.ec,jdk.localedata,jdk.httpserver,java.rmi" + "java.naming,java.net.http,jdk.charsets,jdk.crypto.ec,jdk.localedata,jdk.httpserver,java.rmi" val DEBUG_MODULES = "jdk.jdwp.agent" val PYTHON_MODULES = "jdk.security.auth,java.naming" @@ -3982,6 +4000,7 @@ lazy val `engine-runner` = project "-H:+AddAllCharsets", "-H:+IncludeAllLocales", "-H:+RunReachabilityHandlersConcurrently", + "-R:-InstallSegfaultHandler", // Workaround a problem with build-/runtime-initialization conflict // by disabling this service provider "-H:ServiceLoaderFeatureExcludeServiceProviders=net.snowflake.client.core.FileTypeDetector", @@ -4285,12 +4304,13 @@ lazy val `os-environment` = val targetDir = (Test / target).value NativeImage.buildNativeImage( "test-os-env", - staticOnLinux = true, + staticOnLinux = false, targetDir = targetDir, mainClass = Some("org.enso.os.environment.TestRunner"), additionalOptions = Seq( "-ea", - "--features=org.enso.os.environment.TestCollectorFeature" + "--features=org.enso.os.environment.TestCollectorFeature", + "-R:-InstallSegfaultHandler" ) ) }.value, @@ -4301,7 +4321,7 @@ lazy val `os-environment` = val exeFile = (Test / target).value / ("test-os-env" + exeSuffix) val binPath = exeFile.getAbsolutePath - val res = binPath ! logger + val res = Process(Seq(binPath), None, "JAVA_OPTS" -> "") ! logger if (res != 0) { logger.error("Some test in os-environment failed") throw new TestsFailedException() @@ -5027,6 +5047,11 @@ lazy val `std-table` = project "org.mockito" % "mockito-core" % mockitoJavaVersion % Test, "org.mockito" % "mockito-junit-jupiter" % mockitoJavaVersion % Test ), + Compile / unmanagedJars := { + Seq( + Attributed.blank((`poi-wrapper` / assembly).value) + ) + }, Compile / packageBin := { val result = (Compile / packageBin).value val cacheStoreFactory = streams.value.cacheStoreFactory @@ -5037,13 +5062,21 @@ lazy val `std-table` = project ignoreScalaLibrary = true, libraryUpdates = (Compile / update).value, unmanagedClasspath = (Compile / unmanagedJars).value, - logger = streams.value.log, - cacheStoreFactory = cacheStoreFactory, - previousRun = None + ignoreDependencies = Some( + Seq( + "org.apache.poi" % "poi" % poiOoxmlVersion, + "org.apache.poi" % "poi-ooxml" % poiOoxmlVersion, + "org.apache.poi" % "poi-ooxml-lite" % poiOoxmlVersion + ) + ), + logger = streams.value.log, + cacheStoreFactory = cacheStoreFactory, + previousRun = None ) result } ) + .dependsOn(`poi-wrapper`) .dependsOn(`std-base` % "provided") lazy val extractNativeLibs = taskKey[AnalysisOfExtractedNativeLibs]( @@ -5082,7 +5115,8 @@ lazy val `std-image` = project `image-polyglot-root`, Seq("std-image.jar", "opencv.jar"), ignoreScalaLibrary = true, - ignoreDependency = Some("org.openpnp" % "opencv" % opencvVersion), + ignoreDependencies = + Some(Seq("org.openpnp" % "opencv" % opencvVersion)), libraryUpdates = (Compile / update).value, logger = logger, cacheStoreFactory = cacheStoreFactory, @@ -5515,7 +5549,6 @@ lazy val `std-tableau` = project unmanagedClasspath = unmanagedClasspath, previousRun = prev ) - StdBits .extractNativeLibsFromTableau( `std-tableau-polyglot-root`, diff --git a/distribution/engine/THIRD-PARTY/NOTICE b/distribution/engine/THIRD-PARTY/NOTICE index d3f3a5e007df..ce5a7da4db1c 100644 --- a/distribution/engine/THIRD-PARTY/NOTICE +++ b/distribution/engine/THIRD-PARTY/NOTICE @@ -361,11 +361,6 @@ The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `jakarta.inject.jakarta.inject-api-2.0.1`. -'jna', licensed under the Apache-2.0, is distributed with the engine. -The license file can be found at `licenses/APACHE2.0`. -Copyright notices related to this dependency can be found in the directory `net.java.dev.jna.jna-5.14.0`. - - 'commons-compress', licensed under the Apache-2.0, is distributed with the engine. The license file can be found at `licenses/APACHE2.0`. Copyright notices related to this dependency can be found in the directory `org.apache.commons.commons-compress-1.23.0`. @@ -531,9 +526,9 @@ The license file can be found at `licenses/BSD-3-Clause`. Copyright notices related to this dependency can be found in the directory `org.jline.jline-terminal-3.26.3`. -'jline-terminal-jna', licensed under the The BSD License, is distributed with the engine. +'jline-terminal-jni', licensed under the The BSD License, is distributed with the engine. The license file can be found at `licenses/BSD-3-Clause`. -Copyright notices related to this dependency can be found in the directory `org.jline.jline-terminal-jna-3.26.3`. +Copyright notices related to this dependency can be found in the directory `org.jline.jline-terminal-jni-3.26.3`. 'org-netbeans-modules-sampler', licensed under the The Apache Software License, Version 2.0, is distributed with the engine. diff --git a/distribution/engine/THIRD-PARTY/net.java.dev.jna.jna-5.14.0/NOTICES b/distribution/engine/THIRD-PARTY/net.java.dev.jna.jna-5.14.0/NOTICES deleted file mode 100644 index ff7264fb2cba..000000000000 --- a/distribution/engine/THIRD-PARTY/net.java.dev.jna.jna-5.14.0/NOTICES +++ /dev/null @@ -1,29 +0,0 @@ -Copyright (c) 2007 Timothy Wall, All Rights Reserved - -Copyright (c) 2007 Wayne Meissner, All Rights Reserved - -Copyright (c) 2007-2008 Timothy Wall, All Rights Reserved - -Copyright (c) 2007-2012 Timothy Wall, All Rights Reserved - -Copyright (c) 2007-2013 Timothy Wall, All Rights Reserved - -Copyright (c) 2007-2015 Timothy Wall, All Rights Reserved - -Copyright (c) 2009 Timothy Wall, All Rights Reserved - -Copyright (c) 2011 Timothy Wall, All Rights Reserved - -Copyright (c) 2012 Timothy Wall, All Rights Reserved - -Copyright (c) 2017 Matthias Bläsing, All Rights Reserved - -Copyright (c) 2018 Matthias Bläsing - -Copyright (c) 2019 Matthias Bläsing, All Rights Reserved - -Copyright (c) 2021, Matthias Bläsing, All Rights Reserved - -Copyright (c) 2022 Carlos Ballesteros, All Rights Reserved - -Copyright 2007 Timothy Wall diff --git a/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jna-3.26.3/NOTICES b/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jna-3.26.3/NOTICES deleted file mode 100644 index 37df4c6d42da..000000000000 --- a/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jna-3.26.3/NOTICES +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2002-2016, the original author(s). - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * https://opensource.org/licenses/BSD-3-Clause - */ - -/* - * Copyright (c) 2002-2017, the original author(s). - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * https://opensource.org/licenses/BSD-3-Clause - */ - -/* - * Copyright (c) 2002-2018, the original author(s). - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * https://opensource.org/licenses/BSD-3-Clause - */ - -/* - * Copyright (c) 2002-2020, the original author(s). - * - * This software is distributable under the BSD license. See the terms of the - * BSD license in the documentation provided with this software. - * - * https://opensource.org/licenses/BSD-3-Clause - */ - -Copyright (C) 2022 the original author(s). - -Copyright (c) 2002-2020, the original author or authors. diff --git a/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jni-3.26.3/NOTICES b/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jni-3.26.3/NOTICES new file mode 100644 index 000000000000..6f4e488e5643 --- /dev/null +++ b/distribution/engine/THIRD-PARTY/org.jline.jline-terminal-jni-3.26.3/NOTICES @@ -0,0 +1,9 @@ +Copyright (C) 2022 the original author(s). + +Copyright (c) 2002-2017, the original author(s). + +Copyright (c) 2002-2020, the original author or authors. + +Copyright (c) 2002-2020, the original author(s). + +Copyright (c) 2009-2018, the original author(s). diff --git a/engine/runner/src/main/java/org/enso/runner/JavaFinder.java b/engine/runner/src/main/java/org/enso/runner/JavaFinder.java index a8724a5af5bf..edc5bca29201 100644 --- a/engine/runner/src/main/java/org/enso/runner/JavaFinder.java +++ b/engine/runner/src/main/java/org/enso/runner/JavaFinder.java @@ -1,5 +1,6 @@ package org.enso.runner; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Comparator; @@ -27,10 +28,10 @@ private JavaFinder() {} * @return null if cannot be found. Otherwise, returns the absolute path to the executable, or * simply {@code java} if it is on the {@code PATH}. */ - static String findJavaExecutable() { + static File findJavaExecutable() { var javaInRuntime = findJavaExecutableInDistributionRuntimes(); if (javaInRuntime != null) { - return javaInRuntime.toAbsolutePath().toString(); + return javaInRuntime.toAbsolutePath().toFile(); } logger.warn("No appropriate JDK found in the distribution runtimes. Trying system-wide JDK."); var javaHome = System.getenv("JAVA_HOME"); @@ -44,7 +45,7 @@ static String findJavaExecutable() { } if (javaExe.toFile().exists()) { logger.info("Found JDK in JAVA_HOME: {}", javaHome); - return javaExe.toAbsolutePath().toString(); + return javaExe.toAbsolutePath().toFile(); } else { logger.warn( "No JDK found in JAVA_HOME (missing Java executable at {}). Trying java on PATH.", @@ -54,10 +55,9 @@ static String findJavaExecutable() { logger.warn("JAVA_HOME is not set. Trying java on PATH."); } - if (isJavaOnPath()) { - var javaExe = isOnWindows() ? "java.exe" : "java"; - logger.warn("Falling back to java on PATH: {}", javaExe); - return javaExe; + if (findJavaOnPath() instanceof File javaExecutable) { + logger.warn("Falling back to java on PATH: {}", javaExecutable); + return javaExecutable; } logger.warn("No JDK found on PATH. Cannot start the runtime."); return null; @@ -107,7 +107,7 @@ private static Path findJavaExecutableInDistributionRuntimes() { return null; } - private static boolean isJavaOnPath() { + private static File findJavaOnPath() { try { ProcessBuilder processBuilder; if (isOnWindows()) { @@ -116,10 +116,11 @@ private static boolean isJavaOnPath() { processBuilder = new ProcessBuilder("java", "-h"); } Process process = processBuilder.start(); + var pathOpt = process.info().command(); boolean exitSucc = process.waitFor(5L, TimeUnit.SECONDS); - return exitSucc; + return exitSucc && pathOpt.isPresent() ? new File(pathOpt.get()) : null; } catch (IOException | InterruptedException e) { - return false; + return null; } } } diff --git a/engine/runner/src/main/java/org/enso/runner/Main.java b/engine/runner/src/main/java/org/enso/runner/Main.java index f84b797c0390..af004c0cbfad 100644 --- a/engine/runner/src/main/java/org/enso/runner/Main.java +++ b/engine/runner/src/main/java/org/enso/runner/Main.java @@ -36,6 +36,7 @@ import org.enso.distribution.Environment; import org.enso.editions.DefaultEdition; import org.enso.libraryupload.LibraryUploader.UploadFailedError; +import org.enso.os.environment.jni.JVM; import org.enso.pkg.Contact; import org.enso.pkg.PackageManager; import org.enso.pkg.PackageManager$; @@ -1437,17 +1438,20 @@ private boolean isJvmModeEnabled(CommandLine line) { } private void launchJvm( - CommandLine line, Map props, File component, String javaPath) + CommandLine line, Map props, File component, File javaExecutable) throws IOException, InterruptedException { + var useJNI = true; var commandAndArgs = new ArrayList(); - commandAndArgs.add(javaPath); - var jvmOptions = System.getenv("JAVA_OPTS"); - if (jvmOptions != null) { - for (var op : jvmOptions.split(" ")) { - if (op.isEmpty()) { - continue; + if (!useJNI) { + commandAndArgs.add(javaExecutable.getPath()); + var jvmOptions = System.getenv("JAVA_OPTS"); + if (jvmOptions != null) { + for (var op : jvmOptions.split(" ")) { + if (op.isEmpty()) { + continue; + } + commandAndArgs.add(op); } - commandAndArgs.add(op); } } var assertsOn = false; @@ -1463,13 +1467,23 @@ private void launchJvm( commandAndArgs.add("--sun-misc-unsafe-memory-access=allow"); commandAndArgs.add("--enable-native-access=org.graalvm.truffle"); commandAndArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); - commandAndArgs.add("--module-path"); if (!component.isDirectory()) { throw new IOException("Cannot find " + component + " directory"); } - commandAndArgs.add(component.getPath()); - commandAndArgs.add("-m"); - commandAndArgs.add("org.enso.runner/org.enso.runner.Main"); + JVM jvm; + if (useJNI) { + commandAndArgs.add("--module-path=" + component.getPath()); + commandAndArgs.add("-Djdk.module.main=org.enso.runner"); + var javaHome = javaExecutable.getParentFile().getParentFile(); + jvm = JVM.create(javaHome, commandAndArgs.toArray(new String[0])); + commandAndArgs.clear(); + } else { + commandAndArgs.add("--module-path"); + commandAndArgs.add(component.getPath()); + commandAndArgs.add("-m"); + commandAndArgs.add("org.enso.runner/org.enso.runner.Main"); + jvm = null; + } var it = line.iterator(); while (it.hasNext()) { var op = it.next(); @@ -1491,11 +1505,18 @@ private void launchJvm( } } commandAndArgs.addAll(line.getArgList()); - var pb = new ProcessBuilder(); - pb.inheritIO(); - pb.command(commandAndArgs); - var p = pb.start(); - var exitCode = p.waitFor(); + int exitCode; + if (jvm != null) { + jvm.executeMain("org/enso/runner/Main", commandAndArgs.toArray(new String[0])); + // the above call should never return + exitCode = 1; + } else { + var pb = new ProcessBuilder(); + pb.inheritIO(); + pb.command(commandAndArgs); + var p = pb.start(); + exitCode = p.waitFor(); + } if (exitCode == 0) { throw exitSuccess(); } else { @@ -1541,16 +1562,15 @@ private void launch(String[] args) throws IOException, InterruptedException, URI var javaExe = JavaFinder.findJavaExecutable(); if (javaExe == null) { // Try your best if `jvm` mode enabled in a project - if (!jvmInProjectEnforced) throw exitFail("Cannot find java executable"); + if (!jvmInProjectEnforced) { + throw exitFail("Cannot find java executable"); + } } else { launchJvm(line, props, component, javaExe); } } else { - launchJvm( - line, - props, - component, - new File(new File(new File(jvm), "bin"), "java").getAbsolutePath()); + var javaExecutable = new File(new File(new File(jvm), "bin"), "java").getAbsoluteFile(); + launchJvm(line, props, component, javaExecutable); } } } diff --git a/lib/java/os-environment/src/main/java/module-info.java b/lib/java/os-environment/src/main/java/module-info.java index 7b0d1a32723e..b64339edf424 100644 --- a/lib/java/os-environment/src/main/java/module-info.java +++ b/lib/java/os-environment/src/main/java/module-info.java @@ -9,6 +9,7 @@ requires org.apache.commons.io; exports org.enso.os.environment; + exports org.enso.os.environment.jni; exports org.enso.os.environment.chdir; exports org.enso.os.environment.trash; exports org.enso.os.environment.directories; diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/DesktopEnvironment.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/DesktopEnvironment.java deleted file mode 100644 index 7cdc3cfd2c89..000000000000 --- a/lib/java/os-environment/src/main/java/org/enso/os/environment/DesktopEnvironment.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.enso.os.environment; - -import org.enso.os.environment.directories.Directories; -import org.enso.os.environment.trash.TrashBin; - -public final class DesktopEnvironment { - private static final Directories DIRECTORIES = Directories.getCurrent(); - private static final TrashBin TRASH_BIN = TrashBin.getCurrent(); - - private DesktopEnvironment() {} - - public static Directories getDirectories() { - return DIRECTORIES; - } - - public static TrashBin getTrashBin() { - return TRASH_BIN; - } -} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNI.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNI.java new file mode 100644 index 000000000000..630f8e14fc34 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNI.java @@ -0,0 +1,855 @@ +package org.enso.os.environment.jni; + +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.constant.CConstant; +import org.graalvm.nativeimage.c.function.CFunction.Transition; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CPointerTo; +import org.graalvm.nativeimage.c.struct.CStruct; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CDoublePointer; +import org.graalvm.nativeimage.c.type.CFloatPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.CLongPointer; +import org.graalvm.nativeimage.c.type.CShortPointer; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.word.PointerBase; + +@CContext(JNIDirectives.class) +public final class JNI { + @CConstant + static native int JNI_OK(); + + @CConstant + static native int JNI_ERR(); + + @CConstant + static native int JNI_EDETACHED(); + + @CConstant + static native int JNI_EVERSION(); + + @CConstant + static native int JNI_ENOMEM(); + + @CConstant + static native int JNI_EEXIST(); + + @CConstant + static native int JNI_EINVAL(); + + @CConstant + static native int JNI_VERSION_1_1(); + + @CConstant + static native int JNI_VERSION_10(); + + @CConstant + static native int JNI_VERSION_21(); + + interface JMethodID extends PointerBase {} + + interface JFieldID extends PointerBase {} + + interface JObject extends PointerBase {} + + interface JArray extends JObject { + + int MODE_WRITE_RELEASE = 0; + int MODE_WRITE = 1; + int MODE_RELEASE = 2; + } + + interface JBooleanArray extends JArray {} + + interface JByteArray extends JArray {} + + interface JCharArray extends JArray {} + + interface JShortArray extends JArray {} + + interface JIntArray extends JArray {} + + interface JLongArray extends JArray {} + + interface JFloatArray extends JArray {} + + interface JDoubleArray extends JArray {} + + interface JObjectArray extends JArray {} + + interface JClass extends JObject {} + + interface JString extends JObject {} + + interface JThrowable extends JObject {} + + interface JWeak extends JObject {} + + @CStruct("jvalue") + interface JValue extends PointerBase { + + // @formatter:off + @CField("z") + boolean getBoolean(); + + @CField("b") + byte getByte(); + + @CField("c") + char getChar(); + + @CField("s") + short getShort(); + + @CField("i") + int getInt(); + + @CField("j") + long getLong(); + + @CField("f") + float getFloat(); + + @CField("d") + double getDouble(); + + @CField("l") + JObject getJObject(); + + @CField("z") + void setBoolean(boolean b); + + @CField("b") + void setByte(byte b); + + @CField("c") + void setChar(char ch); + + @CField("s") + void setShort(short s); + + @CField("i") + void setInt(int i); + + @CField("j") + void setLong(long l); + + @CField("f") + void setFloat(float f); + + @CField("d") + void setDouble(double d); + + @CField("l") + void setJObject(JObject obj); + + // @formatter:on + + /** Gets JValue in an array of JValues pointed to by this object. */ + JValue addressOf(int index); + } + + @CStruct(value = "JNIEnv_", addStructKeyword = true) + interface JNIEnv extends PointerBase { + + @CField("functions") + JNINativeInterface getFunctions(); + } + + @CPointerTo(JNIEnv.class) + interface JNIEnvPointer extends PointerBase { + + JNIEnv readJNIEnv(); + + void writeJNIEnv(JNIEnv env); + } + + @CStruct(value = "JavaVM_", addStructKeyword = true) + interface JavaVM extends PointerBase { + + @CField("functions") + JNIInvokeInterface getFunctions(); + } + + @CPointerTo(JavaVM.class) + interface JavaVMPointer extends PointerBase { + + JavaVM readJavaVM(); + + void writeJavaVM(JavaVM javaVM); + } + + @CStruct(value = "JavaVMAttachArgs", addStructKeyword = true) + interface JavaVMAttachArgs extends PointerBase { + + @CField("version") + int getVersion(); + + @CField("version") + void setVersion(int version); + + @CField("name") + CCharPointer getName(); + + @CField("name") + void setName(CCharPointer name); + + @CField("group") + JObject getGroup(); + + @CField("group") + void setGroup(JObject group); + } + + @CStruct(value = "JNIInvokeInterface_", addStructKeyword = true) + interface JNIInvokeInterface extends PointerBase { + + @CField("AttachCurrentThread") + AttachCurrentThread getAttachCurrentThread(); + + @CField("AttachCurrentThreadAsDaemon") + AttachCurrentThreadAsDaemon getAttachCurrentThreadAsDaemon(); + + @CField("DetachCurrentThread") + DetachCurrentThread getDetachCurrentThread(); + + @CField("GetEnv") + GetEnv getGetEnv(); + } + + interface CallStaticIntMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface CallStaticBooleanMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface CallStaticVoidMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface CallStaticObjectMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JObject callNoTransition(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface CallStaticLongMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + long call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface CallObjectMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject object, JMethodID methodID, JValue args); + } + + interface CallVoidMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallBooleanMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallShortMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + short call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallIntMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallLongMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + long call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallDoubleMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + double call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallFloatMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + float call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallByteMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + byte call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface CallCharMethodA extends CFunctionPointer { + + @InvokeCFunctionPointer + char call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + interface DeleteGlobalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JObject gref); + } + + interface DeleteWeakGlobalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JWeak wref); + } + + interface DeleteLocalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JObject lref); + } + + interface PushLocalFrame extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, int capacity); + } + + interface PopLocalFrame extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject result); + } + + interface ExceptionCheck extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + boolean callNoTransition(JNIEnv env); + } + + interface ExceptionClear extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env); + } + + interface ExceptionDescribe extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + void callNoTransition(JNIEnv env); + } + + interface ExceptionOccurred extends CFunctionPointer { + + @InvokeCFunctionPointer + JThrowable call(JNIEnv env); + } + + interface FindClass extends CFunctionPointer { + + @InvokeCFunctionPointer + JClass call(JNIEnv env, CCharPointer name); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JClass callNoTransition(JNIEnv env, CCharPointer name); + } + + interface DefineClass extends CFunctionPointer { + + @InvokeCFunctionPointer + JClass call(JNIEnv env, CCharPointer name, JObject loader, CCharPointer buf, long bufLen); + } + + interface GetArrayLength extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JArray array); + } + + interface GetBooleanArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JBooleanArray array, JValue isCopy); + } + + interface GetByteArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JByteArray array, JValue isCopy); + } + + interface GetCharArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JCharArray array, JValue isCopy); + } + + interface GetShortArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JShortArray array, JValue isCopy); + } + + interface GetIntArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CIntPointer call(JNIEnv env, JIntArray array, JValue isCopy); + } + + interface GetLongArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CLongPointer call(JNIEnv env, JLongArray array, JValue isCopy); + } + + interface GetFloatArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CFloatPointer call(JNIEnv env, JFloatArray array, JValue isCopy); + } + + interface GetDoubleArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + CDoublePointer call(JNIEnv env, JDoubleArray array, JValue isCopy); + } + + interface GetMethodID extends CFunctionPointer { + + @InvokeCFunctionPointer + JMethodID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JMethodID callNoTransition(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + interface GetObjectArrayElement extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObjectArray array, int index); + } + + interface GetObjectClass extends CFunctionPointer { + + @InvokeCFunctionPointer + JClass call(JNIEnv env, JObject object); + } + + interface GetObjectRefType extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JObject obj); + } + + interface GetStaticMethodID extends CFunctionPointer { + + @InvokeCFunctionPointer + JMethodID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JMethodID callNoTransition(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + interface GetStringChars extends CFunctionPointer { + + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JString string, JValue isCopy); + } + + interface GetStringLength extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JString string); + } + + interface GetStringUTFChars extends CFunctionPointer { + + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JString string, JValue isCopy); + } + + interface GetStringUTFLength extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JString str); + } + + interface IsSameObject extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject ref1, JObject ref2); + } + + interface NewBooleanArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JBooleanArray call(JNIEnv env, int len); + } + + interface NewByteArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JByteArray call(JNIEnv env, int len); + } + + interface NewCharArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JCharArray call(JNIEnv env, int len); + } + + interface NewShortArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JShortArray call(JNIEnv env, int len); + } + + interface NewIntArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JIntArray call(JNIEnv env, int len); + } + + interface NewLongArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JLongArray call(JNIEnv env, int len); + } + + interface NewFloatArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JFloatArray call(JNIEnv env, int len); + } + + interface NewDoubleArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JDoubleArray call(JNIEnv env, int len); + } + + interface NewGlobalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject lobj); + } + + interface NewWeakGlobalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + JWeak call(JNIEnv env, JObject lobj); + } + + interface NewObjectA extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JObject callNoTransition(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + interface NewLocalRef extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject obj); + } + + interface NewObjectArray extends CFunctionPointer { + + @InvokeCFunctionPointer + JObjectArray call(JNIEnv env, int len, JClass clazz, JObject init); + } + + interface NewString extends CFunctionPointer { + + @InvokeCFunctionPointer + JString call(JNIEnv env, CShortPointer unicode, int len); + } + + interface NewStringUTF8 extends CFunctionPointer { + + @InvokeCFunctionPointer + JString call(JNIEnv env, CCharPointer bytes); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JString callNoTransition(JNIEnv env, CCharPointer bytes); + } + + interface ReleaseBooleanArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, CCharPointer elems, int mode); + } + + interface ReleaseByteArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, CCharPointer elems, int mode); + } + + interface ReleaseCharArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, CShortPointer elems, int mode); + } + + interface ReleaseShortArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, CShortPointer elems, int mode); + } + + interface ReleaseIntArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, CIntPointer elems, int mode); + } + + interface ReleaseLongArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, CLongPointer elems, int mode); + } + + interface ReleaseFloatArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, CFloatPointer elems, int mode); + } + + interface ReleaseDoubleArrayElements extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, CDoublePointer elems, int mode); + } + + interface GetBooleanArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, int start, int len, CCharPointer buf); + } + + interface GetByteArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, int start, int len, CCharPointer buf); + } + + interface GetCharArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, int start, int len, CShortPointer buf); + } + + interface GetShortArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, int start, int len, CShortPointer buf); + } + + interface GetIntArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, int start, int len, CIntPointer buf); + } + + interface GetLongArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, int start, int len, CLongPointer buf); + } + + interface GetFloatArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, int start, int len, CFloatPointer buf); + } + + interface GetDoubleArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, int start, int len, CDoublePointer buf); + } + + interface SetBooleanArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, int start, int len, CCharPointer buf); + } + + interface SetByteArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, int start, int len, CCharPointer buf); + } + + interface SetCharArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, int start, int len, CShortPointer buf); + } + + interface SetShortArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, int start, int len, CShortPointer buf); + } + + interface SetIntArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, int start, int len, CIntPointer buf); + } + + interface SetLongArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, int start, int len, CLongPointer buf); + } + + interface SetFloatArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, int start, int len, CFloatPointer buf); + } + + interface SetDoubleArrayRegion extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, int start, int len, CDoublePointer buf); + } + + interface ReleaseStringChars extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JString string, CShortPointer chars); + } + + interface ReleaseStringUTFChars extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JString string, CCharPointer chars); + } + + interface SetObjectArrayElement extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JObjectArray array, int index, JObject val); + } + + interface Throw extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JThrowable throwable); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + int callNoTransition(JNIEnv env, JThrowable throwable); + } + + interface GetDirectBufferAddress extends CFunctionPointer { + + @InvokeCFunctionPointer + VoidPointer call(JNIEnv env, JObject buf); + } + + interface IsInstanceOf extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject o, JClass c); + } + + interface GetStaticFieldID extends CFunctionPointer { + + @InvokeCFunctionPointer + JFieldID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + interface GetFieldID extends CFunctionPointer { + + @InvokeCFunctionPointer + JFieldID call(JNIEnv env, JClass c, CCharPointer name, CCharPointer sig); + } + + interface GetStaticObjectField extends CFunctionPointer { + + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JFieldID fieldID); + } + + interface GetIntField extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JObject o, JFieldID fieldId); + } + + interface GetStaticBooleanField extends CFunctionPointer { + + @InvokeCFunctionPointer + boolean call(JNIEnv env, JClass clazz, JFieldID fieldID); + } + + interface SetStaticBooleanField extends CFunctionPointer { + + @InvokeCFunctionPointer + void call(JNIEnv env, JClass clazz, JFieldID fieldID, boolean value); + } + + interface GetJavaVM extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JNIEnv env, JavaVMPointer javaVMOut); + } + + interface AttachCurrentThread extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, JavaVMAttachArgs args); + } + + interface AttachCurrentThreadAsDaemon extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, JavaVMAttachArgs args); + } + + interface DetachCurrentThread extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JavaVM vm); + } + + interface GetEnv extends CFunctionPointer { + + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, int version); + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIBoot.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIBoot.java new file mode 100644 index 000000000000..4b1d15a13332 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIBoot.java @@ -0,0 +1,64 @@ +package org.enso.os.environment.jni; + +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CStruct; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.WordPointer; +import org.graalvm.word.PointerBase; + +/** Java virtual machine initialization API. */ +@CContext(JNIDirectives.class) +final class JNIBoot { + interface JNICreateJavaVMPointer extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNI.JavaVMPointer jvmptr, JNI.JNIEnvPointer env, Args args); + } + + @CStruct("JavaVMInitArgs") + interface Args extends PointerBase { + @CField + int version(); + + @CField + void version(int v); + + @CField + int nOptions(); + + @CField + void nOptions(int n); + + @CField + Option options(); + + @CField + void options(Option ptr); + + @CField + boolean ignoreUnrecognized(); + + @CField + void ignoreUnrecognized(boolean v); + } + + @CStruct(value = "JavaVMOption") + interface Option extends PointerBase { + + @CField("optionString") + CCharPointer getOptionString(); + + @CField("optionString") + void setOptionString(CCharPointer value); + + @CField("extraInfo") + WordPointer getExtraInfo(); + + @CField("extraInfo") + void setExtraInfo(WordPointer value); + + Option addressOf(int index); + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIDirectives.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIDirectives.java new file mode 100644 index 000000000000..81a0d0d9b19e --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNIDirectives.java @@ -0,0 +1,42 @@ +package org.enso.os.environment.jni; + +import java.io.File; +import java.util.List; +import org.graalvm.nativeimage.c.CContext; + +final class JNIDirectives implements CContext.Directives { + + @Override + public List getLibraryPaths() { + var javaHome = new File(System.getProperty("java.home")); + var binServer = new File(new File(javaHome, "bin"), "server"); + var libServer = new File(new File(javaHome, "lib"), "server"); + if (binServer.isDirectory()) { + return List.of(binServer.getPath()); + } else { + return List.of(libServer.getPath()); + } + } + + @Override + public List getOptions() { + var javaHome = new File(System.getProperty("java.home")); + var include = new File(javaHome, "include"); + assert include.isDirectory(); + var jni = new File(include, "jni.h"); + assert jni.canRead(); + for (var subDir : include.listFiles()) { + var md = new File(subDir, "jni_md.h"); + if (md.canRead()) { + var includes = List.of("-I", jni.getParent(), "-I", md.getParent()); + return includes; + } + } + throw new AssertionError("Cannot find libs in " + javaHome); + } + + @Override + public List getHeaderFiles() { + return List.of("", ""); + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNINativeInterface.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNINativeInterface.java new file mode 100644 index 000000000000..bc79381c0f8a --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JNINativeInterface.java @@ -0,0 +1,302 @@ +package org.enso.os.environment.jni; + +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CStruct; +import org.graalvm.word.PointerBase; + +@CContext(value = JNIDirectives.class) +@CStruct(value = "JNINativeInterface_", addStructKeyword = true) +public interface JNINativeInterface extends PointerBase { + + @CField(value = "NewString") + JNI.NewString getNewString(); + + @CField(value = "GetStringLength") + JNI.GetStringLength getGetStringLength(); + + @CField(value = "GetStringChars") + JNI.GetStringChars getGetStringChars(); + + @CField(value = "ReleaseStringChars") + JNI.ReleaseStringChars getReleaseStringChars(); + + @CField(value = "NewStringUTF") + JNI.NewStringUTF8 getNewStringUTF(); + + @CField(value = "GetStringUTFLength") + JNI.GetStringUTFLength getGetStringUTFLength(); + + @CField(value = "GetStringUTFChars") + JNI.GetStringUTFChars getGetStringUTFChars(); + + @CField(value = "ReleaseStringUTFChars") + JNI.ReleaseStringUTFChars getReleaseStringUTFChars(); + + @CField(value = "GetArrayLength") + JNI.GetArrayLength getGetArrayLength(); + + @CField(value = "NewLocalRef") + JNI.NewLocalRef getNewLocalRef(); + + @CField(value = "NewObjectArray") + JNI.NewObjectArray getNewObjectArray(); + + @CField(value = "NewBooleanArray") + JNI.NewBooleanArray getNewBooleanArray(); + + @CField(value = "NewByteArray") + JNI.NewByteArray getNewByteArray(); + + @CField(value = "NewCharArray") + JNI.NewCharArray getNewCharArray(); + + @CField(value = "NewShortArray") + JNI.NewShortArray getNewShortArray(); + + @CField(value = "NewIntArray") + JNI.NewIntArray getNewIntArray(); + + @CField(value = "NewLongArray") + JNI.NewLongArray getNewLongArray(); + + @CField(value = "NewFloatArray") + JNI.NewFloatArray getNewFloatArray(); + + @CField(value = "NewDoubleArray") + JNI.NewDoubleArray getNewDoubleArray(); + + @CField(value = "GetObjectArrayElement") + JNI.GetObjectArrayElement getGetObjectArrayElement(); + + @CField(value = "SetObjectArrayElement") + JNI.SetObjectArrayElement getSetObjectArrayElement(); + + @CField(value = "GetBooleanArrayElements") + JNI.GetBooleanArrayElements getGetBooleanArrayElements(); + + @CField(value = "GetByteArrayElements") + JNI.GetByteArrayElements getGetByteArrayElements(); + + @CField(value = "GetCharArrayElements") + JNI.GetCharArrayElements getGetCharArrayElements(); + + @CField(value = "GetShortArrayElements") + JNI.GetShortArrayElements getGetShortArrayElements(); + + @CField(value = "GetIntArrayElements") + JNI.GetIntArrayElements getGetIntArrayElements(); + + @CField(value = "GetLongArrayElements") + JNI.GetLongArrayElements getGetLongArrayElements(); + + @CField(value = "GetFloatArrayElements") + JNI.GetFloatArrayElements getGetFloatArrayElements(); + + @CField(value = "GetDoubleArrayElements") + JNI.GetDoubleArrayElements getGetDoubleArrayElements(); + + @CField(value = "ReleaseBooleanArrayElements") + JNI.ReleaseBooleanArrayElements getReleaseBooleanArrayElements(); + + @CField(value = "ReleaseByteArrayElements") + JNI.ReleaseByteArrayElements getReleaseByteArrayElements(); + + @CField(value = "ReleaseCharArrayElements") + JNI.ReleaseCharArrayElements getReleaseCharArrayElements(); + + @CField(value = "ReleaseShortArrayElements") + JNI.ReleaseShortArrayElements getReleaseShortArrayElements(); + + @CField(value = "ReleaseIntArrayElements") + JNI.ReleaseIntArrayElements getReleaseIntArrayElements(); + + @CField(value = "ReleaseLongArrayElements") + JNI.ReleaseLongArrayElements getReleaseLongArrayElements(); + + @CField(value = "ReleaseFloatArrayElements") + JNI.ReleaseFloatArrayElements getReleaseFloatArrayElements(); + + @CField(value = "ReleaseDoubleArrayElements") + JNI.ReleaseDoubleArrayElements getReleaseDoubleArrayElements(); + + @CField(value = "GetBooleanArrayRegion") + JNI.GetBooleanArrayRegion getGetBooleanArrayRegion(); + + @CField(value = "GetByteArrayRegion") + JNI.GetByteArrayRegion getGetByteArrayRegion(); + + @CField(value = "GetCharArrayRegion") + JNI.GetCharArrayRegion getGetCharArrayRegion(); + + @CField(value = "GetShortArrayRegion") + JNI.GetShortArrayRegion getGetShortArrayRegion(); + + @CField(value = "GetIntArrayRegion") + JNI.GetIntArrayRegion getGetIntArrayRegion(); + + @CField(value = "GetLongArrayRegion") + JNI.GetLongArrayRegion getGetLongArrayRegion(); + + @CField(value = "GetFloatArrayRegion") + JNI.GetFloatArrayRegion getGetFloatArrayRegion(); + + @CField(value = "GetDoubleArrayRegion") + JNI.GetDoubleArrayRegion getGetDoubleArrayRegion(); + + @CField(value = "SetBooleanArrayRegion") + JNI.SetBooleanArrayRegion getSetBooleanArrayRegion(); + + @CField(value = "SetByteArrayRegion") + JNI.SetByteArrayRegion getSetByteArrayRegion(); + + @CField(value = "SetCharArrayRegion") + JNI.SetCharArrayRegion getSetCharArrayRegion(); + + @CField(value = "SetShortArrayRegion") + JNI.SetShortArrayRegion getSetShortArrayRegion(); + + @CField(value = "SetIntArrayRegion") + JNI.SetIntArrayRegion getSetIntArrayRegion(); + + @CField(value = "SetLongArrayRegion") + JNI.SetLongArrayRegion getSetLongArrayRegion(); + + @CField(value = "SetFloatArrayRegion") + JNI.SetFloatArrayRegion getSetFloatArrayRegion(); + + @CField(value = "SetDoubleArrayRegion") + JNI.SetDoubleArrayRegion getSetDoubleArrayRegion(); + + @CField(value = "FindClass") + JNI.FindClass getFindClass(); + + @CField(value = "DefineClass") + JNI.DefineClass getDefineClass(); + + @CField(value = "IsSameObject") + JNI.IsSameObject getIsSameObject(); + + @CField(value = "GetObjectClass") + JNI.GetObjectClass getGetObjectClass(); + + @CField(value = "NewGlobalRef") + JNI.NewGlobalRef getNewGlobalRef(); + + @CField(value = "DeleteGlobalRef") + JNI.DeleteGlobalRef getDeleteGlobalRef(); + + @CField(value = "NewWeakGlobalRef") + JNI.NewWeakGlobalRef getNewWeakGlobalRef(); + + @CField(value = "DeleteWeakGlobalRef") + JNI.DeleteWeakGlobalRef getDeleteWeakGlobalRef(); + + @CField(value = "DeleteLocalRef") + JNI.DeleteLocalRef getDeleteLocalRef(); + + @CField(value = "PushLocalFrame") + JNI.PushLocalFrame getPushLocalFrame(); + + @CField(value = "PopLocalFrame") + JNI.PopLocalFrame getPopLocalFrame(); + + @CField(value = "NewObjectA") + JNI.NewObjectA getNewObjectA(); + + @CField(value = "GetStaticMethodID") + JNI.GetStaticMethodID getGetStaticMethodID(); + + @CField(value = "GetMethodID") + JNI.GetMethodID getGetMethodID(); + + @CField(value = "GetStaticFieldID") + JNI.GetStaticFieldID getGetStaticFieldID(); + + @CField(value = "GetFieldID") + JNI.GetFieldID getGetFieldID(); + + @CField(value = "CallStaticBooleanMethodA") + JNI.CallStaticBooleanMethodA getCallStaticBooleanMethodA(); + + @CField(value = "CallStaticIntMethodA") + JNI.CallStaticIntMethodA getCallStaticIntMethodA(); + + @CField(value = "CallStaticVoidMethodA") + JNI.CallStaticVoidMethodA getCallStaticVoidMethodA(); + + @CField(value = "CallStaticObjectMethodA") + JNI.CallStaticObjectMethodA getCallStaticObjectMethodA(); + + @CField(value = "CallStaticLongMethodA") + JNI.CallStaticLongMethodA getCallStaticLongMethodA(); + + @CField(value = "CallObjectMethodA") + JNI.CallObjectMethodA getCallObjectMethodA(); + + @CField(value = "CallVoidMethodA") + JNI.CallVoidMethodA getCallVoidMethodA(); + + @CField(value = "CallBooleanMethodA") + JNI.CallBooleanMethodA getCallBooleanMethodA(); + + @CField(value = "CallShortMethodA") + JNI.CallShortMethodA getCallShortMethodA(); + + @CField(value = "CallIntMethodA") + JNI.CallIntMethodA getCallIntMethodA(); + + @CField(value = "CallLongMethodA") + JNI.CallLongMethodA getCallLongMethodA(); + + @CField(value = "CallDoubleMethodA") + JNI.CallDoubleMethodA getCallDoubleMethodA(); + + @CField(value = "CallFloatMethodA") + JNI.CallFloatMethodA getCallFloatMethodA(); + + @CField(value = "CallByteMethodA") + JNI.CallByteMethodA getCallByteMethodA(); + + @CField(value = "CallCharMethodA") + JNI.CallCharMethodA getCallCharMethodA(); + + @CField(value = "GetStaticObjectField") + JNI.GetStaticObjectField getGetStaticObjectField(); + + @CField(value = "GetIntField") + JNI.GetIntField getGetIntField(); + + @CField(value = "GetStaticBooleanField") + JNI.GetStaticBooleanField getGetStaticBooleanField(); + + @CField(value = "SetStaticBooleanField") + JNI.SetStaticBooleanField getSetStaticBooleanField(); + + @CField(value = "ExceptionCheck") + JNI.ExceptionCheck getExceptionCheck(); + + @CField(value = "ExceptionOccurred") + JNI.ExceptionOccurred getExceptionOccurred(); + + @CField(value = "ExceptionClear") + JNI.ExceptionClear getExceptionClear(); + + @CField(value = "ExceptionDescribe") + JNI.ExceptionDescribe getExceptionDescribe(); + + @CField(value = "Throw") + JNI.Throw getThrow(); + + @CField(value = "GetObjectRefType") + JNI.GetObjectRefType getGetObjectRefType(); + + @CField(value = "GetDirectBufferAddress") + JNI.GetDirectBufferAddress getGetDirectBufferAddress(); + + @CField(value = "IsInstanceOf") + JNI.IsInstanceOf getIsInstanceOf(); + + @CField(value = "GetJavaVM") + JNI.GetJavaVM getGetJavaVM(); +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JVM.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JVM.java new file mode 100644 index 000000000000..822fc0edccd1 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/JVM.java @@ -0,0 +1,134 @@ +package org.enso.os.environment.jni; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import org.enso.common.Platform; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.UnmanagedMemory; +import org.graalvm.nativeimage.c.struct.SizeOf; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.WordFactory; + +/** Represents a JVM inside of current process. */ +public final class JVM { + private final JNIBoot.JNICreateJavaVMPointer createJvmFn; + private final String[] options; + private JNI.JNIEnv env = WordFactory.nullPointer(); + + JVM(JNIBoot.JNICreateJavaVMPointer factory, String[] options) { + this.createJvmFn = factory; + this.options = options; + } + + /** + * Create new JVM.Use {@link #env()} to obtain reference to JNI interface and make calls into the + * JVM. + * + * @param javaHome path where the JDK is installed + * @param options parameters to pass to the JVM + * @return new instance of the JVM + */ + public static JVM create(File javaHome, String... options) { + var createJvmFn = + switch (Platform.getOperatingSystem()) { + case WINDOWS -> WindowsJVM.createImpl(javaHome); + case LINUX, MACOS -> PosixJVM.createImpl(javaHome); + }; + + var jvmArgs = new ArrayList(); + + // java.home + jvmArgs.add("-Djava.home=" + javaHome); + + var jvmOptions = System.getenv("JAVA_OPTS"); + if (jvmOptions != null) { + for (var op : jvmOptions.split(" ")) { + if (op.isEmpty()) { + continue; + } + jvmArgs.add(op); + } + } + jvmArgs.addAll(Arrays.asList(options)); + return new JVM(createJvmFn, jvmArgs.toArray(new String[0])); + } + + /** + * Executes main method of provided class + * + * @param classNameWithSlashes class (with `/` as separators) to search main method in + * @param args arguments to pass to the main method + */ + public void executeMain(String classNameWithSlashes, String... args) { + var e = env(); + try (var className = CTypeConversion.toCString(classNameWithSlashes); + var mainName = CTypeConversion.toCString("main"); + var stringName = CTypeConversion.toCString("java/lang/String"); + var mainSig = CTypeConversion.toCString("([Ljava/lang/String;)V"); ) { + var fn = e.getFunctions(); + var mainClazz = fn.getFindClass().call(e, className.get()); + assert mainClazz.isNonNull() : "Class not found " + classNameWithSlashes; + var mainMethod = fn.getGetStaticMethodID().call(e, mainClazz, mainName.get(), mainSig.get()); + assert mainMethod.isNonNull() : "main method found in " + classNameWithSlashes; + var stringClazz = fn.getFindClass().call(e, stringName.get()); + var argsCopy = + fn.getNewObjectArray().call(e, args.length, stringClazz, WordFactory.nullPointer()); + + for (var i = 0; i < args.length; i++) { + try (var ithArg = CTypeConversion.toCString(args[i]); ) { + var str = fn.getNewStringUTF().call(e, ithArg.get()); + fn.getSetObjectArrayElement().call(e, argsCopy, i, str); + } + } + var arg = StackValue.get(JNI.JValue.class); + arg.setJObject(argsCopy); + fn.getCallStaticVoidMethodA().call(e, mainClazz, mainMethod, arg); + } + } + + /** + * Initialize or just obtain environment associated with this JVM. + * + * @return JNI environment to make calls into the JVM + */ + final JNI.JNIEnv env() { + if (env.isNull()) { + env = initializeEnv(); + } + return env; + } + + private synchronized JNI.JNIEnv initializeEnv() { + var jvmArgs = StackValue.get(JNIBoot.Args.class); + var optionsCount = options.length; + jvmArgs.nOptions(optionsCount); + var sizeOfOption = SizeOf.get(JNIBoot.Option.class); + JNIBoot.Option jvmOpts = UnmanagedMemory.calloc(optionsCount * sizeOfOption); + var holder = new CTypeConversion.CCharPointerHolder[optionsCount]; + for (var i = 0; i < optionsCount; i++) { + holder[i] = CTypeConversion.toCString(options[i]); + var nth = jvmOpts.addressOf(i); + nth.setOptionString(holder[i].get()); + nth.setExtraInfo(WordFactory.nullPointer()); + } + jvmArgs.options(jvmOpts); + jvmArgs.version(JNI.JNI_VERSION_10()); + jvmArgs.ignoreUnrecognized(false); + + var jvmPtr = StackValue.get(JNI.JavaVMPointer.class); + var envPtr = StackValue.get(JNI.JNIEnvPointer.class); + + int res = createJvmFn.call(jvmPtr, envPtr, jvmArgs); + if (res != 0) { + throw new AssertionError("Error creating JVM: " + res); + } + + for (var i = 0; i < optionsCount; i++) { + holder[i].close(); + } + UnmanagedMemory.free(jvmOpts); + + return envPtr.readJNIEnv(); + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/PosixJVM.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/PosixJVM.java new file mode 100644 index 000000000000..fa3c3a99fcc3 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/PosixJVM.java @@ -0,0 +1,73 @@ +package org.enso.os.environment.jni; + +import java.io.File; +import java.util.List; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.constant.CConstant; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.PointerBase; + +@CContext(PosixJVM.Direct.class) +final class PosixJVM { + static JNIBoot.JNICreateJavaVMPointer createImpl(File javaHome) { + var libJvmPath = findDynamicLibrary(javaHome).getPath(); + try (var libPath = CTypeConversion.toCString(libJvmPath); + var createJvm = CTypeConversion.toCString("JNI_CreateJavaVM")) { + var jvmSo = dlopen(libPath.get(), RTLD_NOW()); + if (jvmSo.isNull()) { + var err = new StringBuilder("Cannot load ").append(libJvmPath); + err.append(" error: ").append(CTypeConversion.toJavaString(dlerror())); + throw new AssertionError(err.toString()); + } + JNIBoot.JNICreateJavaVMPointer sym = dlsym(jvmSo, createJvm.get()); + if (sym.isNull()) { + throw new AssertionError("No such symbol found in " + libJvmPath); + } + return sym; + } + } + + private static File findDynamicLibrary(File javaHome) { + var libName = + switch (org.enso.common.Platform.getOperatingSystem()) { + case LINUX -> "libjvm.so"; + case MACOS -> "libjvm.dylib"; + case org.enso.common.Platform other -> throw new IllegalStateException( + "Unknown OS: " + other); + }; + var lib = new File(new File(new File(javaHome, "lib"), "server"), libName); + if (!lib.exists()) { + throw new IllegalStateException("Cannot find " + lib); + } + return lib; + } + + @CConstant + static native int RTLD_NOW(); + + @CFunction + static native PointerBase dlopen(CCharPointer file, int mode); + + @CFunction(transition = CFunction.Transition.NO_TRANSITION) + static native T dlsym(PointerBase handle, CCharPointer name); + + @CFunction + static native CCharPointer dlerror(); + + static final class Direct implements CContext.Directives { + + @Override + public boolean isInConfiguration() { + return Platform.includedIn(Platform.LINUX.class) + || Platform.includedIn(Platform.DARWIN.class); + } + + @Override + public List getHeaderFiles() { + return List.of(""); + } + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/TestMain.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/TestMain.java new file mode 100644 index 000000000000..a662efdc4948 --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/TestMain.java @@ -0,0 +1,28 @@ +package org.enso.os.environment.jni; + +import java.io.File; +import java.io.FileWriter; +import java.math.BigInteger; + +public final class TestMain { + private TestMain() {} + + public static void main(String... args) throws Exception { + var out = new File(args[0]); + var n = Integer.parseInt(args[1]); + try (java.io.FileWriter os = new FileWriter(out)) { + os.write(factorial(n).toString()); + } + } + + static BigInteger factorial(int n) { + var acc = BigInteger.valueOf(1); + for (; ; ) { + acc = acc.multiply(BigInteger.valueOf(n)); + if (--n == 0) { + break; + } + } + return acc; + } +} diff --git a/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/WindowsJVM.java b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/WindowsJVM.java new file mode 100644 index 000000000000..62c63a2ca6eb --- /dev/null +++ b/lib/java/os-environment/src/main/java/org/enso/os/environment/jni/WindowsJVM.java @@ -0,0 +1,59 @@ +package org.enso.os.environment.jni; + +import static org.graalvm.nativeimage.c.function.CFunction.Transition.NO_TRANSITION; + +import java.io.File; +import java.util.List; +import org.enso.os.environment.jni.JNIBoot.JNICreateJavaVMPointer; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.PointerBase; + +@CContext(WindowsJVM.Direct.class) +final class WindowsJVM { + static JNICreateJavaVMPointer createImpl(File javaHome) { + var dllPath = findDynamicLibrary(javaHome).getPath(); + + try (var libPath = CTypeConversion.toCString(dllPath); + var createJvm = CTypeConversion.toCString("JNI_CreateJavaVM")) { + var dll = LoadLibraryA(libPath.get()); + assert dll.isNonNull(); + return GetProcAddress(dll, createJvm.get()); + } + } + + private static File findDynamicLibrary(File javaHome) { + var dll = new File(new File(new File(javaHome, "bin"), "server"), "jvm.dll"); + if (!dll.exists()) { + throw new AssertionError("Cannot find " + dll); + } + return dll; + } + + /** Loads the specified module into the address space of the calling process. */ + @CFunction(transition = NO_TRANSITION) + static native HMODULE LoadLibraryA(CCharPointer lpLibFileName); + + @CFunction(transition = NO_TRANSITION) + static native T GetProcAddress(HMODULE hModule, CCharPointer lpProcName); + + /** Windows Module Handle type */ + interface HMODULE extends PointerBase {} + + @Platforms(Platform.WINDOWS.class) + static final class Direct implements CContext.Directives { + @Override + public final boolean isInConfiguration() { + return Platform.includedIn(Platform.WINDOWS.class); + } + + @Override + public final List getHeaderFiles() { + return List.of(""); + } + } +} diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/ListOfTests.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/ListOfTests.java index 1b1f909b2ddf..a9af8812fe14 100644 --- a/lib/java/os-environment/src/test/java/org/enso/os/environment/ListOfTests.java +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/ListOfTests.java @@ -9,6 +9,7 @@ private ListOfTests() {} List.of( "org.enso.os.environment.PlatformTest", "org.enso.os.environment.RandomUtilsTest", + "org.enso.os.environment.jni.LoadClassTest", "org.enso.os.environment.chdir.TestChangeDirectory", "org.enso.os.environment.directories.DirectoriesTest", "org.enso.os.environment.trash.TrashBinTest"); diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/PlatformTest.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/PlatformTest.java index 0967a30a9047..891c9d046eac 100644 --- a/lib/java/os-environment/src/test/java/org/enso/os/environment/PlatformTest.java +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/PlatformTest.java @@ -1,6 +1,8 @@ package org.enso.os.environment; import org.enso.common.Platform; +import org.enso.os.environment.directories.Directories; +import org.enso.os.environment.trash.TrashBin; import org.junit.Assert; import org.junit.Test; @@ -13,11 +15,11 @@ public void getOperatingSystem() { @Test public void getDirectories() { - Assert.assertNotNull(DesktopEnvironment.getDirectories()); + Assert.assertNotNull(Directories.getCurrent()); } @Test public void getTrashBin() { - Assert.assertNotNull(DesktopEnvironment.getTrashBin()); + Assert.assertNotNull(TrashBin.getCurrent()); } } diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/TestCollectorFeature.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/TestCollectorFeature.java index 1d244619cb21..1893822b878d 100644 --- a/lib/java/os-environment/src/test/java/org/enso/os/environment/TestCollectorFeature.java +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/TestCollectorFeature.java @@ -1,11 +1,16 @@ package org.enso.os.environment; +import java.io.File; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeReflection; public final class TestCollectorFeature implements Feature { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { + recordModulePath(access); + for (var testClass : ListOfTests.TEST_CLASSES) { var testClazz = access.findClassByName(testClass); if (testClazz == null) { @@ -21,4 +26,27 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { } System.err.println("Registered test classes for reflection: " + ListOfTests.TEST_CLASSES); } + + private static void recordModulePath(BeforeAnalysisAccess access) { + var bootClassName = "org.enso.os.environment.jni.LoadClassTest"; + var bootClass = access.findClassByName(bootClassName); + try { + var f = bootClass.getField("MODULE_PATH"); + var fromMp = access.getApplicationModulePath().stream().map((t) -> t.toFile().getPath()); + var fromCp = + access.getApplicationClassPath().stream() + .map((t) -> t.toFile().getPath()) + .filter( + p -> { + return !p.contains("frgaal") + && !p.contains("junit-interface") + && !p.contains("hamcrest") + && !p.contains("test-interface"); + }); + var allPath = Stream.concat(fromMp, fromCp).collect(Collectors.joining(File.pathSeparator)); + f.set(null, allPath); + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + } } diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/TestRunner.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/TestRunner.java index 353ace8954e4..de9555029c47 100644 --- a/lib/java/os-environment/src/test/java/org/enso/os/environment/TestRunner.java +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/TestRunner.java @@ -25,7 +25,7 @@ private static void printSummary(List results) { System.out.println("Number of test classes: " + results.size()); System.out.println("Number of tests failed: " + failedTests.size()); System.out.println("Number of tests ignored: " + ignoredTests); - System.out.println("Number of successfull tests: " + runTests); + System.out.println("Number of successful tests: " + runTests); var success = failedTests.isEmpty(); System.out.println("Test run successful: " + success); if (!success) { diff --git a/lib/java/os-environment/src/test/java/org/enso/os/environment/jni/LoadClassTest.java b/lib/java/os-environment/src/test/java/org/enso/os/environment/jni/LoadClassTest.java new file mode 100644 index 000000000000..fcb5b3c512f4 --- /dev/null +++ b/lib/java/os-environment/src/test/java/org/enso/os/environment/jni/LoadClassTest.java @@ -0,0 +1,121 @@ +package org.enso.os.environment.jni; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Files; +import java.util.Random; +import org.enso.os.environment.jni.JNI.JValue; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.junit.Test; + +public class LoadClassTest { + private static final String PATH = System.getProperty("java.home"); + // set from TestCollectorFeature + public static String MODULE_PATH; + + private static JVM impl; + + private static JVM jvm() { + if (impl == null) { + assert MODULE_PATH != null : "MODULE_PATH field must be set!"; + var path = new File(PATH); + assert path.isDirectory() : "Java home exists: " + path; + impl = + JVM.create( + path, + "--module-path=" + MODULE_PATH, + "-Djdk.module.main=org.enso.os.environment", + "-Dsay=Ahoj"); + } + return impl; + } + + private static JNI.JNIEnv env() { + return jvm().env(); + } + + @Test + public void invokeParseShortMethod() { + var env = env(); + assertTrue("JNI created", env.isNonNull()); + + var findClassFn = env.getFunctions().getFindClass(); + var getStaticMethodIDFn = env.getFunctions().getGetStaticMethodID(); + var newStringFn = env.getFunctions().getNewStringUTF(); + var callStaticMethodFn = env.getFunctions().getCallStaticIntMethodA(); + + try (var shortName = CTypeConversion.toCString("java/lang/Short"); + var valueOfName = CTypeConversion.toCString("parseShort"); + var valueOfSig = CTypeConversion.toCString("(Ljava/lang/String;)S"); + var toParse = CTypeConversion.toCString("345"); ) { + var Short = findClassFn.call(env, shortName.get()); + + assertTrue("Short class is loaded", Short.isNonNull()); + + var valueOf = getStaticMethodIDFn.call(env, Short, valueOfName.get(), valueOfSig.get()); + assertTrue("valueOf method found", valueOf.isNonNull()); + + var args = StackValue.get(JNI.JValue.class); + var str = newStringFn.call(env, toParse.get()); + args.setJObject(str); + var res = callStaticMethodFn.call(env, Short, valueOf, args); + assertEquals(345, res); + } + } + + @Test + public void setSystemProperty() { + var env = env(); + assertTrue("JNI created", env.isNonNull()); + + var findClassFn = env.getFunctions().getFindClass(); + var getStaticMethodIDFn = env.getFunctions().getGetStaticMethodID(); + var newStringFn = env.getFunctions().getNewStringUTF(); + var strLengthFn = env.getFunctions().getGetStringUTFLength(); + var strCharsFn = env.getFunctions().getGetStringUTFChars(); + var strReleaseFn = env.getFunctions().getReleaseStringUTFChars(); + var callStaticMethodFn = env.getFunctions().getCallStaticObjectMethodA(); + + try (var systemName = CTypeConversion.toCString("java/lang/System"); + var getPropertyName = CTypeConversion.toCString("getProperty"); + var getPropertySig = CTypeConversion.toCString("(Ljava/lang/String;)Ljava/lang/String;"); + var propName = CTypeConversion.toCString("say"); ) { + var System = findClassFn.call(env, systemName.get()); + + assertTrue("System class is loaded", System.isNonNull()); + + var valueOf = + getStaticMethodIDFn.call(env, System, getPropertyName.get(), getPropertySig.get()); + assertTrue("getProperty method found", valueOf.isNonNull()); + + var args = StackValue.get(JNI.JValue.class); + var str = newStringFn.call(env, propName.get()); + args.setJObject(str); + var res = (JNI.JString) callStaticMethodFn.call(env, System, valueOf, args); + assertTrue("There should be a property 'say' defined", res.isNonNull()); + var len = strLengthFn.call(env, res); + assertEquals("'Ahoj' has four letters", 4, len); + var valueFalse = StackValue.get(JValue.class); + valueFalse.setBoolean(false); + var chars = strCharsFn.call(env, res, valueFalse); + assertEquals("Ahoj", CTypeConversion.toJavaString(chars)); + strReleaseFn.call(env, res, chars); + } + } + + @Test + public void executeMainClass() throws Exception { + var out = File.createTempFile("check-main", ".log"); + var gen = new Random(); + for (var i = 0; i < 5; i++) { + var n = gen.nextInt(10000, 20000); + jvm().executeMain("org/enso/os/environment/jni/TestMain", out.getPath(), "" + n); + var content = Files.readString(out.toPath()); + assertEquals("Factorial of " + n + " is the same", TestMain.factorial(n).toString(), content); + out.delete(); + } + } +} diff --git a/lib/java/poi-wrapper/src/main/java/org/apache/poi/hssf/util/HSSFColor.java b/lib/java/poi-wrapper/src/main/java/org/apache/poi/hssf/util/HSSFColor.java new file mode 100644 index 000000000000..e7e6f75bdc4d --- /dev/null +++ b/lib/java/poi-wrapper/src/main/java/org/apache/poi/hssf/util/HSSFColor.java @@ -0,0 +1,379 @@ +package org.apache.poi.hssf.util; + +/* ==================================================================== + 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. +==================================================================== */ + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import org.apache.poi.ss.usermodel.Color; + +/** + * Intends to provide support for the very evil index to triplet issue and will likely replace the + * color constants interface for HSSF 2.0. This class contains static inner class members for + * representing colors. Each color has an index (for the standard palette in Excel (tm) ), native + * (RGB) triplet and string triplet. The string triplet is as the color would be represented by + * Gnumeric. Having (string) this here is a bit of a collision of function between HSSF and the + * HSSFSerializer but I think its a reasonable one in this case. + */ +public class HSSFColor implements Color { + + private static Map indexHash; + private static Map enumList; + + private final int index; + private final int index2; + private final int rgb; + + /** + * Predefined HSSFColors with their given palette index (and an optional 2nd index) + * + * @since POI 3.16 beta 2 + */ + public enum HSSFColorPredefined { + BLACK(0x08, -1, 0x000000), + BROWN(0x3C, -1, 0x993300), + OLIVE_GREEN(0x3B, -1, 0x333300), + DARK_GREEN(0x3A, -1, 0x003300), + DARK_TEAL(0x38, -1, 0x003366), + DARK_BLUE(0x12, 0x20, 0x000080), + INDIGO(0x3E, -1, 0x333399), + GREY_80_PERCENT(0x3F, -1, 0x333333), + ORANGE(0x35, -1, 0xFF6600), + DARK_YELLOW(0x13, -1, 0x808000), + GREEN(0x11, -1, 0x008000), + TEAL(0x15, 0x26, 0x008080), + BLUE(0x0C, 0x27, 0x0000FF), + BLUE_GREY(0x36, -1, 0x666699), + GREY_50_PERCENT(0x17, -1, 0x808080), + RED(0x0A, -1, 0xFF0000), + LIGHT_ORANGE(0x34, -1, 0xFF9900), + LIME(0x32, -1, 0x99CC00), + SEA_GREEN(0x39, -1, 0x339966), + AQUA(0x31, -1, 0x33CCCC), + LIGHT_BLUE(0x30, -1, 0x3366FF), + VIOLET(0x14, 0x24, 0x800080), + GREY_40_PERCENT(0x37, -1, 0x969696), + PINK(0x0E, 0x21, 0xFF00FF), + GOLD(0x33, -1, 0xFFCC00), + YELLOW(0x0D, 0x22, 0xFFFF00), + BRIGHT_GREEN(0x0B, -1, 0x00FF00), + TURQUOISE(0x0F, 0x23, 0x00FFFF), + DARK_RED(0x10, 0x25, 0x800000), + SKY_BLUE(0x28, -1, 0x00CCFF), + PLUM(0x3D, 0x19, 0x993366), + GREY_25_PERCENT(0x16, -1, 0xC0C0C0), + ROSE(0x2D, -1, 0xFF99CC), + LIGHT_YELLOW(0x2B, -1, 0xFFFF99), + LIGHT_GREEN(0x2A, -1, 0xCCFFCC), + LIGHT_TURQUOISE(0x29, 0x1B, 0xCCFFFF), + PALE_BLUE(0x2C, -1, 0x99CCFF), + LAVENDER(0x2E, -1, 0xCC99FF), + WHITE(0x09, -1, 0xFFFFFF), + CORNFLOWER_BLUE(0x18, -1, 0x9999FF), + LEMON_CHIFFON(0x1A, -1, 0xFFFFCC), + MAROON(0x19, -1, 0x7F0000), + ORCHID(0x1C, -1, 0x660066), + CORAL(0x1D, -1, 0xFF8080), + ROYAL_BLUE(0x1E, -1, 0x0066CC), + LIGHT_CORNFLOWER_BLUE(0x1F, -1, 0xCCCCFF), + TAN(0x2F, -1, 0xFFCC99), + + /** + * Special Default/Normal/Automatic color. + * + *

Note: This class is NOT in the default Map returned by HSSFColor. The index is a + * special case which is interpreted in the various setXXXColor calls. + */ + AUTOMATIC(0x40, -1, 0x000000); + + private final HSSFColor color; + + HSSFColorPredefined(int index, int index2, int rgb) { + this.color = new HSSFColor(index, index2, rgb); + } + + /** + * @see HSSFColor#getIndex() + */ + public short getIndex() { + return color.getIndex(); + } + + /** + * @see HSSFColor#getIndex2() + */ + public short getIndex2() { + return color.getIndex2(); + } + + /** + * @see HSSFColor#getTriplet() + */ + public short[] getTriplet() { + return color.getTriplet(); + } + + /** + * @see HSSFColor#getHexString() + */ + public String getHexString() { + return color.getHexString(); + } + + /** + * @return (a copy of) the HSSFColor assigned to the enum + */ + public HSSFColor getColor() { + return new HSSFColor(getIndex(), getIndex2(), color.rgb); + } + } + + /** Creates a new instance of HSSFColor */ + public HSSFColor() { + // automatic index + this(0x40, -1, null); + } + + public HSSFColor(int index, int index2, java.awt.Color color) { + this.index = index; + this.index2 = index2; + if (color != null) throw new IllegalArgumentException("Unexpected color: " + color); + this.rgb = 0x000000; + } + + HSSFColor(int index, int index2, int rgb) { + this.index = index; + this.index2 = index2; + this.rgb = 0xff000000 | rgb; + } + + /** + * This function returns all the colours in an unmodifiable Map. The map is cached on first use. + * + * @return a Map containing all colours keyed by {@code Integer} excel-style palette indexes + */ + public static synchronized Map getIndexHash() { + if (indexHash == null) { + indexHash = Collections.unmodifiableMap(createColorsByIndexMap()); + } + + return indexHash; + } + + /** + * This function returns all the Colours, stored in a Map that can be edited. No caching is + * performed. If you don't need to edit the table, then call {@link #getIndexHash()} which returns + * a statically cached immutable map of colours. + */ + public static Map getMutableIndexHash() { + return createColorsByIndexMap(); + } + + private static Map createColorsByIndexMap() { + Map eList = mapEnumToColorClass(); + Map result = new HashMap<>(eList.size() * 3 / 2); + + for (Map.Entry colorRef : eList.entrySet()) { + Integer index1 = (int) colorRef.getKey().getIndex(); + if (!result.containsKey(index1)) { + result.put(index1, colorRef.getValue()); + } + Integer index2 = (int) colorRef.getKey().getIndex2(); + if (index2 != -1 && !result.containsKey(index2)) { + result.put(index2, colorRef.getValue()); + } + } + return result; + } + + /** + * this function returns all colors in a hastable. It's not implemented as a static + * member/statically initialized because that would be dirty in a server environment as it is + * intended. This means you'll eat the time it takes to create it once per request but you will + * not hold onto it if you have none of those requests. + * + * @return a Map containing all colors keyed by String gnumeric-like triplets + */ + public static Map getTripletHash() { + return createColorsByHexStringMap(); + } + + private static Map createColorsByHexStringMap() { + Map eList = mapEnumToColorClass(); + Map result = new HashMap<>(eList.size()); + + for (Map.Entry colorRef : eList.entrySet()) { + String hexString = colorRef.getKey().getHexString(); + if (!result.containsKey(hexString)) { + result.put(hexString, colorRef.getValue()); + } + } + return result; + } + + /** Maps the Enums to the HSSFColor, in cases of user code evaluating the classname */ + private static synchronized Map mapEnumToColorClass() { + if (enumList == null) { + enumList = new EnumMap<>(HSSFColorPredefined.class); + // AUTOMATIC is not add to list + addHSSFColorPredefined(HSSFColorPredefined.BLACK); + addHSSFColorPredefined(HSSFColorPredefined.BROWN); + addHSSFColorPredefined(HSSFColorPredefined.OLIVE_GREEN); + addHSSFColorPredefined(HSSFColorPredefined.DARK_GREEN); + addHSSFColorPredefined(HSSFColorPredefined.DARK_TEAL); + addHSSFColorPredefined(HSSFColorPredefined.DARK_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.INDIGO); + addHSSFColorPredefined(HSSFColorPredefined.GREY_80_PERCENT); + addHSSFColorPredefined(HSSFColorPredefined.ORANGE); + addHSSFColorPredefined(HSSFColorPredefined.DARK_YELLOW); + addHSSFColorPredefined(HSSFColorPredefined.GREEN); + addHSSFColorPredefined(HSSFColorPredefined.TEAL); + addHSSFColorPredefined(HSSFColorPredefined.BLUE); + addHSSFColorPredefined(HSSFColorPredefined.BLUE_GREY); + addHSSFColorPredefined(HSSFColorPredefined.GREY_50_PERCENT); + addHSSFColorPredefined(HSSFColorPredefined.RED); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_ORANGE); + addHSSFColorPredefined(HSSFColorPredefined.LIME); + addHSSFColorPredefined(HSSFColorPredefined.SEA_GREEN); + addHSSFColorPredefined(HSSFColorPredefined.AQUA); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.VIOLET); + addHSSFColorPredefined(HSSFColorPredefined.GREY_40_PERCENT); + addHSSFColorPredefined(HSSFColorPredefined.PINK); + addHSSFColorPredefined(HSSFColorPredefined.GOLD); + addHSSFColorPredefined(HSSFColorPredefined.YELLOW); + addHSSFColorPredefined(HSSFColorPredefined.BRIGHT_GREEN); + addHSSFColorPredefined(HSSFColorPredefined.TURQUOISE); + addHSSFColorPredefined(HSSFColorPredefined.DARK_RED); + addHSSFColorPredefined(HSSFColorPredefined.SKY_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.PLUM); + addHSSFColorPredefined(HSSFColorPredefined.GREY_25_PERCENT); + addHSSFColorPredefined(HSSFColorPredefined.ROSE); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_YELLOW); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_GREEN); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_TURQUOISE); + addHSSFColorPredefined(HSSFColorPredefined.PALE_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.LAVENDER); + addHSSFColorPredefined(HSSFColorPredefined.WHITE); + addHSSFColorPredefined(HSSFColorPredefined.CORNFLOWER_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.LEMON_CHIFFON); + addHSSFColorPredefined(HSSFColorPredefined.MAROON); + addHSSFColorPredefined(HSSFColorPredefined.ORCHID); + addHSSFColorPredefined(HSSFColorPredefined.CORAL); + addHSSFColorPredefined(HSSFColorPredefined.ROYAL_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.LIGHT_CORNFLOWER_BLUE); + addHSSFColorPredefined(HSSFColorPredefined.TAN); + } + return enumList; + } + + private static void addHSSFColorPredefined(HSSFColorPredefined color) { + enumList.put(color, color.getColor()); + } + + /** + * returns color standard palette index + * + * @return index to the standard palette + */ + public short getIndex() { + return (short) index; + } + + /** + * returns alternative color standard palette index + * + * @return alternative index to the standard palette, if -1 this index is not defined + */ + public short getIndex2() { + return (short) index2; + } + + private int getRed() { + return (rgb >> 16) & 0xFF; + } + + private int getGreen() { + return (rgb >> 8) & 0xFF; + } + + private int getBlue() { + return (rgb >> 0) & 0xFF; + } + + /** + * returns RGB triplet (0, 0, 0) + * + * @return triplet representation like that in Excel + */ + public short[] getTriplet() { + return new short[] {(short) getRed(), (short) getGreen(), (short) getBlue()}; + } + + /** + * returns colon-delimited hex string "0:0:0" + * + * @return a hex string exactly like a gnumeric triplet + */ + public String getHexString() { + return (Integer.toHexString(getRed() * 0x101) + + ":" + + Integer.toHexString(getGreen() * 0x101) + + ":" + + Integer.toHexString(getBlue() * 0x101)) + .toUpperCase(Locale.ROOT); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HSSFColor hssfColor = (HSSFColor) o; + + if (index != hssfColor.index) return false; + if (index2 != hssfColor.index2) return false; + return Objects.equals(rgb, hssfColor.rgb); + } + + @Override + public int hashCode() { + return Objects.hash(rgb, index, index2); + } + + /** + * Checked type cast {@code color} to an HSSFColor. + * + * @param color the color to type cast + * @return the type casted color + * @throws IllegalArgumentException if color is null or is not an instance of HSSFColor + */ + public static HSSFColor toHSSFColor(Color color) { + // FIXME: this method would be more useful if it could convert any Color to an HSSFColor + // Currently the only benefit of this method is to throw an IllegalArgumentException + // instead of a ClassCastException. + if (color != null && !(color instanceof HSSFColor)) { + throw new IllegalArgumentException( + "Only HSSFColor objects are supported, but had " + color.getClass()); + } + return (HSSFColor) color; + } +} diff --git a/lib/java/poi-wrapper/src/main/java/org/apache/poi/ss/usermodel/DataFormatter.java b/lib/java/poi-wrapper/src/main/java/org/apache/poi/ss/usermodel/DataFormatter.java new file mode 100644 index 000000000000..51595c0aa234 --- /dev/null +++ b/lib/java/poi-wrapper/src/main/java/org/apache/poi/ss/usermodel/DataFormatter.java @@ -0,0 +1,1397 @@ +package org.apache.poi.ss.usermodel; + +/* ==================================================================== + 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. + + 2012 - Alfresco Software, Ltd. + Alfresco Software has modified source of this file + The details of changes as svn diff can be found in svn at location root/projects/3rd-party/src +==================================================================== */ + +// +// Modified version of org.apache.poi:poi-ooxml:5.2.3 that avoids +// dependency on java.beans package from java.desktop module +// +// Remove once POI bug is fixed +// + +// import java.beans.PropertyChangeSupport; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.poi.ss.format.CellFormat; +import org.apache.poi.ss.format.CellFormatResult; +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.util.DateFormatConverter; +import org.apache.poi.ss.util.NumberToTextConverter; +import org.apache.poi.util.LocaleUtil; +import org.apache.poi.util.StringUtil; + +/** + * DataFormatter contains methods for formatting the value stored in a Cell. This can be useful for + * reports and GUI presentations when you need to display data exactly as it appears in Excel. + * Supported formats include currency, SSN, percentages, decimals, dates, phone numbers, zip codes, + * etc. + * + *

Internally, formats will be implemented using subclasses of {@link Format} such as {@link + * DecimalFormat} and {@link SimpleDateFormat}. Therefore the formats used by this class must obey + * the same pattern rules as these Format subclasses. This means that only legal number pattern + * characters ("0", "#", ".", "," etc.) may appear in number formats. Other characters can be + * inserted before or after the number pattern to form a prefix or suffix. + * + *

For example the Excel pattern {@code "$#,##0.00 "USD"_);($#,##0.00 "USD")" } will be correctly + * formatted as "$1,000.00 USD" or "($1,000.00 USD)". However the pattern {@code "00-00-00"} is + * incorrectly formatted by DecimalFormat as "000000--". For Excel formats that are not compatible + * with DecimalFormat, you can provide your own custom {@link Format} implementation via {@code + * DataFormatter.addFormat(String,Format)}. The following custom formats are already provided by + * this class: + * + *

{@code
+ * SSN "000-00-0000"
+ * Phone Number "(###) ###-####"
+ * Zip plus 4 "00000-0000"
+ * }
+ * + *

If the Excel format pattern cannot be parsed successfully, then a default format will be used. + * The default number format will mimic the Excel General format: "#" for whole numbers and + * "#.##########" for decimal numbers. You can override the default format pattern with {@code + * DataFormatter.setDefaultNumberFormat(Format)}. Note: the default format will only be used + * when a Format cannot be created from the cell's data format string. + * + *

Note that by default formatted numeric values are trimmed. Excel formats can contain spacers + * and padding and the default behavior is to strip them off. + * + *

Example: + * + *

Consider a numeric cell with a value {@code 12.343} and format {@code "##.##_ "}. The trailing + * underscore and space ("_ ") in the format adds a space to the end and Excel formats this cell as + * {@code "12.34 "}, but {@code DataFormatter} trims the formatted value and returns {@code + * "12.34"}. You can enable spaces by passing the {@code emulateCSV=true} flag in the {@code + * DateFormatter} cosntructor. If set to true, then the output tries to conform to what you get when + * you take an xls or xlsx in Excel and Save As CSV file: + * + *

    + *
  • returned values are not trimmed + *
  • Invalid dates are formatted as 255 pound signs ("#") + *
  • simulate Excel's handling of a format string of all # when the value is 0. Excel will + * output "", {@code DataFormatter} will output "0". + *
+ * + *

Some formats are automatically "localized" by Excel, eg show as mm/dd/yyyy when loaded in + * Excel in some Locales but as dd/mm/yyyy in others. These are always returned in the "default" + * (US) format, as stored in the file. Some format strings request an alternate locale, eg {@code + * [$-809]d/m/yy h:mm AM/PM} which explicitly requests UK locale. These locale directives are + * (currently) ignored. You can use {@link DateFormatConverter} to do some of this localisation if + * you need it. + */ +@SuppressWarnings("unused") +public class DataFormatter { + private static final String defaultFractionWholePartFormat = "#"; + private static final String defaultFractionFractionPartFormat = "#/##"; + + /** Pattern to find a number format: "0" or "#" */ + private static final Pattern numPattern = Pattern.compile("[0#]+"); + + /** Pattern to find days of week as text "ddd...." */ + private static final Pattern daysAsText = Pattern.compile("([d]{3,})", Pattern.CASE_INSENSITIVE); + + /** Pattern to find "AM/PM" marker */ + private static final Pattern amPmPattern = + Pattern.compile("(([AP])[M/P]*)", Pattern.CASE_INSENSITIVE); + + /** Pattern to find formats with condition ranges e.g. [>=100] */ + private static final Pattern rangeConditionalPattern = + Pattern.compile(".*\\[\\s*(>|>=|<|<=|=)\\s*[0-9]*\\.*[0-9].*"); + + /** + * A regex to find locale patterns like [$$-1009] and [$?-452]. Note that we don't currently + * process these into locales + */ + private static final Pattern localePatternGroup = Pattern.compile("(\\[\\$[^-\\]]*-[0-9A-Z]+])"); + + /** + * A regex to match the colour formatting's rules. Allowed colours are: Black, Blue, Cyan, Green, + * Magenta, Red, White, Yellow, "Color n" (1<=n<=56) + */ + private static final Pattern colorPattern = + Pattern.compile( + "(\\[BLACK])|(\\[BLUE])|(\\[CYAN])|(\\[GREEN])|" + + "(\\[MAGENTA])|(\\[RED])|(\\[WHITE])|(\\[YELLOW])|" + + "(\\[COLOR\\s*\\d])|(\\[COLOR\\s*[0-5]\\d])", + Pattern.CASE_INSENSITIVE); + + /** + * A regex to identify a fraction pattern. This requires that replaceAll("\\?", "#") has already + * been called + */ + private static final Pattern fractionPattern = + Pattern.compile("(?:([#\\d]+)\\s+)?(#+)\\s*/\\s*([#\\d]+)"); + + /** A regex to strip junk out of fraction formats */ + private static final Pattern fractionStripper = Pattern.compile("(\"[^\"]*\")|([^ ?#\\d/]+)"); + + /** A regex to detect if an alternate grouping character is used in a numeric format */ + private static final Pattern alternateGrouping = Pattern.compile("([#0]([^.#0])[#0]{3})"); + + /** + * Cells formatted with a date or time format and which contain invalid date or time values show + * 255 pound signs ("#"). + */ + private static final String invalidDateTimeString; + + static { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < 255; i++) buf.append('#'); + invalidDateTimeString = buf.toString(); + } + + /** The decimal symbols of the locale used for formatting values. */ + private DecimalFormatSymbols decimalSymbols; + + /** The date symbols of the locale used for formatting values. */ + private DateFormatSymbols dateSymbols; + + /** A default date format, if no date format was given */ + private DateFormat defaultDateformat; + + /** General format for numbers. */ + private Format generalNumberFormat; + + /** A default format to use when a number pattern cannot be parsed. */ + private Format defaultNumFormat; + + /** A map to cache formats. Map formats */ + private final Map formats = new HashMap<>(); + + /** whether CSV friendly adjustments should be made to the formatted text * */ + private boolean emulateCSV = false; + + /** + * whether years in dates should be displayed with 4 digits even if the formatString specifies + * only 2 * + */ + private boolean use4DigitYearsInAllDateFormats = false; + + /** + * if set to true, avoid recalculating the values if there is a cached value available (default is + * false) + */ + private boolean useCachedValuesForFormulaCells = false; + + /** stores the locale set by updateLocale method */ + private Locale locale; + + /** stores if the locale should change according to {@link LocaleUtil#getUserLocale()} */ + private boolean localeIsAdapting; + + // contain a support object instead of extending the support class + // private final PropertyChangeSupport pcs; + + /** For logging any problems we find */ + private static final Logger LOG = LogManager.getLogger(DataFormatter.class); + + /** Creates a formatter using the {@link Locale#getDefault() default locale}. */ + public DataFormatter() { + this(false); + } + + /** + * Creates a formatter using the {@link Locale#getDefault() default locale}. + * + * @param emulateCSV whether to emulate CSV output. + */ + public DataFormatter(boolean emulateCSV) { + this(LocaleUtil.getUserLocale(), true, emulateCSV); + } + + /** Creates a formatter using the given locale. */ + public DataFormatter(Locale locale) { + this(locale, false); + } + + /** + * Creates a formatter using the given locale. + * + * @param emulateCSV whether to emulate CSV output. + */ + public DataFormatter(Locale locale, boolean emulateCSV) { + this(locale, false, emulateCSV); + } + + /** + * Creates a formatter using the given locale. + * + * @param localeIsAdapting (true only if locale is not user-specified) + * @param emulateCSV whether to emulate CSV output. + */ + public DataFormatter(Locale locale, boolean localeIsAdapting, boolean emulateCSV) { + this.localeIsAdapting = false; + // pcs = new PropertyChangeSupport(this); + // localeIsAdapting must be true prior to this first checkForLocaleChange call. + // checkForLocaleChange(locale); + // set localeIsAdapting so subsequent checks perform correctly + // (whether a specific locale was provided to this DataFormatter or DataFormatter should + // adapt to the current user locale as the locale changes) + this.localeIsAdapting = localeIsAdapting; + this.emulateCSV = emulateCSV; + } + + /** + * @param emulateCSV whether to emulate CSV output (default false). + * @since POI 5.2.0 + */ + public void setEmulateCSV(boolean emulateCSV) { + this.emulateCSV = emulateCSV; + } + + /** + * @return whether to emulate CSV output (default false). + * @since POI 5.2.0 + */ + public boolean isEmulateCSV() { + return emulateCSV; + } + + /** + * @param useCachedValuesForFormulaCells if set to true, when you do not provide a {@link + * FormulaEvaluator}, for cells with formulas, we will return the cached value for the cell + * (if available), otherwise - we return the formula itself. The default is false and this + * means we return the formula itself. + * @since POI 5.2.0 + */ + public void setUseCachedValuesForFormulaCells(boolean useCachedValuesForFormulaCells) { + this.useCachedValuesForFormulaCells = useCachedValuesForFormulaCells; + } + + /** + * @return useCachedValuesForFormulaCells if set to true, when you do not provide a {@link + * FormulaEvaluator}, for cells with formulas, we will return the cached value for the cell + * (if available), otherwise - we return the formula itself. The default is false and this + * means we return the formula itself. + * @since POI 5.2.0 + */ + public boolean useCachedValuesForFormulaCells() { + return useCachedValuesForFormulaCells; + } + + /** + * @param use4DigitYearsInAllDateFormats set to true if you want to have all dates formatted with + * 4 digit years (even if the format associated with the cell specifies just 2) + * @since POI 5.2.0 + */ + public void setUse4DigitYearsInAllDateFormats(boolean use4DigitYearsInAllDateFormats) { + this.use4DigitYearsInAllDateFormats = use4DigitYearsInAllDateFormats; + } + + /** + * @return use4DigitYearsInAllDateFormats set to true if you want to have all dates formatted with + * 4 digit years (even if the format associated with the cell specifies just 2) + * @since POI 5.2.0 + */ + public boolean use4DigitYearsInAllDateFormats() { + return use4DigitYearsInAllDateFormats; + } + + /** + * Return a Format for the given cell if one exists, otherwise try to create one. This method will + * return {@code null} if any of the following is true: + * + *

    + *
  • the cell's style is null + *
  • the style's data format string is null or empty + *
  • the format string cannot be recognized as either a number or date + *
+ * + * @param cell The cell to retrieve a Format for + * @return A Format for the format String + */ + private Format getFormat(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { + if (cell == null) return null; + + ExcelNumberFormat numFmt = ExcelNumberFormat.from(cell, cfEvaluator); + + if (numFmt == null) { + return null; + } + + int formatIndex = numFmt.getIdx(); + String formatStr = numFmt.getFormat(); + if (StringUtil.isBlank(formatStr)) { + return null; + } + return getFormat(cell.getNumericCellValue(), formatIndex, formatStr, isDate1904(cell)); + } + + private boolean isDate1904(Cell cell) { + if (cell != null && cell.getSheet().getWorkbook() instanceof Date1904Support) { + return ((Date1904Support) cell.getSheet().getWorkbook()).isDate1904(); + } + return false; + } + + private Format getFormat( + double cellValue, int formatIndex, String formatStrIn, boolean use1904Windowing) { + checkForLocaleChange(); + + // Might be better to separate out the n p and z formats, falling back to p when n and z are not + // set. + // That however would require other code to be re factored. + // String[] formatBits = formatStrIn.split(";"); + // int i = cellValue > 0.0 ? 0 : cellValue < 0.0 ? 1 : 2; + // String formatStr = (i < formatBits.length) ? formatBits[i] : formatBits[0]; + + // this replace is done to fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63211 + String formatStr = formatStrIn.replace("\\%", "\'%\'"); + + // Excel supports 2+ part conditional data formats, eg positive/negative/zero, + // or (>1000),(>0),(0),(negative). As Java doesn't handle these kinds + // of different formats for different ranges, just +ve/-ve, we need to + // handle these ourselves in a special way. + // For now, if we detect 2+ parts, we call out to CellFormat to handle it + // TODO Going forward, we should really merge the logic between the two classes + if (formatStr.contains(";") + && (formatStr.indexOf(';') != formatStr.lastIndexOf(';') + || rangeConditionalPattern.matcher(formatStr).matches())) { + try { + // Ask CellFormat to get a formatter for it + CellFormat cfmt = CellFormat.getInstance(locale, formatStr); + // CellFormat requires callers to identify date vs not, so do so + // don't try to handle Date value 0, let a 3 or 4-part format take care of it + Object cellValueO = + (cellValue != 0.0 && DateUtil.isADateFormat(formatIndex, formatStr)) + ? DateUtil.getJavaDate(cellValue, use1904Windowing) + : cellValue; + // Wrap and return (non-cacheable - CellFormat does that) + return new CellFormatResultWrapper(cfmt.apply(cellValueO)); + } catch (Exception e) { + LOG.atWarn() + .withThrowable(e) + .log("Formatting failed for format {}, falling back", formatStr); + } + } + + // Excel's # with value 0 will output empty where Java will output 0. This hack removes the # + // from the format. + if (emulateCSV && cellValue == 0.0 && formatStr.contains("#") && !formatStr.contains("0")) { + formatStr = formatStr.replace("#", ""); + } + + // See if we already have it cached + Format format = formats.get(formatStr); + if (format != null) { + return format; + } + + // Is it one of the special built in types, General or @? + if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { + return generalNumberFormat; + } + + // Build a formatter, and cache it + format = createFormat(cellValue, formatIndex, formatStr); + formats.put(formatStr, format); + return format; + } + + /** + * Create and return a Format based on the format string from a cell's style. If the pattern + * cannot be parsed, return a default pattern. + * + * @param cell The Excel cell + * @return A Format representing the excel format. May return null. + */ + public Format createFormat(Cell cell) { + + int formatIndex = cell.getCellStyle().getDataFormat(); + String formatStr = cell.getCellStyle().getDataFormatString(); + return createFormat(cell.getNumericCellValue(), formatIndex, formatStr); + } + + private Format createFormat(double cellValue, int formatIndex, String sFormat) { + checkForLocaleChange(); + + String formatStr = sFormat; + + // Remove colour formatting if present + if (formatStr != null) { + Matcher colourM = colorPattern.matcher(formatStr); + while (colourM.find()) { + String colour = colourM.group(); + + // Paranoid replacement... + int at = formatStr.indexOf(colour); + if (at == -1) break; + String nFormatStr = formatStr.substring(0, at) + formatStr.substring(at + colour.length()); + if (nFormatStr.equals(formatStr)) break; + + // Try again in case there's multiple + formatStr = nFormatStr; + colourM = colorPattern.matcher(formatStr); + } + } + + // Strip off the locale information, we use an instance-wide locale for everything + if (formatStr != null) { + Matcher m = localePatternGroup.matcher(formatStr); + while (m.find()) { + String match = m.group(); + String symbol = match.substring(match.indexOf('$') + 1, match.indexOf('-')); + if (symbol.indexOf('$') > -1) { + symbol = + symbol.substring(0, symbol.indexOf('$')) + + '\\' + + symbol.substring(symbol.indexOf('$')); + } + formatStr = m.replaceAll(symbol); + m = localePatternGroup.matcher(formatStr); + } + } + + // Check for special cases + if (StringUtil.isBlank(formatStr)) { + return getDefaultFormat(cellValue); + } + + if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { + return generalNumberFormat; + } + + if (DateUtil.isADateFormat(formatIndex, formatStr) && DateUtil.isValidExcelDate(cellValue)) { + return createDateFormat(formatStr, cellValue); + } + // Excel supports fractions in format strings, which Java doesn't + if (formatStr.contains("#/") || formatStr.contains("?/")) { + String[] chunks = formatStr.split(";"); + for (String chunk1 : chunks) { + String chunk = chunk1.replace("?", "#"); + Matcher matcher = fractionStripper.matcher(chunk); + chunk = matcher.replaceAll(" "); + chunk = chunk.replaceAll(" +", " "); + Matcher fractionMatcher = fractionPattern.matcher(chunk); + // take the first match + if (fractionMatcher.find()) { + String wholePart = + (fractionMatcher.group(1) == null) ? "" : defaultFractionWholePartFormat; + return new FractionFormat(wholePart, fractionMatcher.group(3)); + } + } + + // Strip custom text in quotes and escaped characters for now as it can cause performance + // problems in fractions. + // String strippedFormatStr = formatStr.replaceAll("\\\\ ", " ").replaceAll("\\\\.", + // "").replaceAll("\"[^\"]*\"", " ").replaceAll("\\?", "#"); + return new FractionFormat(defaultFractionWholePartFormat, defaultFractionFractionPartFormat); + } + + if (numPattern.matcher(formatStr).find()) { + return createNumberFormat(formatStr, cellValue); + } + + if (emulateCSV) { + return new ConstantStringFormat(cleanFormatForNumber(formatStr)); + } + // TODO - when does this occur? + return null; + } + + String adjustTo4DigitYearsIfConfigured(String format) { + if (use4DigitYearsInAllDateFormats) { + int ypos2 = format.indexOf("yy"); + if (ypos2 < 0) { + return format; + } else { + int ypos3 = format.indexOf("yyy"); + int ypos4 = format.indexOf("yyyy"); + if (ypos4 == ypos2) { + String part1 = format.substring(0, ypos2 + 4); + String part2 = format.substring(ypos2 + 4); + return part1 + adjustTo4DigitYearsIfConfigured(part2); + } else if (ypos3 == ypos2) { + return format; + } else { + String part1 = format.substring(0, ypos2 + 2); + String part2 = format.substring(ypos2 + 2); + return part1 + "yy" + adjustTo4DigitYearsIfConfigured(part2); + } + } + } + return format; + } + + private Format createDateFormat(String pFormatStr, double cellValue) { + String formatStr = adjustTo4DigitYearsIfConfigured(pFormatStr); + formatStr = formatStr.replace("\\-", "-"); + formatStr = formatStr.replace("\\,", ","); + formatStr = formatStr.replace("\\.", "."); // . is a special regexp char + formatStr = formatStr.replace("\\ ", " "); + formatStr = formatStr.replace("\\/", "/"); // weird: m\\/d\\/yyyy + formatStr = formatStr.replace(";@", ""); + formatStr = formatStr.replace("\"/\"", "/"); // "/" is escaped for no reason in: mm"/"dd"/"yyyy + formatStr = formatStr.replace("\"\"", "'"); // replace Excel quoting with Java style quoting + formatStr = formatStr.replace("\\T", "'T'"); // Quote the T is iso8601 style dates + + boolean hasAmPm = false; + Matcher amPmMatcher = amPmPattern.matcher(formatStr); + while (amPmMatcher.find()) { + formatStr = amPmMatcher.replaceAll("@"); + hasAmPm = true; + amPmMatcher = amPmPattern.matcher(formatStr); + } + formatStr = formatStr.replace('@', 'a'); + + Matcher dateMatcher = daysAsText.matcher(formatStr); + if (dateMatcher.find()) { + String match = dateMatcher.group(0).toUpperCase(Locale.ROOT).replace('D', 'E'); + formatStr = dateMatcher.replaceAll(match); + } + + // Convert excel date format to SimpleDateFormat. + // Excel uses lower and upper case 'm' for both minutes and months. + // From Excel help: + /* + The "m" or "mm" code must appear immediately after the "h" or"hh" + code or immediately before the "ss" code; otherwise, Microsoft + Excel displays the month instead of minutes." + */ + + StringBuilder sb = new StringBuilder(); + char[] chars = formatStr.toCharArray(); + boolean mIsMonth = true; + List ms = new ArrayList<>(); + boolean isElapsed = false; + for (int j = 0; j < chars.length; j++) { + char c = chars[j]; + if (c == '\'') { + sb.append(c); + j++; + + // skip until the next quote + while (j < chars.length) { + c = chars[j]; + sb.append(c); + if (c == '\'') { + break; + } + j++; + } + } else if (c == '[' && !isElapsed) { + isElapsed = true; + mIsMonth = false; + sb.append(c); + } else if (c == ']' && isElapsed) { + isElapsed = false; + sb.append(c); + } else if (isElapsed) { + if (c == 'h' || c == 'H') { + sb.append('H'); + } else if (c == 'm' || c == 'M') { + sb.append('m'); + } else if (c == 's' || c == 'S') { + sb.append('s'); + } else { + sb.append(c); + } + } else if (c == 'h' || c == 'H') { + mIsMonth = false; + if (hasAmPm) { + sb.append('h'); + } else { + sb.append('H'); + } + } else if (c == 'm' || c == 'M') { + if (mIsMonth) { + sb.append('M'); + ms.add(sb.length() - 1); + } else { + sb.append('m'); + } + } else if (c == 's' || c == 'S') { + sb.append('s'); + // if 'M' precedes 's' it should be minutes ('m') + for (int index : ms) { + if (sb.charAt(index) == 'M') { + sb.replace(index, index + 1, "m"); + } + } + mIsMonth = true; + ms.clear(); + } else if (Character.isLetter(c)) { + mIsMonth = true; + ms.clear(); + if (c == 'y' || c == 'Y') { + sb.append('y'); + } else if (c == 'd' || c == 'D') { + sb.append('d'); + } else { + sb.append(c); + } + } else { + if (Character.isWhitespace(c)) { + ms.clear(); + } + sb.append(c); + } + } + formatStr = sb.toString(); + + try { + return new ExcelStyleDateFormatter(formatStr, dateSymbols); + } catch (IllegalArgumentException iae) { + LOG.atDebug() + .withThrowable(iae) + .log("Formatting failed for format {}, falling back", formatStr); + // the pattern could not be parsed correctly, + // so fall back to the default number format + return getDefaultFormat(cellValue); + } + } + + private String cleanFormatForNumber(String formatStrIn) { + // this replace is done to fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63211 + String formatStr = formatStrIn.replace("\\%", "\'%\'"); + + StringBuilder sb = new StringBuilder(formatStr); + + if (emulateCSV) { + // Requested spacers with "_" are replaced by a single space. + // Full-column-width padding "*" are removed. + // Not processing fractions at this time. Replace ? with space. + // This matches CSV output. + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '_' || c == '*' || c == '?') { + if (i > 0 && sb.charAt((i - 1)) == '\\') { + // It's escaped, don't worry + continue; + } + if (c == '?') { + sb.setCharAt(i, ' '); + } else if (i < sb.length() - 1) { + // Remove the character we're supposed + // to match the space of / pad to the + // column width with + if (c == '_') { + sb.setCharAt(i + 1, ' '); + } else { + sb.deleteCharAt(i + 1); + } + // Remove the character too + sb.deleteCharAt(i); + i--; + } + } + } + } else { + // If they requested spacers, with "_", + // remove those as we don't do spacing + // If they requested full-column-width + // padding, with "*", remove those too + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '_' || c == '*') { + if (i > 0 && sb.charAt((i - 1)) == '\\') { + // It's escaped, don't worry + continue; + } + if (i < sb.length() - 1) { + // Remove the character we're supposed + // to match the space of / pad to the + // column width with + sb.deleteCharAt(i + 1); + } + // Remove the _ too + sb.deleteCharAt(i); + i--; + } + } + } + + // Now, handle the other aspects like + // quoting and scientific notation + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + // remove quotes and back slashes + if (c == '\\' || c == '"') { + sb.deleteCharAt(i); + i--; + + // for scientific/engineering notation + } else if ((c == '+' || c == '-') && i > 0 && sb.charAt(i - 1) == 'E') { + sb.deleteCharAt(i); + i--; + } + } + + return sb.toString(); + } + + private static class InternalDecimalFormatWithScale extends Format { + + private static final Pattern endsWithCommas = Pattern.compile("(,+)$"); + private final BigDecimal divider; + private static final BigDecimal ONE_THOUSAND = BigDecimal.valueOf(1000); + private final DecimalFormat df; + + private static String trimTrailingCommas(String s) { + return s.replaceAll(",+$", ""); + } + + public InternalDecimalFormatWithScale(String pattern, DecimalFormatSymbols symbols) { + df = new DecimalFormat(trimTrailingCommas(pattern), symbols); + setExcelStyleRoundingMode(df); + Matcher endsWithCommasMatcher = endsWithCommas.matcher(pattern); + if (endsWithCommasMatcher.find()) { + String commas = (endsWithCommasMatcher.group(1)); + BigDecimal temp = BigDecimal.ONE; + for (int i = 0; i < commas.length(); ++i) { + temp = temp.multiply(ONE_THOUSAND); + } + divider = temp; + } else { + divider = null; + } + } + + private Object scaleInput(Object obj) { + if (divider != null) { + if (obj instanceof BigDecimal) { + obj = ((BigDecimal) obj).divide(divider, RoundingMode.HALF_UP); + } else if (obj instanceof Double) { + obj = (Double) obj / divider.doubleValue(); + } else { + throw new UnsupportedOperationException(); + } + } + return obj; + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + obj = scaleInput(obj); + return df.format(obj, toAppendTo, pos); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + throw new UnsupportedOperationException(); + } + } + + private Format createNumberFormat(String formatStr, double cellValue) { + String format = cleanFormatForNumber(formatStr); + DecimalFormatSymbols symbols = decimalSymbols; + + // Do we need to change the grouping character? + // eg for a format like #'##0 which wants 12'345 not 12,345 + Matcher agm = alternateGrouping.matcher(format); + if (agm.find()) { + char grouping = agm.group(2).charAt(0); + // Only replace the grouping character if it is not the default + // grouping character for the US locale (',') in order to enable + // correct grouping for non-US locales. + if (grouping != ',') { + symbols = DecimalFormatSymbols.getInstance(locale); + + symbols.setGroupingSeparator(grouping); + String oldPart = agm.group(1); + String newPart = oldPart.replace(grouping, ','); + format = format.replace(oldPart, newPart); + } + } + + try { + return new InternalDecimalFormatWithScale(format, symbols); + } catch (IllegalArgumentException iae) { + LOG.atDebug() + .withThrowable(iae) + .log("Formatting failed for format {}, falling back", formatStr); + // the pattern could not be parsed correctly, + // so fall back to the default number format + return getDefaultFormat(cellValue); + } + } + + /** + * Returns a default format for a cell. + * + * @param cell The cell + * @return a default format + */ + public Format getDefaultFormat(Cell cell) { + return getDefaultFormat(cell.getNumericCellValue()); + } + + private Format getDefaultFormat(double cellValue) { + checkForLocaleChange(); + + // for numeric cells try user supplied default + if (defaultNumFormat != null) { + return defaultNumFormat; + + // otherwise use general format + } + return generalNumberFormat; + } + + /** Performs Excel-style date formatting, using the supplied Date and format */ + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private String performDateFormatting(Date d, Format dateFormat) { + Format df = dateFormat != null ? dateFormat : defaultDateformat; + synchronized (df) { + return df.format(d); + } + } + + /** + * Returns the formatted value of an Excel date as a {@code String} based on the cell's {@code + * DataFormat}. i.e. "Thursday, January 02, 2003" , "01/02/2003" , "02-Jan" , etc. + * + *

If any conditional format rules apply, the highest priority with a number format is used. If + * no rules contain a number format, or no rules apply, the cell's style format is used. If the + * style does not have a format, the default date format is applied. + * + * @param cell to format + * @param cfEvaluator ConditionalFormattingEvaluator (if available) + * @return Formatted value + */ + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + private String getFormattedDateString(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { + if (cell == null) { + return null; + } + Format dateFormat = getFormat(cell, cfEvaluator); + if (dateFormat == null) { + if (defaultDateformat == null) { + DateFormatSymbols sym = DateFormatSymbols.getInstance(LocaleUtil.getUserLocale()); + SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", sym); + sdf.setTimeZone(LocaleUtil.getUserTimeZone()); + dateFormat = sdf; + } else { + dateFormat = defaultNumFormat; + } + } + synchronized (dateFormat) { + if (dateFormat instanceof ExcelStyleDateFormatter) { + // Hint about the raw excel value + ((ExcelStyleDateFormatter) dateFormat).setDateToBeFormatted(cell.getNumericCellValue()); + } + Date d = cell.getDateCellValue(); + return performDateFormatting(d, dateFormat); + } + } + + /** + * Returns the formatted value of an Excel number as a {@code String} based on the cell's {@code + * DataFormat}. Supported formats include currency, percents, decimals, phone number, SSN, etc.: + * "61.54%", "$100.00", "(800) 555-1234". + * + *

Format comes from either the highest priority conditional format rule with a specified + * format, or from the cell style. + * + * @param cell The cell + * @param cfEvaluator if available, or null + * @return a formatted number string + */ + private String getFormattedNumberString(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { + if (cell == null) { + return null; + } + Format numberFormat = getFormat(cell, cfEvaluator); + double d = cell.getNumericCellValue(); + if (numberFormat == null) { + return Double.toString(d); + } + String formatted; + try { + // see https://github.com/apache/poi/pull/321 -- but this sometimes fails, thus the catch and + // retry + formatted = numberFormat.format(BigDecimal.valueOf(d)); + } catch (NumberFormatException nfe) { + formatted = numberFormat.format(d); + } + return formatted.replaceFirst("E(\\d)", "E+$1"); // to match Excel's E-notation + } + + /** + * Formats the given raw cell value, based on the supplied format index and string, according to + * excel style rules. + * + * @see #formatCellValue(Cell) + */ + public String formatRawCellContents(double value, int formatIndex, String formatString) { + return formatRawCellContents(value, formatIndex, formatString, false); + } + + /** + * Formats the given raw cell value, based on the supplied format index and string, according to + * excel style rules. + * + * @see #formatCellValue(Cell) + */ + public String formatRawCellContents( + double value, int formatIndex, String formatString, boolean use1904Windowing) { + checkForLocaleChange(); + + // Is it a date? + if (DateUtil.isADateFormat(formatIndex, formatString)) { + if (DateUtil.isValidExcelDate(value)) { + Format dateFormat = getFormat(value, formatIndex, formatString, use1904Windowing); + if (dateFormat instanceof ExcelStyleDateFormatter) { + // Hint about the raw excel value + ((ExcelStyleDateFormatter) dateFormat).setDateToBeFormatted(value); + } + Date d = DateUtil.getJavaDate(value, use1904Windowing); + return performDateFormatting(d, dateFormat); + } + // RK: Invalid dates are 255 #s. + if (emulateCSV) { + return invalidDateTimeString; + } + } + + // else Number + Format numberFormat = getFormat(value, formatIndex, formatString, use1904Windowing); + if (numberFormat == null) { + return String.valueOf(value); + } + + // When formatting 'value', double to text to BigDecimal produces more + // accurate results than double to Double in JDK8 (as compared to + // previous versions). However, if the value contains E notation, this + // would expand the values, which we do not want, so revert to + // original method. + String result; + final String textValue = NumberToTextConverter.toText(value); + if (textValue.indexOf('E') > -1) { + result = numberFormat.format(value); + } else { + result = numberFormat.format(new BigDecimal(textValue)); + } + + // If they requested a non-abbreviated Scientific format, + // and there's an E## (but not E-##), add the missing '+' for E+## + String fslc = formatString.toLowerCase(Locale.ROOT); + if ((fslc.contains("general") || fslc.contains("e+0")) + && result.contains("E") + && !result.contains("E-")) { + result = result.replaceFirst("E", "E+"); + } + return result; + } + + /** + * Returns the formatted value of a cell as a {@code String} regardless of the cell type. If the + * Excel format pattern cannot be parsed then the cell value will be formatted using a default + * format. + * + *

When passed a null or blank cell, this method will return an empty String (""). Formulas in + * formula type cells will not be evaluated. {@link #setUseCachedValuesForFormulaCells} controls + * how these cells are evaluated. + * + * @param cell The cell + * @return the formatted cell value as a String + * @see #setUseCachedValuesForFormulaCells(boolean) + * @see #formatCellValue(Cell, FormulaEvaluator) + * @see #formatCellValue(Cell, FormulaEvaluator, ConditionalFormattingEvaluator) + */ + public String formatCellValue(Cell cell) { + return formatCellValue(cell, null); + } + + /** + * Returns the formatted value of a cell as a {@code String} regardless of the cell type. If the + * Excel number format pattern cannot be parsed then the cell value will be formatted using a + * default format. + * + *

When passed a null or blank cell, this method will return an empty String (""). Formula + * cells will be evaluated using the given {@link FormulaEvaluator} if the evaluator is non-null. + * If the evaluator is null, then the formula String will be returned. The caller is responsible + * for setting the currentRow on the evaluator. + * + * @param cell The cell (can be null) + * @param evaluator The FormulaEvaluator (can be null) + * @return a string value of the cell + * @see #formatCellValue(Cell) + * @see #formatCellValue(Cell, FormulaEvaluator, ConditionalFormattingEvaluator) + */ + public String formatCellValue(Cell cell, FormulaEvaluator evaluator) { + return formatCellValue(cell, evaluator, null); + } + + /** + * Returns the formatted value of a cell as a {@code String} regardless of the cell type. If the + * Excel number format pattern cannot be parsed then the cell value will be formatted using a + * default format. + * + *

When passed a null or blank cell, this method will return an empty String (""). Formula + * cells will be evaluated using the given {@link FormulaEvaluator} if the evaluator is non-null. + * If the evaluator is null, then the formula String will be returned. The caller is responsible + * for setting the currentRow on the evaluator + * + *

When a ConditionalFormattingEvaluator is present, it is checked first to see if there is a + * number format to apply. If multiple rules apply, the last one is used. If no + * ConditionalFormattingEvaluator is present, no rules apply, or the applied rules do not define a + * format, the cell's style format is used. + * + *

The two evaluators should be from the same context, to avoid inconsistencies in cached + * values. + * + * @param cell The cell (can be null) + * @param evaluator The FormulaEvaluator (can be null) + * @param cfEvaluator ConditionalFormattingEvaluator (can be null) + * @return a string value of the cell + * @see #formatCellValue(Cell) + * @see #formatCellValue(Cell, FormulaEvaluator) + */ + public String formatCellValue( + Cell cell, FormulaEvaluator evaluator, ConditionalFormattingEvaluator cfEvaluator) { + checkForLocaleChange(); + + if (cell == null) { + return ""; + } + + CellType cellType = cell.getCellType(); + if (cellType == CellType.FORMULA) { + if (evaluator == null) { + if (useCachedValuesForFormulaCells) { + try { + cellType = cell.getCachedFormulaResultType(); + } catch (Exception e) { + return cell.getCellFormula(); + } + } else { + return cell.getCellFormula(); + } + } else { + cellType = evaluator.evaluateFormulaCell(cell); + } + } + switch (cellType) { + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell, cfEvaluator)) { + return getFormattedDateString(cell, cfEvaluator); + } + return getFormattedNumberString(cell, cfEvaluator); + + case STRING: + return cell.getRichStringCellValue().getString(); + + case BOOLEAN: + return cell.getBooleanCellValue() ? "TRUE" : "FALSE"; + case BLANK: + return ""; + case ERROR: + return FormulaError.forInt(cell.getErrorCellValue()).getString(); + default: + throw new RuntimeException("Unexpected celltype (" + cellType + ")"); + } + } + + /** + * Sets a default number format to be used when the Excel format cannot be parsed successfully. + * Note: This is a fall back for when an error occurs while parsing an Excel number format + * pattern. This will not affect cells with the General format. + * + *

The value that will be passed to the Format's format method (specified by {@code + * java.text.Format#format}) will be a double value from a numeric cell. Therefore the code in the + * format method should expect a {@code Number} value. + * + * @param format A Format instance to be used as a default + * @see Format#format + */ + public void setDefaultNumberFormat(Format format) { + for (Map.Entry entry : formats.entrySet()) { + if (entry.getValue() == generalNumberFormat) { + entry.setValue(format); + } + } + defaultNumFormat = format; + } + + /** + * Adds a new format to the available formats. + * + *

The value that will be passed to the Format's format method (specified by {@code + * java.text.Format#format}) will be a double value from a numeric cell. Therefore the code in the + * format method should expect a {@code Number} value. + * + * @param excelFormatStr The data format string + * @param format A Format instance + */ + public void addFormat(String excelFormatStr, Format format) { + formats.put(excelFormatStr, format); + } + + // Some custom formats + + /** + * @return a {@code DecimalFormat} with parseIntegerOnly set {@code true} + */ + private static DecimalFormat createIntegerOnlyFormat(String fmt) { + DecimalFormatSymbols dsf = DecimalFormatSymbols.getInstance(Locale.ROOT); + DecimalFormat result = new DecimalFormat(fmt, dsf); + result.setParseIntegerOnly(true); + return result; + } + + /** Enables excel style rounding mode (round half up) on the Decimal Format given. */ + public static void setExcelStyleRoundingMode(DecimalFormat format) { + setExcelStyleRoundingMode(format, RoundingMode.HALF_UP); + } + + /** + * Enables custom rounding mode on the given Decimal Format. + * + * @param format DecimalFormat + * @param roundingMode RoundingMode + */ + public static void setExcelStyleRoundingMode(DecimalFormat format, RoundingMode roundingMode) { + format.setRoundingMode(roundingMode); + } + + /** + * If the Locale has been changed via {@link LocaleUtil#setUserLocale(Locale)} the stored formats + * need to be refreshed. All formats which aren't originated from DataFormatter itself, i.e. all + * Formats added via {@link DataFormatter#addFormat(String, Format)} and {@link + * DataFormatter#setDefaultNumberFormat(Format)}, need to be added again. To notify callers, the + * returned {@link PropertyChangeSupport} should be used. The Locale in {@link + * #updateLocale(Locale)} is the new Locale. + * + * @return the listener object, where callers can register themselves + */ + // public PropertyChangeSupport getLocaleChangedObservable() { + // return pcs; + // } + + private void checkForLocaleChange() { + // checkForLocaleChange(LocaleUtil.getUserLocale()); + } + + /** + * Update formats when locale has been changed + * + * @param newLocale the new locale + */ + public void updateLocale(Locale newLocale) { + if (!localeIsAdapting || newLocale.equals(locale)) return; + + locale = newLocale; + + dateSymbols = DateFormatSymbols.getInstance(locale); + decimalSymbols = DecimalFormatSymbols.getInstance(locale); + generalNumberFormat = new ExcelGeneralNumberFormat(locale); + + // taken from Date.toString() + defaultDateformat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", dateSymbols); + defaultDateformat.setTimeZone(LocaleUtil.getUserTimeZone()); + + // init built-in formats + + formats.clear(); + Format zipFormat = ZipPlusFourFormat.instance; + addFormat("00000\\-0000", zipFormat); + addFormat("00000-0000", zipFormat); + + Format phoneFormat = PhoneFormat.instance; + // allow for format string variations + addFormat("[<=9999999]###\\-####;\\(###\\)\\ ###\\-####", phoneFormat); + addFormat("[<=9999999]###-####;(###) ###-####", phoneFormat); + addFormat("###\\-####;\\(###\\)\\ ###\\-####", phoneFormat); + addFormat("###-####;(###) ###-####", phoneFormat); + + Format ssnFormat = SSNFormat.instance; + addFormat("000\\-00\\-0000", ssnFormat); + addFormat("000-00-0000", ssnFormat); + } + + /** Format class for Excel's SSN format. This class mimics Excel's built-in SSN formatting. */ + @SuppressWarnings("serial") + private static final class SSNFormat extends Format { + public static final Format instance = new SSNFormat(); + private static final DecimalFormat df = createIntegerOnlyFormat("000000000"); + + private SSNFormat() { + // enforce singleton + } + + /** Format a number as an SSN */ + public static String format(Number num) { + String result = df.format(num); + return result.substring(0, 3) + '-' + result.substring(3, 5) + '-' + result.substring(5, 9); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number) obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel Zip + 4 format. This class mimics Excel's built-in formatting for Zip + + * 4. + */ + @SuppressWarnings("serial") + private static final class ZipPlusFourFormat extends Format { + public static final Format instance = new ZipPlusFourFormat(); + private static final DecimalFormat df = createIntegerOnlyFormat("000000000"); + + private ZipPlusFourFormat() { + // enforce singleton + } + + /** Format a number as Zip + 4 */ + public static String format(Number num) { + String result = df.format(num); + return result.substring(0, 5) + '-' + result.substring(5, 9); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number) obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel phone number format. This class mimics Excel's built-in phone number + * formatting. + */ + @SuppressWarnings("serial") + private static final class PhoneFormat extends Format { + public static final Format instance = new PhoneFormat(); + private static final DecimalFormat df = createIntegerOnlyFormat("##########"); + + private PhoneFormat() { + // enforce singleton + } + + /** Format a number as a phone number */ + public static String format(Number num) { + String result = df.format(num); + StringBuilder sb = new StringBuilder(); + String seg1, seg2, seg3; + int len = result.length(); + if (len <= 4) { + return result; + } + + seg3 = result.substring(len - 4, len); + seg2 = result.substring(Math.max(0, len - 7), len - 4); + seg1 = result.substring(Math.max(0, len - 10), Math.max(0, len - 7)); + + if (StringUtil.isNotBlank(seg1)) { + sb.append('(').append(seg1).append(") "); + } + if (StringUtil.isNotBlank(seg2)) { + sb.append(seg2).append('-'); + } + sb.append(seg3); + return sb.toString(); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number) obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class that does nothing and always returns a constant string. + * + *

This format is used to simulate Excel's handling of a format string of all # when the value + * is 0. Excel will output "", Java will output "0". + * + * @see DataFormatter#createFormat(double, int, String) + */ + @SuppressWarnings("serial") + private static final class ConstantStringFormat extends Format { + private static final DecimalFormat df = createIntegerOnlyFormat("##########"); + private final String str; + + public ConstantStringFormat(String s) { + str = s; + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(str); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Workaround until we merge {@link DataFormatter} with {@link CellFormat}. Constant, non-cachable + * wrapper around a {@link CellFormatResult} + */ + @SuppressWarnings("serial") + private final class CellFormatResultWrapper extends Format { + private final CellFormatResult result; + + private CellFormatResultWrapper(CellFormatResult result) { + this.result = result; + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + if (emulateCSV) { + return toAppendTo.append(result.text); + } else { + return toAppendTo.append(result.text.trim()); + } + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return null; // Not supported + } + } +} diff --git a/lib/java/poi-wrapper/src/main/java/org/apache/poi/util/GenericRecordJsonWriter.java b/lib/java/poi-wrapper/src/main/java/org/apache/poi/util/GenericRecordJsonWriter.java new file mode 100644 index 000000000000..e4f283274335 --- /dev/null +++ b/lib/java/poi-wrapper/src/main/java/org/apache/poi/util/GenericRecordJsonWriter.java @@ -0,0 +1,491 @@ +package org.apache.poi.util; + +/* + * ==================================================================== + * 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. + * ==================================================================== + */ + +import static org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM; + +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.poi.common.usermodel.GenericRecord; +import org.apache.poi.util.GenericRecordUtil.AnnotatedFlag; + +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) +@Beta +public class GenericRecordJsonWriter implements Closeable { + private static final String TABS; + private static final String ZEROS = "0000000000000000"; + private static final Pattern ESC_CHARS = Pattern.compile("[\"\\p{Cntrl}\\\\]"); + private static final String NL = System.getProperty("line.separator"); + + @FunctionalInterface + protected interface GenericRecordHandler { + /** + * Handler method + * + * @param record the parent record, applied via instance method reference + * @param name the name of the property + * @param object the value of the property + * @return {@code true}, if the element was handled and output produced, The provided methods + * can be overridden and a implementation can return {@code false}, if the element hasn't + * been written to the stream + */ + boolean print(GenericRecordJsonWriter record, String name, Object object); + } + + private static final List, GenericRecordHandler>> handler = new ArrayList<>(); + + static { + char[] t = new char[255]; + Arrays.fill(t, '\t'); + TABS = new String(t); + handler(String.class, GenericRecordJsonWriter::printObject); + handler(Number.class, GenericRecordJsonWriter::printNumber); + handler(Boolean.class, GenericRecordJsonWriter::printBoolean); + handler(List.class, GenericRecordJsonWriter::printList); + handler(GenericRecord.class, GenericRecordJsonWriter::printGenericRecord); + handler(AnnotatedFlag.class, GenericRecordJsonWriter::printAnnotatedFlag); + handler(byte[].class, GenericRecordJsonWriter::printBytes); + // handler(Point2D.class, GenericRecordJsonWriter::printPoint); + // handler(Dimension2D.class, GenericRecordJsonWriter::printDimension); + // handler(Rectangle2D.class, GenericRecordJsonWriter::printRectangle); + // handler(Path2D.class, GenericRecordJsonWriter::printPath); + // handler(AffineTransform.class, GenericRecordJsonWriter::printAffineTransform); + // handler(Color.class, GenericRecordJsonWriter::printColor); + // handler(BufferedImage.class, GenericRecordJsonWriter::printImage); + handler(Array.class, GenericRecordJsonWriter::printArray); + handler(Object.class, GenericRecordJsonWriter::printObject); + } + + private static void handler(Class c, GenericRecordHandler printer) { + handler.add(new AbstractMap.SimpleEntry<>(c, printer)); + } + + protected final AppendableWriter aw; + protected final PrintWriter fw; + protected int indent = 0; + protected boolean withComments = true; + protected int childIndex = 0; + + public GenericRecordJsonWriter(File fileName) throws IOException { + OutputStream os = + ("null".equals(fileName.getName())) ? NULL_OUTPUT_STREAM : new FileOutputStream(fileName); + aw = new AppendableWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); + fw = new PrintWriter(aw); + } + + public GenericRecordJsonWriter(Appendable buffer) { + aw = new AppendableWriter(buffer); + fw = new PrintWriter(aw); + } + + public static String marshal(GenericRecord record) { + return marshal(record, true); + } + + public static String marshal(GenericRecord record, boolean withComments) { + final StringBuilder sb = new StringBuilder(); + try (GenericRecordJsonWriter w = new GenericRecordJsonWriter(sb)) { + w.setWithComments(withComments); + w.write(record); + return sb.toString(); + } catch (IOException e) { + return "{}"; + } + } + + public void setWithComments(boolean withComments) { + this.withComments = withComments; + } + + @Override + public void close() throws IOException { + fw.close(); + } + + protected String tabs() { + return TABS.substring(0, Math.min(indent, TABS.length())); + } + + public void write(GenericRecord record) { + final String tabs = tabs(); + Enum type = record.getGenericRecordType(); + String recordName = (type != null) ? type.name() : record.getClass().getSimpleName(); + fw.append(tabs); + fw.append("{"); + if (withComments) { + fw.append(" /* "); + fw.append(recordName); + if (childIndex > 0) { + fw.append(" - index: "); + fw.print(childIndex); + } + fw.append(" */"); + } + fw.println(); + + boolean hasProperties = writeProperties(record); + fw.println(); + + writeChildren(record, hasProperties); + + fw.append(tabs); + fw.append("}"); + } + + protected boolean writeProperties(GenericRecord record) { + Map> prop = record.getGenericProperties(); + if (prop == null || prop.isEmpty()) { + return false; + } + + final int oldChildIndex = childIndex; + childIndex = 0; + long cnt = prop.entrySet().stream().filter(e -> writeProp(e.getKey(), e.getValue())).count(); + childIndex = oldChildIndex; + + return cnt > 0; + } + + protected boolean writeChildren(GenericRecord record, boolean hasProperties) { + List list = record.getGenericChildren(); + if (list == null || list.isEmpty()) { + return false; + } + + indent++; + aw.setHoldBack(tabs() + (hasProperties ? ", " : "") + "\"children\": [" + NL); + final int oldChildIndex = childIndex; + childIndex = 0; + long cnt = list.stream().filter(l -> writeValue(null, l) && ++childIndex > 0).count(); + childIndex = oldChildIndex; + aw.setHoldBack(null); + + if (cnt > 0) { + fw.println(); + fw.println(tabs() + "]"); + } + indent--; + + return cnt > 0; + } + + public void writeError(String errorMsg) { + fw.append("{ error: "); + printObject("error", errorMsg); + fw.append(" }"); + } + + protected boolean writeProp(String name, Supplier value) { + final boolean isNext = (childIndex > 0); + aw.setHoldBack(isNext ? NL + tabs() + "\t, " : tabs() + "\t "); + final int oldChildIndex = childIndex; + childIndex = 0; + boolean written = writeValue(name, value.get()); + childIndex = oldChildIndex + (written ? 1 : 0); + aw.setHoldBack(null); + return written; + } + + protected boolean writeValue(String name, Object o) { + if (childIndex > 0) { + aw.setHoldBack(","); + } + + GenericRecordHandler grh = + (o == null) + ? GenericRecordJsonWriter::printNull + : handler.stream() + .filter(h -> matchInstanceOrArray(h.getKey(), o)) + .findFirst() + .map(Map.Entry::getValue) + .orElse(null); + + boolean result = grh != null && grh.print(this, name, o); + aw.setHoldBack(null); + return result; + } + + protected static boolean matchInstanceOrArray(Class key, Object instance) { + return key.isInstance(instance) || (Array.class.equals(key) && instance.getClass().isArray()); + } + + protected void printName(String name) { + fw.print(name != null ? "\"" + name + "\": " : ""); + } + + protected boolean printNull(String name, Object o) { + printName(name); + fw.write("null"); + return true; + } + + @SuppressWarnings("java:S3516") + protected boolean printNumber(String name, Object o) { + Number n = (Number) o; + printName(name); + + if (o instanceof Float) { + fw.print(n.floatValue()); + return true; + } else if (o instanceof Double) { + fw.print(n.doubleValue()); + return true; + } + + fw.print(n.longValue()); + + final int size; + if (n instanceof Byte) { + size = 2; + } else if (n instanceof Short) { + size = 4; + } else if (n instanceof Integer) { + size = 8; + } else if (n instanceof Long) { + size = 16; + } else { + size = -1; + } + + long l = n.longValue(); + if (withComments && size > 0 && (l < 0 || l > 9)) { + fw.write(" /* 0x"); + fw.write(trimHex(l, size)); + fw.write(" */"); + } + return true; + } + + protected boolean printBoolean(String name, Object o) { + printName(name); + fw.write(((Boolean) o).toString()); + return true; + } + + protected boolean printList(String name, Object o) { + printName(name); + fw.println("["); + int oldChildIndex = childIndex; + childIndex = 0; + ((List) o) + .forEach( + e -> { + writeValue(null, e); + childIndex++; + }); + childIndex = oldChildIndex; + fw.write(tabs() + "\t]"); + return true; + } + + protected boolean printGenericRecord(String name, Object o) { + printName(name); + this.indent++; + write((GenericRecord) o); + this.indent--; + return true; + } + + protected boolean printAnnotatedFlag(String name, Object o) { + printName(name); + AnnotatedFlag af = (AnnotatedFlag) o; + fw.print(af.getValue().get().longValue()); + if (withComments) { + fw.write(" /* "); + fw.write(af.getDescription()); + fw.write(" */ "); + } + return true; + } + + protected boolean printBytes(String name, Object o) { + printName(name); + fw.write('"'); + fw.write(Base64.getEncoder().encodeToString((byte[]) o)); + fw.write('"'); + return true; + } + + protected boolean printPoint(String name, Object o) { + throw new NoClassDefFoundError("Point2D"); + } + + protected boolean printDimension(String name, Object o) { + throw new NoClassDefFoundError("Dimension2D"); + } + + protected boolean printRectangle(String name, Object o) { + throw new NoClassDefFoundError("Rectangle2D"); + } + + protected boolean printPath(String name, Object o) { + throw new NoClassDefFoundError("Path2D"); + } + + protected boolean printObject(String name, Object o) { + printName(name); + fw.write('"'); + + final String str = o.toString(); + final Matcher m = ESC_CHARS.matcher(str); + int pos = 0; + while (m.find()) { + fw.append(str, pos, m.start()); + String match = m.group(); + switch (match) { + case "\n": + fw.write("\\\\n"); + break; + case "\r": + fw.write("\\\\r"); + break; + case "\t": + fw.write("\\\\t"); + break; + case "\b": + fw.write("\\\\b"); + break; + case "\f": + fw.write("\\\\f"); + break; + case "\\": + fw.write("\\\\\\\\"); + break; + case "\"": + fw.write("\\\\\""); + break; + default: + fw.write("\\\\u"); + fw.write(trimHex(match.charAt(0), 4)); + break; + } + pos = m.end(); + } + fw.append(str, pos, str.length()); + fw.write('"'); + return true; + } + + protected boolean printAffineTransform(String name, Object o) { + throw new NoClassDefFoundError("AffineTransform"); + } + + protected boolean printColor(String name, Object o) { + throw new NoClassDefFoundError("Color"); + } + + protected boolean printArray(String name, Object o) { + printName(name); + fw.write("["); + int length = Array.getLength(o); + final int oldChildIndex = childIndex; + for (childIndex = 0; childIndex < length; childIndex++) { + writeValue(null, Array.get(o, childIndex)); + } + childIndex = oldChildIndex; + fw.write(tabs() + "\t]"); + return true; + } + + protected boolean printImage(String name, Object o) { + throw new NoClassDefFoundError("BufferedImage"); + } + + static String trimHex(final long l, final int size) { + final String b = Long.toHexString(l); + int len = b.length(); + return ZEROS.substring(0, Math.max(0, size - len)) + b.substring(Math.max(0, len - size), len); + } + + static class AppendableWriter extends Writer { + private final Appendable appender; + private final Writer writer; + private String holdBack; + + AppendableWriter(Appendable buffer) { + super(buffer); + this.appender = buffer; + this.writer = null; + } + + AppendableWriter(Writer writer) { + super(writer); + this.appender = null; + this.writer = writer; + } + + void setHoldBack(String holdBack) { + this.holdBack = holdBack; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (holdBack != null) { + if (appender != null) { + appender.append(holdBack); + } else if (writer != null) { + writer.write(holdBack); + } + holdBack = null; + } + + if (appender != null) { + appender.append(String.valueOf(cbuf), off, len); + } else if (writer != null) { + writer.write(cbuf, off, len); + } + } + + @Override + public void flush() throws IOException { + Object o = (appender != null) ? appender : writer; + if (o instanceof Flushable) { + ((Flushable) o).flush(); + } + } + + @Override + public void close() throws IOException { + flush(); + Object o = (appender != null) ? appender : writer; + if (o instanceof Closeable) { + ((Closeable) o).close(); + } + } + } +} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration/StorageConfig.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration/StorageConfig.scala index a18e17dca3f7..2270a080a0f2 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration/StorageConfig.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration/StorageConfig.scala @@ -1,6 +1,6 @@ package org.enso.projectmanager.boot.configuration -import org.enso.os.environment.DesktopEnvironment +import org.enso.os.environment.directories.Directories import java.io.{File, IOException} @@ -21,7 +21,7 @@ case class StorageConfig( def userProjectsPath: File = { val projectsRootDirectory = projectsRoot.getOrElse( - DesktopEnvironment.getDirectories.getDocuments.toFile + Directories.getCurrent.getDocuments.toFile ) new File(projectsRootDirectory, projectsDirectory) } diff --git a/project/StdBits.scala b/project/StdBits.scala index 58e5960962ec..6614d1feadd3 100644 --- a/project/StdBits.scala +++ b/project/StdBits.scala @@ -26,7 +26,7 @@ object StdBits { * @param unmanagedClasspath classpath of unmanaged jars, if any * @param logger SBT's logger * @param cacheStoreFactory SBT's cache sotre factory - * @param ignoreDependency A dependency that should be ignored - not copied to the destination + * @param ignoreDependencies Depedencies that should be ignored - not copied to the destination * @param ignoreDependencyIncludeTransitive An optional filter to indicate that a direct dependency should be ignored except for its (transitive) dependencies * @param ignoreUnmanagedDependency An optional filter that tests if an unmanaged dependency should be ignored * @@ -40,7 +40,7 @@ object StdBits { unmanagedClasspath: Classpath, logger: ManagedLogger, cacheStoreFactory: sbt.util.CacheStoreFactory, - ignoreDependency: Option[ModuleID] = None, + ignoreDependencies: Option[Seq[ModuleID]] = None, ignoreDependencyIncludeTransitive: Option[String] = None, ignoreUnmanagedDependency: Option[File => Boolean] = None, previousRun: Option[AnalysisOfExtractedNativeLibs] = None @@ -63,16 +63,16 @@ object StdBits { val graalModuleFilter = DependencyFilter.moduleFilter( organization = new SimpleFilter(!graalVmOrgs.contains(_)) ) - val moduleFilter = ignoreDependency match { + val moduleFilter = ignoreDependencies match { case None => graalModuleFilter - case Some(ignoreDepID) => + case Some(ignoreDepIDs) => DependencyFilter.moduleFilter( organization = new SimpleFilter(orgName => !graalVmOrgs.contains( orgName - ) && orgName != ignoreDepID.organization + ) && !ignoreDepIDs.exists(_.organization == orgName) ), - name = new SimpleFilter(_ != ignoreDepID.name) + name = new SimpleFilter(name => !ignoreDepIDs.exists(_.name == name)) ) } val unmanagedFiles0 = unmanagedClasspath.map(_.data) diff --git a/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/OnlyWithDesktop.java b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/OnlyWithDesktop.java new file mode 100644 index 000000000000..53ed14f67390 --- /dev/null +++ b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/OnlyWithDesktop.java @@ -0,0 +1,14 @@ +package org.enso.nativeimage.workarounds; + +import java.util.function.BooleanSupplier; + +final class OnlyWithDesktop implements BooleanSupplier { + @Override + public boolean getAsBoolean() { + try { + return Class.forName("java.awt.GraphicsEnvironment") != null; + } catch (ClassNotFoundException ex) { + return false; + } + } +} diff --git a/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementGraphicsEnvironment.java b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_GraphicsEnvironment.java similarity index 62% rename from std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementGraphicsEnvironment.java rename to std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_GraphicsEnvironment.java index 81e2971aa5b7..e147da17a320 100644 --- a/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementGraphicsEnvironment.java +++ b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_GraphicsEnvironment.java @@ -3,8 +3,8 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -@TargetClass(java.awt.GraphicsEnvironment.class) -final class ReplacementGraphicsEnvironment { +@TargetClass(className = "java.awt.GraphicsEnvironment", onlyWith = OnlyWithDesktop.class) +final class Target_GraphicsEnvironment { @Substitute public static boolean isHeadless() { return true; diff --git a/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementToolkit.java b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_Toolkit.java similarity index 76% rename from std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementToolkit.java rename to std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_Toolkit.java index 1fb14dde2a19..9ffc05fba46b 100644 --- a/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/ReplacementToolkit.java +++ b/std-bits/table/src/main/java/org/enso/nativeimage/workarounds/Target_Toolkit.java @@ -4,8 +4,8 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -@TargetClass(java.awt.Toolkit.class) -final class ReplacementToolkit { +@TargetClass(className = "java.awt.Toolkit", onlyWith = OnlyWithDesktop.class) +final class Target_Toolkit { @Alias private static boolean loaded; diff --git a/tools/legal-review/engine/net.java.dev.jna.jna-5.14.0/copyright-keep b/tools/legal-review/engine/net.java.dev.jna.jna-5.14.0/copyright-keep deleted file mode 100644 index 489c03ab287c..000000000000 --- a/tools/legal-review/engine/net.java.dev.jna.jna-5.14.0/copyright-keep +++ /dev/null @@ -1,15 +0,0 @@ -Copyright (c) 2007 Timothy Wall, All Rights Reserved -Copyright (c) 2007 Wayne Meissner, All Rights Reserved -Copyright (c) 2007-2008 Timothy Wall, All Rights Reserved -Copyright (c) 2007-2012 Timothy Wall, All Rights Reserved -Copyright (c) 2007-2013 Timothy Wall, All Rights Reserved -Copyright (c) 2007-2015 Timothy Wall, All Rights Reserved -Copyright (c) 2009 Timothy Wall, All Rights Reserved -Copyright (c) 2011 Timothy Wall, All Rights Reserved -Copyright (c) 2012 Timothy Wall, All Rights Reserved -Copyright (c) 2017 Matthias Bläsing, All Rights Reserved -Copyright (c) 2018 Matthias Bläsing -Copyright (c) 2019 Matthias Bläsing, All Rights Reserved -Copyright (c) 2021, Matthias Bläsing, All Rights Reserved -Copyright (c) 2022 Carlos Ballesteros, All Rights Reserved -Copyright 2007 Timothy Wall diff --git a/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep b/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep deleted file mode 100644 index d81ec4d51487..000000000000 --- a/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (C) 2022 the original author(s). -Copyright (c) 2002-2020, the original author or authors. diff --git a/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep-context b/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep-context deleted file mode 100644 index 2b98ea8fc547..000000000000 --- a/tools/legal-review/engine/org.jline.jline-terminal-jna-3.26.3/copyright-keep-context +++ /dev/null @@ -1,4 +0,0 @@ -Copyright (c) 2002-2016, the original author(s). -Copyright (c) 2002-2017, the original author(s). -Copyright (c) 2002-2018, the original author(s). -Copyright (c) 2002-2020, the original author(s). diff --git a/tools/legal-review/engine/org.jline.jline-terminal-jni-3.26.3/copyright-keep b/tools/legal-review/engine/org.jline.jline-terminal-jni-3.26.3/copyright-keep new file mode 100644 index 000000000000..5ae705f78534 --- /dev/null +++ b/tools/legal-review/engine/org.jline.jline-terminal-jni-3.26.3/copyright-keep @@ -0,0 +1,5 @@ +Copyright (c) 2002-2017, the original author(s). +Copyright (c) 2002-2020, the original author or authors. +Copyright (c) 2002-2020, the original author(s). +Copyright (c) 2009-2018, the original author(s). +Copyright (C) 2022 the original author(s). diff --git a/tools/legal-review/engine/report-state b/tools/legal-review/engine/report-state index 596c7e98dac2..e036b436570e 100644 --- a/tools/legal-review/engine/report-state +++ b/tools/legal-review/engine/report-state @@ -1,3 +1,3 @@ -68CB821CDFCC647A517E04B3ACC776E4F62F24B2D087816B165161517075BFE6 -6B9FA2DE37E66A84C4DBE22BC57BBF5AB862DF0EA8CC074C4ED066E8E4CBBE21 +DAF23D6A22977F86980221BF1014E4683A9CB0F53D28BFC64B64078A472AC78D +28917C183D2A860ED1DEE907BFC476F0ED7EC0D0799AD72426D2B04E34ABA69E 0