diff --git a/.github/workflows/graalpy-micronaut-guide.yml b/.github/workflows/graalpy-micronaut-guide.yml index 36af5dd..f4706bf 100644 --- a/.github/workflows/graalpy-micronaut-guide.yml +++ b/.github/workflows/graalpy-micronaut-guide.yml @@ -34,7 +34,7 @@ jobs: ./mvnw --no-transfer-progress mn:run & mnpid="$!" sleep 30 - curl -s -D - -o /dev/null http://localhost:8080/ + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/ kill $mnpid - name: Build and run native 'graalpy-micronaut-guide' using Maven run: | @@ -43,5 +43,5 @@ jobs: ./target/graalpy-micronaut & mnpid="$!" sleep 20 - curl -s -D - -o /dev/null http://localhost:8080/ + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/ kill $mnpid diff --git a/.github/workflows/graalpy-micronaut-pygal-charts.yml b/.github/workflows/graalpy-micronaut-pygal-charts.yml new file mode 100644 index 0000000..8f6cee2 --- /dev/null +++ b/.github/workflows/graalpy-micronaut-pygal-charts.yml @@ -0,0 +1,47 @@ +name: Test GraalPy Micronaut Pygal Charts +on: + push: + paths: + - 'graalpy/graalpy-micronaut-pygal-charts/**' + - '.github/workflows/graalpy-micronaut-pygal-charts.yml' + pull_request: + paths: + - 'graalpy/graalpy-micronaut-pygal-charts/**' + - '.github/workflows/graalpy-micronaut-pygal-charts.yml' + workflow_dispatch: +permissions: + contents: read +env: + NATIVE_IMAGE_OPTIONS: '-J-Xmx16g' +jobs: + run: + name: 'graalpy-micronaut-pygal-charts' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: graalvm/setup-graalvm@v1 + with: + java-version: '23.0.0' + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + cache: 'maven' + native-image-job-reports: 'true' + - name: Build, test, and run 'graalpy-micronaut-pygal-charts' using Maven + run: | + cd graalpy/graalpy-micronaut-pygal-charts + ./mvnw --no-transfer-progress clean test -Dmicronaut.http.client.read-timeout=1m + ./mvnw --no-transfer-progress mn:run & + mnpid="$!" + sleep 30 + curl --fail-with-body --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/java + kill $mnpid + - name: Build and run native 'graalpy-micronaut-pygal-charts' using Maven + run: | + cd graalpy/graalpy-micronaut-pygal-charts + ./mvnw --no-transfer-progress clean package -DskipTests -Dpackaging=native-image + ./target/demo & + mnpid="$!" + sleep 20 + curl --fail-with-body --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/python + kill $mnpid diff --git a/.github/workflows/graalpy-spring-boot-guide.yml b/.github/workflows/graalpy-spring-boot-guide.yml index 08006c2..439643d 100644 --- a/.github/workflows/graalpy-spring-boot-guide.yml +++ b/.github/workflows/graalpy-spring-boot-guide.yml @@ -34,7 +34,7 @@ jobs: ./mvnw --no-transfer-progress spring-boot:run & sbpid="$!" sleep 30 - curl -s -D - -o /dev/null http://localhost:8080/ + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/ kill $sbpid - name: Build and run native 'graalpy-spring-boot-guide' using Maven run: | @@ -43,5 +43,5 @@ jobs: ./target/graalpy-springboot & sbpid="$!" sleep 20 - curl -s -D - -o /dev/null http://localhost:8080/ + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/ kill $sbpid diff --git a/.github/workflows/graalpy-spring-boot-pygal-charts.yml b/.github/workflows/graalpy-spring-boot-pygal-charts.yml new file mode 100644 index 0000000..7895fad --- /dev/null +++ b/.github/workflows/graalpy-spring-boot-pygal-charts.yml @@ -0,0 +1,47 @@ +name: Test GraalPy Spring Pygal Charts +on: + push: + paths: + - 'graalpy/graalpy-spring-boot-pygal-charts/**' + - '.github/workflows/graalpy-spring-boot-pygal-charts.yml' + pull_request: + paths: + - 'graalpy/graalpy-spring-boot-pygal-charts/**' + - '.github/workflows/graalpy-spring-boot-pygal-charts.yml' + workflow_dispatch: +permissions: + contents: read +env: + NATIVE_IMAGE_OPTIONS: '-J-Xmx16g' +jobs: + run: + name: 'graalpy-spring-boot-pygal-charts' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: graalvm/setup-graalvm@v1 + with: + java-version: '23.0.0' + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + cache: 'maven' + native-image-job-reports: 'true' + - name: Build, test, and run 'graalpy-spring-boot-pygal-charts' using Maven + run: | + cd graalpy/graalpy-spring-boot-pygal-charts + ./mvnw --no-transfer-progress clean test -Dspring.mvc.async.request-timeout=60000 + ./mvnw --no-transfer-progress spring-boot:run & + sbpid="$!" + sleep 30 + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/java + kill $sbpid + - name: Build and run native 'graalpy-spring-boot-pygal-charts' using Maven + run: | + cd graalpy/graalpy-spring-boot-pygal-charts + ./mvnw --no-transfer-progress clean -DskipTests -Pnative native:compile + ./target/demo & + sbpid="$!" + sleep 20 + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/python + kill $sbpid diff --git a/.github/workflows/graalwasm-micronaut-photon.yml b/.github/workflows/graalwasm-micronaut-photon.yml index f03ac25..36f6b4a 100644 --- a/.github/workflows/graalwasm-micronaut-photon.yml +++ b/.github/workflows/graalwasm-micronaut-photon.yml @@ -35,4 +35,4 @@ jobs: ./mvnw --no-transfer-progress clean package -Dpackaging=native-image ./target/demo & sleep 10 - curl -s -D - -o /dev/null http://localhost:8080/photo/flipv + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/photo/flipv diff --git a/.github/workflows/graalwasm-spring-boot-photon.yml b/.github/workflows/graalwasm-spring-boot-photon.yml index 617fe1d..0299405 100644 --- a/.github/workflows/graalwasm-spring-boot-photon.yml +++ b/.github/workflows/graalwasm-spring-boot-photon.yml @@ -35,4 +35,4 @@ jobs: ./mvnw --no-transfer-progress clean -Pnative native:compile ./target/demo & sleep 10 - curl -s -D - -o /dev/null http://localhost:8080/photo/flipv + curl --fail-with-body --silent --dump-header - -o /dev/null http://localhost:8080/photo/flipv diff --git a/graalpy/README.md b/graalpy/README.md index 018f654..725bcd1 100644 --- a/graalpy/README.md +++ b/graalpy/README.md @@ -9,6 +9,8 @@ This directory contains demo applications and guides for [GraalPy](https://www.g - [Minimal Java application that embeds GraalPy](graalpy-starter/) - [Minimal Java application that embeds `openai` Python package with GraalPy](graalpy-openai-starter/) - [Embed `qrcode` Python package with GraalPy in JBang](graalpy-jbang-qrcode/) +- [Embed SVG charting library `pygal` with GraalPy in Micronaut](graalpy-micronaut-pygal-charts/) +- [Embed SVG charting library `pygal` with GraalPy in Spring Boot](graalpy-spring-boot-pygal-charts/) ## Guides diff --git a/graalpy/graalpy-micronaut-pygal-charts/.gitignore b/graalpy/graalpy-micronaut-pygal-charts/.gitignore new file mode 100644 index 0000000..5a03bc3 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/.gitignore @@ -0,0 +1,15 @@ +Thumbs.db +.DS_Store +.gradle +build/ +target/ +out/ +.micronaut/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +.factorypath diff --git a/graalpy/graalpy-micronaut-pygal-charts/.mvn/wrapper/maven-wrapper.properties b/graalpy/graalpy-micronaut-pygal-charts/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/graalpy/graalpy-micronaut-pygal-charts/README.md b/graalpy/graalpy-micronaut-pygal-charts/README.md new file mode 100644 index 0000000..d69d788 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/README.md @@ -0,0 +1,48 @@ +## SVG Charts with GraalPy and Micronaut + +This demo illustrates how GraalPy can be used to embed [Pygal](https://github.com/Kozea/pygal), a dynamic SVG charting library written in Python, in a Micronaut application. +In particular, this demo shows four different approaches to interact with Pygal from Java. + +## Preparation + +Install GraalVM for JDK 23 and set the value of `JAVA_HOME` accordingly. +We recommend using [SDKMAN!](https://sdkman.io/). (For other download options, see [GraalVM Downloads](https://www.graalvm.org/downloads/).) + +```bash +sdk install java 23-graal +``` + +## Run the Application + +To start the demo, simply run: + +```bash +./mvnw package mn:run +``` + +When the demo runs, open the follwing URLs in a browser: + +| URL | Service | +|:------------------------------|:------------------------------| +| http://localhost:8080/java | [`PyGalServicePureJava`](src/main/java/com/example/PyGalServicePureJava.java) | +| http://localhost:8080/python | [`PyGalServicePurePython`](src/main/java/com/example/PyGalServicePurePython.java) | +| http://localhost:8080/mixed | [`PyGalServiceMixed`](src/main/java/com/example/PyGalServiceMixed.java) | +| http://localhost:8080/dynamic | [`PyGalServiceValueAPIDynamic`](src/main/java/com/example/PyGalServiceValueAPIDynamic.java) | + + +## Implementation Details + +The `DemoController` uses four services that all render the same XY chart using different implementations: + +- [`PyGalServicePureJava`](src/main/java/com/example/PyGalServicePureJava.java) interacts with Pygal and Python using Java interfaces and `Value.as(Class targetType)`. This is the recommended approach. +- [`PyGalServicePurePython`](src/main/java/com/example/PyGalServicePurePython.java) embeds the Python sample code from the Pygal documentation. +- [`PyGalServiceMixed`](src/main/java/com/example/PyGalServiceMixed.java) uses a Python function which is invoked with Java values. +- [`PyGalServiceValueAPIDynamic`](src/main/java/com/example/PyGalServiceValueAPIDynamic.java) uses the [Value](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html) API from the GraalVM SDK. + + +The `DemoTest` ensures that all four service implementations render the same XY chart. Run it with `./mvnw test`. + +> Note: This demo uses a single [`GraalPyContext`](src/main/java/com/example/GraalPyContext.java), which can execute [Python code in only one thread at a time](https://docs.python.org/3/glossary.html#term-global-interpreter-lock). +> Threads running Python code are internally scheduled in round-robin fashion. +> Pure Python packages including Pygal can be used in multiple GraalPy contexts, for example one context per thread, to improve the throughput of the application. +> Other demos such as [`graalwasm-micronaut-photon`](../graalwasm/graalwasm/graalwasm-micronaut-photon) illustrate how to pool multiple contexts. diff --git a/graalpy/graalpy-micronaut-pygal-charts/aot-jar.properties b/graalpy/graalpy-micronaut-pygal-charts/aot-jar.properties new file mode 100644 index 0000000..3820d55 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/aot-jar.properties @@ -0,0 +1,37 @@ +# AOT configuration properties for jar packaging +# Please review carefully the optimizations enabled below +# Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details + +# Caches environment property values: environment properties will be deemed immutable after application startup. +cached.environment.enabled=true + +# Precomputes Micronaut configuration property keys from the current environment variables +precompute.environment.properties.enabled=true + +# Replaces logback.xml with a pure Java configuration +logback.xml.to.java.enabled=true + +# Converts YAML configuration files to Java configuration +yaml.to.java.config.enabled=true + +# Scans for service types ahead-of-time, avoiding classpath scanning at startup +serviceloading.jit.enabled=true + +# Scans reactive types at build time instead of runtime +scan.reactive.types.enabled=true + +# Deduces the environment at build time instead of runtime +deduce.environment.enabled=true + +# Checks for the existence of some types at build time instead of runtime +known.missing.types.enabled=true + +# Precomputes property sources at build time +sealed.property.source.enabled=true + +# The list of service types to be scanned (comma separated) +service.types=io.micronaut.context.env.PropertySourceLoader,io.micronaut.inject.BeanConfiguration,io.micronaut.inject.BeanDefinitionReference,io.micronaut.http.HttpRequestFactory,io.micronaut.http.HttpResponseFactory,io.micronaut.core.beans.BeanIntrospectionReference,io.micronaut.core.convert.TypeConverterRegistrar,io.micronaut.context.env.PropertyExpressionResolver + +# A list of types that the AOT analyzer needs to check for existence (comma separated) +known.missing.types.list=io.reactivex.Observable,reactor.core.publisher.Flux,kotlinx.coroutines.flow.Flow,io.reactivex.rxjava3.core.Flowable,io.reactivex.rxjava3.core.Observable,io.reactivex.Single,reactor.core.publisher.Mono,io.reactivex.Maybe,io.reactivex.rxjava3.core.Single,io.reactivex.rxjava3.core.Maybe,io.reactivex.Completable,io.reactivex.rxjava3.core.Completable,io.methvin.watchservice.MacOSXListeningWatchService,io.micronaut.core.async.publisher.CompletableFuturePublisher,io.micronaut.core.async.publisher.Publishers.JustPublisher,io.micronaut.core.async.subscriber.Completable + diff --git a/graalpy/graalpy-micronaut-pygal-charts/micronaut-cli.yml b/graalpy/graalpy-micronaut-pygal-charts/micronaut-cli.yml new file mode 100644 index 0000000..dd29634 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/micronaut-cli.yml @@ -0,0 +1,6 @@ +applicationType: default +defaultPackage: com.example +testFramework: junit +sourceLanguage: java +buildTool: maven +features: [app-name, http-client-test, java, java-application, junit, logback, maven, maven-enforcer-plugin, micronaut-aot, micronaut-http-validation, netty-server, properties, readme, serialization-jackson, shade, static-resources] diff --git a/graalpy/graalpy-micronaut-pygal-charts/mvnw b/graalpy/graalpy-micronaut-pygal-charts/mvnw new file mode 100755 index 0000000..8822887 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/mvnw @@ -0,0 +1,287 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.1.1 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + printf '%s' "$(cd "$basedir"; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname $0)") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $wrapperUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + QUIET="--quiet" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + QUIET="" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" + fi + [ $? -eq 0 ] || rm -f "$wrapperJarPath" + elif command -v curl > /dev/null; then + QUIET="--silent" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + QUIET="" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L + fi + [ $? -eq 0 ] || rm -f "$wrapperJarPath" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=`cygpath --path --windows "$javaSource"` + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/graalpy/graalpy-micronaut-pygal-charts/mvnw.bat b/graalpy/graalpy-micronaut-pygal-charts/mvnw.bat new file mode 100644 index 0000000..1d7c59b --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/mvnw.bat @@ -0,0 +1,187 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.1.1 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/graalpy/graalpy-micronaut-pygal-charts/pom.xml b/graalpy/graalpy-micronaut-pygal-charts/pom.xml new file mode 100644 index 0000000..b96660e --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + com.example + demo + 0.1 + ${packaging} + + + io.micronaut.platform + micronaut-parent + 4.6.1 + + + 24.1.0 + jar + 21 + 21 + 4.6.1 + netty + false + com.example.aot.generated + com.example.Application + + + + + central + https://repo.maven.apache.org/maven2 + + + + + + org.graalvm.polyglot + polyglot + ${graalpy.version} + + + + org.graalvm.polyglot + python + ${graalpy.version} + pom + + + + org.graalvm.python + python-embedding + ${graalpy.version} + + + + io.micronaut + micronaut-http-server-netty + compile + + + io.micronaut.serde + micronaut-serde-jackson + compile + + + ch.qos.logback + logback-classic + runtime + + + io.micronaut + micronaut-http-client + test + + + io.micronaut.test + micronaut-test-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.graalvm.python + graalpy-maven-plugin + ${graalpy.version} + + + + + pygal==3.0.5 + + + + process-graalpy-resources + + + + + + io.micronaut.maven + micronaut-maven-plugin + + aot-${packaging}.properties + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + + + io.micronaut + micronaut-http-validation + ${micronaut.core.version} + + + io.micronaut.serde + micronaut-serde-processor + ${micronaut.serialization.version} + + + io.micronaut + micronaut-inject + + + + + + -Amicronaut.processing.group=com.example + -Amicronaut.processing.module=demo + + + + + + + diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/Application.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/Application.java new file mode 100644 index 0000000..6d16a5a --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/Application.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } +} \ No newline at end of file diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/DemoController.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/DemoController.java new file mode 100644 index 0000000..74ac11b --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/DemoController.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; + +@Controller +public class DemoController { + + private final PyGalServicePurePython pyGalServicePurePython; + private final PyGalServicePureJava pyGalServicePureJava; + private final PyGalServiceMixed pyGalServiceMixed; + private final PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic; + + DemoController(PyGalServicePurePython pyGalServicePurePython, PyGalServicePureJava pyGalServicePureJava, PyGalServiceMixed pyGalServiceMixed, PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic) { + this.pyGalServicePurePython = pyGalServicePurePython; + this.pyGalServicePureJava = pyGalServicePureJava; + this.pyGalServiceMixed = pyGalServiceMixed; + this.pyGalServiceValueAPIDynamic = pyGalServiceValueAPIDynamic; + } + + @Get("/python") + @Produces(MediaType.IMAGE_SVG) + public String renderXYChartPurePython() { + return pyGalServicePurePython.renderXYChart(); + } + + @Get("/java") + @Produces(MediaType.IMAGE_SVG) + public String renderXYChartPureJava() { + return pyGalServicePureJava.renderXYChart(); + } + + @Get("/mixed") + @Produces(MediaType.IMAGE_SVG) + public String renderXYChartMixed() { + return pyGalServiceMixed.renderXYChart(); + } + + @Get("/dynamic") + @Produces(MediaType.IMAGE_SVG) + public String renderXYChartValueAPIDynamic() { + return pyGalServiceValueAPIDynamic.renderXYChart(); + } +} + diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/GraalPyContext.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/GraalPyContext.java new file mode 100644 index 0000000..030ca3b --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/GraalPyContext.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import io.micronaut.context.annotation.Context; +import jakarta.annotation.PreDestroy; +import org.graalvm.polyglot.Value; +import org.graalvm.python.embedding.utils.GraalPyResources; + +@Context +public class GraalPyContext { + private final org.graalvm.polyglot.Context context = GraalPyResources.createContext(); + + public GraalPyContext() { + context.initialize("python"); + } + + public Value eval(String source) { + return context.eval("python", source); + } + + @PreDestroy + void close() { + context.close(true); + } +} diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalService.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalService.java new file mode 100644 index 0000000..ab2136c --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalService.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +/** + * Example PyGal code: + *
+ *     from math import cos
+ *     xy_chart = pygal.XY()
+ *     xy_chart.title = 'XY Cosinus'
+ *     xy_chart.add('x = cos(y)', [(cos(x / 10.), x / 10.) for x in range(-50, 50, 5)])
+ *     xy_chart.add('y = cos(x)', [(x / 10., cos(x / 10.)) for x in range(-50, 50, 5)])
+ *     xy_chart.add('x = 1',  [(1, -5), (1, 5)])
+ *     xy_chart.add('x = -1', [(-1, -5), (-1, 5)])
+ *     xy_chart.add('y = 1',  [(-5, 1), (5, 1)])
+ *     xy_chart.add('y = -1', [(-5, -1), (5, -1)])
+ *     xy_chart.render()
+ * 
+ */ +interface PyGalService { + String renderXYChart(); +} \ No newline at end of file diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceMixed.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceMixed.java new file mode 100644 index 0000000..f93fc7e --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceMixed.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import jakarta.inject.Singleton; +import org.graalvm.polyglot.Value; + +import java.util.List; +import java.util.stream.IntStream; + +@Singleton +public class PyGalServiceMixed implements PyGalService { + private final Value pythonFunctionXY; + + PyGalServiceMixed(GraalPyContext graalPyContext) { + pythonFunctionXY = graalPyContext.eval( + // language=python + """ + import pygal + + def render_xy(title, label_datapoint_entries): + xy_chart = pygal.XY() + xy_chart.title = title + for entry in label_datapoint_entries: + xy_chart.add(entry.label(), entry.dataPoints()) + return xy_chart.render().decode() + + render_xy"""); + } + + public record Entry(String label, double[][] dataPoints) { + } + + @Override + public String renderXYChart() { + String title = "XY Cosinus"; + List labelDatapointEntries = List.of( + new Entry("x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)), + new Entry("y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)), + new Entry("x = 1", new double[][]{{1, -5}, {1, 5}}), + new Entry("x = -1", new double[][]{{-1, -5}, {-1, 5}}), + new Entry("y = 1", new double[][]{{-5, 1}, {5, 1}}), + new Entry("y = -1", new double[][]{{-5, -1}, {5, -1}}) + ); + return pythonFunctionXY.execute(title, labelDatapointEntries).asString(); + } +} diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePureJava.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePureJava.java new file mode 100644 index 0000000..1ef895b --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePureJava.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import jakarta.inject.Singleton; +import org.graalvm.polyglot.Value; + +import java.util.stream.IntStream; + + +@Singleton +public class PyGalServicePureJava implements PyGalService { + private final PyGal pyGalModule; + + PyGalServicePureJava(GraalPyContext graalPyContext) { + pyGalModule = graalPyContext.eval("import pygal; pygal").as(PyGal.class); + } + + public interface PyGal { + XY XY(); + } + + public interface XY { + default void title(String title) { + Value.asValue(this).putMember("title", title); + } + + void add(String label, Object[] values); + + BytesIO render(); + } + + public interface BytesIO { + String decode(); + } + + @Override + public String renderXYChart() { + XY xyChart = pyGalModule.XY(); + xyChart.title("XY Cosinus"); + xyChart.add("x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)); + xyChart.add("y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)); + xyChart.add("x = 1", new int[][]{{1, -5}, {1, 5}}); + xyChart.add("x = -1", new int[][]{{-1, -5}, {-1, 5}}); + xyChart.add("y = 1", new int[][]{{-5, 1}, {5, 1}}); + xyChart.add("y = -1", new int[][]{{-5, -1}, {5, -1}}); + return xyChart.render().decode(); + } +} diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePurePython.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePurePython.java new file mode 100644 index 0000000..4840bcc --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServicePurePython.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import jakarta.inject.Singleton; + +@Singleton +public class PyGalServicePurePython implements PyGalService { + private final String purePythonXY; + + PyGalServicePurePython(GraalPyContext graalPyContext) { + purePythonXY = graalPyContext.eval( + // language=python + """ + import pygal + from math import cos + xy_chart = pygal.XY() + xy_chart.title = 'XY Cosinus' + xy_chart.add('x = cos(y)', [(cos(x / 10.), x / 10.) for x in range(-50, 50, 5)]) + xy_chart.add('y = cos(x)', [(x / 10., cos(x / 10.)) for x in range(-50, 50, 5)]) + xy_chart.add('x = 1', [(1, -5), (1, 5)]) + xy_chart.add('x = -1', [(-1, -5), (-1, 5)]) + xy_chart.add('y = 1', [(-5, 1), (5, 1)]) + xy_chart.add('y = -1', [(-5, -1), (5, -1)]) + xy_chart.render().decode() + """).asString(); + } + + @Override + public String renderXYChart() { + return purePythonXY; + } +} diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceValueAPIDynamic.java b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceValueAPIDynamic.java new file mode 100644 index 0000000..21da725 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/java/com/example/PyGalServiceValueAPIDynamic.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + + +import jakarta.inject.Singleton; +import org.graalvm.polyglot.Value; + +import java.util.stream.IntStream; + +@Singleton +public class PyGalServiceValueAPIDynamic implements PyGalService { + private final Value pyGalModule; + + PyGalServiceValueAPIDynamic(GraalPyContext context) { + pyGalModule = context.eval("import pygal; pygal"); + } + + @Override + public String renderXYChart() { + Value xyChart = pyGalModule.invokeMember("XY"); + xyChart.putMember("title", "XY Cosinus"); + xyChart.invokeMember("add", "x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)); + xyChart.invokeMember("add", "y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)); + xyChart.invokeMember("add", "x = 1", new int[][]{{1, -5}, {1, 5}}); + xyChart.invokeMember("add", "x = -1", new int[][]{{-1, -5}, {-1, 5}}); + xyChart.invokeMember("add", "y = 1", new int[][]{{-5, 1}, {5, 1}}); + xyChart.invokeMember("add", "y = -1", new int[][]{{-5, -1}, {5, -1}}); + return xyChart.invokeMember("render").invokeMember("decode").asString(); + } +} diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json new file mode 100644 index 0000000..56b5111 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json @@ -0,0 +1,37 @@ +{ + "reflection": [ + { + "type": "com.example.PyGalServiceMixed$Entry" + }, + { + "type": "com.example.PyGalServicePureJava$BytesIO" + }, + { + "type": "com.example.PyGalServicePureJava$PyGal" + }, + { + "type": "com.example.PyGalServicePureJava$XY" + }, + { + "type": { + "proxy": [ + "com.example.PyGalServicePureJava$PyGal" + ] + } + }, + { + "type": { + "proxy": [ + "com.example.PyGalServicePureJava$XY" + ] + } + }, + { + "type": { + "proxy": [ + "com.example.PyGalServicePureJava$BytesIO" + ] + } + } + ] +} \ No newline at end of file diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/application.properties b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/application.properties new file mode 100644 index 0000000..caae289 --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/application.properties @@ -0,0 +1,2 @@ +#Mon Sep 09 12:42:25 UTC 2024 +micronaut.application.name=demo diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/logback.xml b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/logback.xml new file mode 100644 index 0000000..2d77bda --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/graalpy/graalpy-micronaut-pygal-charts/src/test/java/com/example/DemoTest.java b/graalpy/graalpy-micronaut-pygal-charts/src/test/java/com/example/DemoTest.java new file mode 100644 index 0000000..ac2273f --- /dev/null +++ b/graalpy/graalpy-micronaut-pygal-charts/src/test/java/com/example/DemoTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; + +@MicronautTest +class DemoTest { + + @Inject + private PyGalServicePureJava pyGalServicePureJava; + @Inject + private PyGalServicePurePython pyGalServicePurePython; + @Inject + private PyGalServiceMixed pyGalServiceMixed; + @Inject + private PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic; + + @Test + void testIdentity() { + // Patterns to remove unique identifiers that PyGal adds every time a chart is rendered + String identifierPattern = "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"; + String chartPattern = "chart-" + identifierPattern; + String pyGalConfigPattern = "pygal\\.config\\['" + identifierPattern + "']"; + + // We use a HashSet to check whether the charts are identical + HashSet xyCharts = new HashSet<>(); + for (PyGalService service : List.of(pyGalServicePureJava, pyGalServicePurePython, pyGalServiceMixed, pyGalServiceValueAPIDynamic)) { + String xyChart = service.renderXYChart(); + String xzChartSanitized = xyChart.replaceAll(chartPattern, "chart").replaceAll(pyGalConfigPattern, "pygal.config"); + xyCharts.add(xzChartSanitized); + } + Assertions.assertEquals(1, xyCharts.size(), "xyCharts are not identical"); + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/.gitignore b/graalpy/graalpy-spring-boot-pygal-charts/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/graalpy/graalpy-spring-boot-pygal-charts/.mvn/wrapper/maven-wrapper.properties b/graalpy/graalpy-spring-boot-pygal-charts/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/graalpy/graalpy-spring-boot-pygal-charts/README.md b/graalpy/graalpy-spring-boot-pygal-charts/README.md new file mode 100644 index 0000000..349381e --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/README.md @@ -0,0 +1,48 @@ +## SVG Charts with GraalPy and Spring Boot + +This demo illustrates how GraalPy can be used to embed [Pygal](https://github.com/Kozea/pygal), a dynamic SVG charting library written in Python, in a Spring Boot application. +In particular, this demo shows four different approaches to interact with Pygal from Java. + +## Preparation + +Install GraalVM for JDK 23 and set the value of `JAVA_HOME` accordingly. +We recommend using [SDKMAN!](https://sdkman.io/). (For other download options, see [GraalVM Downloads](https://www.graalvm.org/downloads/).) + +```bash +sdk install java 23-graal +``` + +## Run the Application + +To start the demo, simply run: + +```bash +./mvnw package spring-boot:run +``` + +When the demo runs, open the follwing URLs in a browser: + +| URL | Service | +|:------------------------------|:------------------------------| +| http://localhost:8080/java | [`PyGalServicePureJava`](src/main/java/com/example/demo/demo/PyGalServicePureJava.java) | +| http://localhost:8080/python | [`PyGalServicePurePython`](src/main/java/com/example/demo/PyGalServicePurePython.java) | +| http://localhost:8080/mixed | [`PyGalServiceMixed`](src/main/java/com/example/demo/PyGalServiceMixed.java) | +| http://localhost:8080/dynamic | [`PyGalServiceValueAPIDynamic`](src/main/java/com/example/demo/PyGalServiceValueAPIDynamic.java) | + + +## Implementation Details + +The `DemoController` uses four services that all render the same XY chart using different implementations: + +- [`PyGalServicePureJava`](src/main/java/com/example/demo/PyGalServicePureJava.java) interacts with Pygal and Python using Java interfaces and `Value.as(Class targetType)`. This is the recommended approach. +- [`PyGalServicePurePython`](src/main/java/com/example/demo/PyGalServicePurePython.java) embeds the Python sample code from the Pygal documentation. +- [`PyGalServiceMixed`](src/main/java/com/example/demo/PyGalServiceMixed.java) uses a Python function which is invoked with Java values. +- [`PyGalServiceValueAPIDynamic`](src/main/java/com/example/demo/PyGalServiceValueAPIDynamic.java) uses the [Value](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html) API from the GraalVM SDK. + + +The `DemoApplicationTests` ensures that all four service implementations render the same XY chart. Run it with `./mvnw test`. + +> Note: This demo uses a single [`GraalPyContext`](src/main/java/com/example/demo/GraalPyContextConfiguration.java), which can execute [Python code in only one thread at a time](https://docs.python.org/3/glossary.html#term-global-interpreter-lock). +> Threads running Python code are internally scheduled in round-robin fashion. +> Pure Python packages including Pygal can be used in multiple GraalPy contexts, for example one context per thread, to improve the throughput of the application. +> Other demos such as [`graalwasm-micronaut-photon`](../graalwasm/graalwasm/graalwasm-spring-boot-photon) illustrate how to pool multiple contexts. \ No newline at end of file diff --git a/graalpy/graalpy-spring-boot-pygal-charts/mvnw b/graalpy/graalpy-spring-boot-pygal-charts/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/graalpy/graalpy-spring-boot-pygal-charts/mvnw.cmd b/graalpy/graalpy-spring-boot-pygal-charts/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/graalpy/graalpy-spring-boot-pygal-charts/pom.xml b/graalpy/graalpy-spring-boot-pygal-charts/pom.xml new file mode 100644 index 0000000..fb0ba13 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + + + + + + + + + + + + + + 24.1.0 + 21 + + + + org.graalvm.polyglot + polyglot + ${graalpy.version} + + + + org.graalvm.polyglot + python + ${graalpy.version} + pom + + + + org.graalvm.python + python-embedding + ${graalpy.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.graalvm.python + graalpy-maven-plugin + ${graalpy.version} + + + + + pygal==3.0.5 + + + + process-graalpy-resources + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + + + diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoApplication.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..71ffad1 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoController.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoController.java new file mode 100644 index 0000000..cde336f --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/DemoController.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class DemoController { + + private final PyGalServicePurePython pyGalServicePurePython; + private final PyGalServicePureJava pyGalServicePureJava; + private final PyGalServiceMixed pyGalServiceMixed; + private final PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic; + + public DemoController(PyGalServicePurePython pyGalServicePurePython, PyGalServicePureJava pyGalServicePureJava, PyGalServiceMixed pyGalServiceMixed, PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic) { + this.pyGalServicePurePython = pyGalServicePurePython; + this.pyGalServicePureJava = pyGalServicePureJava; + this.pyGalServiceMixed = pyGalServiceMixed; + this.pyGalServiceValueAPIDynamic = pyGalServiceValueAPIDynamic; + } + + @GetMapping(value = "/python", produces = "image/svg+xml") + public String renderXYChartPurePython() { + return pyGalServicePurePython.renderXYChart(); + } + + @GetMapping(value = "/java", produces = "image/svg+xml") + public String renderXYChartPureJava() { + return pyGalServicePureJava.renderXYChart(); + } + + @GetMapping(value = "/mixed", produces = "image/svg+xml") + public String renderXYChartMixed() { + return pyGalServiceMixed.renderXYChart(); + } + + @GetMapping(value = "/dynamic", produces = "image/svg+xml") + public String renderXYChartValueAPIDynamic() { + return pyGalServiceValueAPIDynamic.renderXYChart(); + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/GraalPyContextConfiguration.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/GraalPyContextConfiguration.java new file mode 100644 index 0000000..8978e7e --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/GraalPyContextConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.graalvm.python.embedding.utils.GraalPyResources; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GraalPyContextConfiguration { + /* + * Make GraalPy context available for injection as a Spring bean. + */ + @Bean(destroyMethod = "close") + public GraalPyContext graalPyContext() { + Context context = GraalPyResources.createContext(); + context.initialize("python"); + return new GraalPyContext(context); + } + + public record GraalPyContext(Context context) { + public Value eval(String source) { + return context.eval("python", source); + } + + public void close() { + context.close(true); + } + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalService.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalService.java new file mode 100644 index 0000000..b4d4063 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalService.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +/** + * Example PyGal code: + *
+ *     from math import cos
+ *     xy_chart = pygal.XY()
+ *     xy_chart.title = 'XY Cosinus'
+ *     xy_chart.add('x = cos(y)', [(cos(x / 10.), x / 10.) for x in range(-50, 50, 5)])
+ *     xy_chart.add('y = cos(x)', [(x / 10., cos(x / 10.)) for x in range(-50, 50, 5)])
+ *     xy_chart.add('x = 1',  [(1, -5), (1, 5)])
+ *     xy_chart.add('x = -1', [(-1, -5), (-1, 5)])
+ *     xy_chart.add('y = 1',  [(-5, 1), (5, 1)])
+ *     xy_chart.add('y = -1', [(-5, -1), (5, -1)])
+ *     xy_chart.render()
+ * 
+ */ +interface PyGalService { + String renderXYChart(); +} \ No newline at end of file diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceMixed.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceMixed.java new file mode 100644 index 0000000..f0fc98b --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceMixed.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import com.example.demo.GraalPyContextConfiguration.GraalPyContext; +import org.graalvm.polyglot.Value; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.IntStream; + +@Service +public class PyGalServiceMixed implements PyGalService { + private final Value pythonFunctionXY; + + PyGalServiceMixed(GraalPyContext graalPyContext) { + pythonFunctionXY = graalPyContext.eval( + // language=python + """ + import pygal + + def render_xy(title, label_datapoint_entries): + xy_chart = pygal.XY() + xy_chart.title = title + for entry in label_datapoint_entries: + xy_chart.add(entry.label(), entry.dataPoints()) + return xy_chart.render().decode() + + render_xy"""); + } + + public record Entry(String label, double[][] dataPoints) { + } + + @Override + public String renderXYChart() { + String title = "XY Cosinus"; + List labelDatapointEntries = List.of( + new Entry("x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)), + new Entry("y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)), + new Entry("x = 1", new double[][]{{1, -5}, {1, 5}}), + new Entry("x = -1", new double[][]{{-1, -5}, {-1, 5}}), + new Entry("y = 1", new double[][]{{-5, 1}, {5, 1}}), + new Entry("y = -1", new double[][]{{-5, -1}, {5, -1}}) + ); + return pythonFunctionXY.execute(title, labelDatapointEntries).asString(); + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePureJava.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePureJava.java new file mode 100644 index 0000000..6106d4f --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePureJava.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import com.example.demo.GraalPyContextConfiguration.GraalPyContext; +import org.graalvm.polyglot.Value; +import org.springframework.stereotype.Service; + +import java.util.stream.IntStream; + + +@Service +public class PyGalServicePureJava implements PyGalService { + private final PyGal pyGalModule; + + PyGalServicePureJava(GraalPyContext graalPyContext) { + pyGalModule = graalPyContext.eval("import pygal; pygal").as(PyGal.class); + } + + public interface PyGal { + XY XY(); + } + + public interface XY { + default void title(String title) { + Value.asValue(this).putMember("title", title); + } + + void add(String label, Object[] values); + + BytesIO render(); + } + + public interface BytesIO { + String decode(); + } + + @Override + public String renderXYChart() { + XY xyChart = pyGalModule.XY(); + xyChart.title("XY Cosinus"); + xyChart.add("x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)); + xyChart.add("y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)); + xyChart.add("x = 1", new int[][]{{1, -5}, {1, 5}}); + xyChart.add("x = -1", new int[][]{{-1, -5}, {-1, 5}}); + xyChart.add("y = 1", new int[][]{{-5, 1}, {5, 1}}); + xyChart.add("y = -1", new int[][]{{-5, -1}, {5, -1}}); + return xyChart.render().decode(); + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePurePython.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePurePython.java new file mode 100644 index 0000000..69d9b2b --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServicePurePython.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import com.example.demo.GraalPyContextConfiguration.GraalPyContext; +import org.springframework.stereotype.Service; + +@Service +public class PyGalServicePurePython implements PyGalService { + private final String purePythonXY; + + PyGalServicePurePython(GraalPyContext graalPyContext) { + purePythonXY = graalPyContext.eval( + // language=python + """ + import pygal + from math import cos + xy_chart = pygal.XY() + xy_chart.title = 'XY Cosinus' + xy_chart.add('x = cos(y)', [(cos(x / 10.), x / 10.) for x in range(-50, 50, 5)]) + xy_chart.add('y = cos(x)', [(x / 10., cos(x / 10.)) for x in range(-50, 50, 5)]) + xy_chart.add('x = 1', [(1, -5), (1, 5)]) + xy_chart.add('x = -1', [(-1, -5), (-1, 5)]) + xy_chart.add('y = 1', [(-5, 1), (5, 1)]) + xy_chart.add('y = -1', [(-5, -1), (5, -1)]) + xy_chart.render().decode() + """).asString(); + } + + @Override + public String renderXYChart() { + return purePythonXY; + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceValueAPIDynamic.java b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceValueAPIDynamic.java new file mode 100644 index 0000000..6051e8e --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/java/com/example/demo/PyGalServiceValueAPIDynamic.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + + +import com.example.demo.GraalPyContextConfiguration.GraalPyContext; +import org.graalvm.polyglot.Value; +import org.springframework.stereotype.Service; + +import java.util.stream.IntStream; + +@Service +public class PyGalServiceValueAPIDynamic implements PyGalService { + private final Value pyGalModule; + + PyGalServiceValueAPIDynamic(GraalPyContext context) { + pyGalModule = context.eval("import pygal; pygal"); + } + + @Override + public String renderXYChart() { + Value xyChart = pyGalModule.invokeMember("XY"); + xyChart.putMember("title", "XY Cosinus"); + xyChart.invokeMember("add", "x = cos(y)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{Math.cos(x / 10.0), x / 10.0}).toArray(double[][]::new)); + xyChart.invokeMember("add", "y = cos(x)", IntStream.range(-50, 50).filter(x -> x % 5 == 0).mapToObj(x -> new double[]{x / 10.0, Math.cos(x / 10.0)}).toArray(double[][]::new)); + xyChart.invokeMember("add", "x = 1", new int[][]{{1, -5}, {1, 5}}); + xyChart.invokeMember("add", "x = -1", new int[][]{{-1, -5}, {-1, 5}}); + xyChart.invokeMember("add", "y = 1", new int[][]{{-5, 1}, {5, 1}}); + xyChart.invokeMember("add", "y = -1", new int[][]{{-5, -1}, {5, -1}}); + return xyChart.invokeMember("render").invokeMember("decode").asString(); + } +} diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json b/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json new file mode 100644 index 0000000..b8dcc03 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/META-INF/native-image/com.example/demo/reachability-metadata.json @@ -0,0 +1,37 @@ +{ + "reflection": [ + { + "type": "com.example.demo.PyGalServiceMixed$Entry" + }, + { + "type": "com.example.demo.PyGalServicePureJava$BytesIO" + }, + { + "type": "com.example.demo.PyGalServicePureJava$PyGal" + }, + { + "type": "com.example.demo.PyGalServicePureJava$XY" + }, + { + "type": { + "proxy": [ + "com.example.demo.PyGalServicePureJava$PyGal" + ] + } + }, + { + "type": { + "proxy": [ + "com.example.demo.PyGalServicePureJava$XY" + ] + } + }, + { + "type": { + "proxy": [ + "com.example.demo.PyGalServicePureJava$BytesIO" + ] + } + } + ] +} \ No newline at end of file diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/application.properties b/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/application.properties new file mode 100644 index 0000000..2109a44 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=demo diff --git a/graalpy/graalpy-spring-boot-pygal-charts/src/test/java/com/example/demo/DemoApplicationTests.java b/graalpy/graalpy-spring-boot-pygal-charts/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..f93ac80 --- /dev/null +++ b/graalpy/graalpy-spring-boot-pygal-charts/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL. + */ + +package com.example.demo; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashSet; +import java.util.List; + +@SpringBootTest +class DemoApplicationTests { + + @Autowired + private PyGalServicePureJava pyGalServicePureJava; + @Autowired + private PyGalServicePurePython pyGalServicePurePython; + @Autowired + private PyGalServiceMixed pyGalServiceMixed; + @Autowired + private PyGalServiceValueAPIDynamic pyGalServiceValueAPIDynamic; + + @Test + void testIdentity() { + // Patterns to remove unique identifiers that PyGal adds every time a chart is rendered + String identifierPattern = "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"; + String chartPattern = "chart-" + identifierPattern; + String pyGalConfigPattern = "pygal\\.config\\['" + identifierPattern + "']"; + + // We use a HashSet to check whether the charts are identical + HashSet xyCharts = new HashSet<>(); + for (PyGalService service : List.of(pyGalServicePureJava, pyGalServicePurePython, pyGalServiceMixed, pyGalServiceValueAPIDynamic)) { + String xyChart = service.renderXYChart(); + String xzChartSanitized = xyChart.replaceAll(chartPattern, "chart").replaceAll(pyGalConfigPattern, "pygal.config"); + xyCharts.add(xzChartSanitized); + } + Assertions.assertEquals(1, xyCharts.size(), "xyCharts are not identical"); + } +}