├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── jupyter │ ├── AsDisplayData.java │ ├── Displayer.java │ ├── Displayers.java │ ├── MIMETypes.java │ ├── Registration.java │ └── ToStringDisplayer.java └── test └── java └── jupyter ├── TestAsDisplayData.java ├── TestMimeTypeNotification.java ├── TestRegistration.java ├── TestToStringDisplayer.java └── Thing.java /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | out/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Project Jupyter 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JVM Repr 2 | 3 | [![Build Status](https://travis-ci.org/jupyter/jvm-repr.svg?branch=master)](https://travis-ci.org/jupyter/jvm-repr) 4 | [![JVM repr on jitpack](https://jitpack.io/v/jupyter/jvm-repr.svg)](https://jitpack.io/#jupyter/jvm-repr) 5 | 6 | [**Configuration**](#configuration) | [**Usage for Library Authors**](#usage---library-authors) | [**Usage for Kernel Authors**](#usage---kernel-authors) 7 | 8 | Standardizing object representation and inspection across JVM kernels (Scala, Clojure, Groovy, ...). 9 | 10 | ## Purpose 11 | 12 | JVM languages use various conventions to convert REPL outputs to forms that can be displayed. This project is an attempt to standardize by providing an API that libraries can use to register display methods for any jupyter frontend, whether console, notebook, 13 | or dashboard. 14 | 15 | This API has two main uses: 16 | 17 | * For library authors to provide a way to convert from a library's JVM objects to useful representations by MIME type 18 | * For kernel authors to convert any JVM object to useful representations by MIME type 19 | 20 | As it is with IPython, this is a contract between the libraries within the ecosystem, 21 | the kernel that inspects the objects, and the frontends that display them. 22 | 23 | ## Configuration 24 | 25 | See [instructions on JitPack](https://jitpack.io/#jupyter/jvm-repr) for gradle, maven, sbt, or leiningen. 26 | 27 | ## Usage 28 | 29 | ### Usage - Library authors 30 | 31 | Library authors can register conversion code either 32 | - by implementing a `Displayer` and registering it with `Displayers`, or 33 | - by having classes extend `AsDisplayData`, whose `display` method returns 34 | how an instance should be displayed. 35 | 36 | For example, the following will register a displayer for Vegas graphs: 37 | 38 | ```scala 39 | import java.util.Map 40 | import jupyter.Displayer 41 | import jupyter.Displayers 42 | import scala.collection.JavaConverters._ 43 | import vegas.DSL.ExtendedUnitSpecBuilder 44 | 45 | ... 46 | 47 | Displayers.register(classOf[ExtendedUnitSpecBuilder], 48 | new Displayer[ExtendedUnitSpecBuilder] { 49 | override def display(plot: ExtendedUnitSpecBuilder): Map[String, String] = { 50 | val plotAsJson = plot.toJson 51 | Map( 52 | "text/plain" -> plotAsJson, 53 | "application/json" -> plotAsJson, 54 | "text/html" -> new StaticHTMLRenderer(plotAsJson).frameHTML() 55 | ).asJava 56 | } 57 | }) 58 | ``` 59 | 60 | The following has `Thing` be represented as simple text in Jupyter front-ends, 61 | like `Thing(2)` for `new Thing(2)`: 62 | ```java 63 | import java.util.HashMap; 64 | import java.util.Map; 65 | 66 | class Thing implements AsDisplayData { 67 | private int n; 68 | public Thing(int n) { 69 | this.n = n; 70 | } 71 | public Map display() { 72 | Map result = new HashMap<>(); 73 | result.put(MIMETypes.TEXT, "Thing(" + n + ")"); 74 | return result; 75 | } 76 | } 77 | ``` 78 | 79 | Any kernel implementation can use the method to display Vegas graphs for the DSL 80 | objects. 81 | 82 | Library authors can optionally implement `setMimeTypes(String...)` to receive 83 | hints for the MIME types that the kernel or front-end supports. It is 84 | recommended that library authors use these hints to avoid expensive conversions. 85 | 86 | ### Usage - Kernel authors 87 | 88 | Kernel authors can use this API to display registered objects: 89 | 90 | ```java 91 | import java.util.Map 92 | 93 | // ... 94 | 95 | Object result = interpreter.eval(code); 96 | Map resultByMIME = Displayers.display(result); 97 | Kernel.this.display(resultByMIME); 98 | ``` 99 | 100 | Kernel authors can optionally call `Displayers.setMimeTypes(String...)` to send 101 | hints to display implementations with the set of MIME types that can be used by 102 | the kernel or front-end. 103 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | 4 | group = "jupyter" 5 | version = '0.2.1' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | ext { 12 | scalaVersion = '2.11' 13 | } 14 | 15 | sourceCompatibility = '1.8' 16 | targetCompatibility = '1.8' 17 | 18 | dependencies { 19 | testCompile 'junit:junit:4.12' 20 | } 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/jvm-repr/2e50ad4f70bde84018a1f1ee469517d2b63cd343/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jupyter-repr-api' 2 | -------------------------------------------------------------------------------- /src/main/java/jupyter/AsDisplayData.java: -------------------------------------------------------------------------------- 1 | package jupyter; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Convenience interface for classes whose display data is known in advance. 7 | *

8 | * Classes implementing this interface are displayed via the {@link AsDisplayData#display} method 9 | * by default, without needing to register a {@link Displayer} in advance. 10 | */ 11 | public interface AsDisplayData { 12 | /** 13 | * Called to display this object. 14 | *

15 | * This method should return a map of MIME type strings to representations of 16 | * this object in that MIME type. 17 | *

18 | * To avoid extra conversion, kernels or front-ends can call 19 | * {@link #setMimeTypes(String...)} to pass supported MIME types. 20 | * Implementations may ignore these MIME type hints. 21 | * 22 | * @return a Map of representations of this object by MIME type 23 | */ 24 | Map display(); 25 | 26 | /** 27 | * Called to pass the MIME types supported by the kernel or front-end. 28 | *

29 | * Implementations may ignore these MIME type hints. 30 | * 31 | * @see Displayer#setMimeTypes(String...) 32 | * 33 | * @param types MIME types that are supported by the kernel 34 | */ 35 | default void setMimeTypes(String... types) { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/jupyter/Displayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import java.util.Map; 20 | 21 | /** 22 | * Converts objects to representations for display, by MIME type. 23 | * 24 | * @param the class or interface of objects the instance can convert. 25 | */ 26 | public abstract class Displayer { 27 | /** 28 | * Called to display an object. 29 | *

30 | * This method should return a map of MIME type strings to representations of 31 | * the object in that MIME type. 32 | *

33 | * This method can be called to display objects of type T or subtypes of T. 34 | *

35 | * To avoid extra conversion, kernels or front-ends can call 36 | * {@link #setMimeTypes(String...)} to pass supported MIME types. 37 | * Implementations may ignore these MIME type hints. 38 | * 39 | * @param obj an object instance to display 40 | * @return a Map of representations of this object by MIME type 41 | */ 42 | public abstract Map display(T obj); 43 | 44 | /** 45 | * Called to pass the MIME types supported by the kernel or front-end. 46 | *

47 | * Implementations may ignore these MIME type hints. 48 | * 49 | * @param types MIME types that are supported by the kernel 50 | */ 51 | public void setMimeTypes(String... types) { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/jupyter/Displayers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import java.util.Collections; 20 | import java.util.Map; 21 | 22 | /** 23 | * Convenience methods using common JVM registration. 24 | */ 25 | public abstract class Displayers { 26 | /** 27 | * Returns the common {@link Registration} in this JVM. 28 | */ 29 | public static Registration registration() { 30 | return Registration.INSTANCE; 31 | } 32 | 33 | /** 34 | * Registers a Displayer instance for a class or interface in this JVM. 35 | * 36 | * @param objClass the class of objects to display 37 | * @param displayer a Displayer instance 38 | */ 39 | public static void register(Class objClass, Displayer displayer) { 40 | registration().add(objClass, displayer); 41 | } 42 | 43 | /** 44 | * Sets the MIME type hint for all registered {@link Displayer} instances in 45 | * this JVM. 46 | * 47 | * @param types supported MIME types 48 | */ 49 | public static void setMimeTypes(String... types) { 50 | registration().setMimeTypes(types); 51 | } 52 | 53 | /** 54 | * Converts an object to one or more displayable representations by MIME type. 55 | * 56 | * @param obj an Object to display 57 | * @return a Map of representations of the object, by MIME type. 58 | */ 59 | @SuppressWarnings("unchecked") 60 | public static Map display(T obj) { 61 | Displayer displayer = registration().find((Class) obj.getClass()); 62 | if (displayer != null) { 63 | return displayer.display(obj); 64 | } else { 65 | return Collections.emptyMap(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/jupyter/MIMETypes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | public class MIMETypes { 20 | public static final String TEXT = "text/plain"; 21 | public static final String HTML = "text/html"; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/jupyter/Registration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.LinkedList; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | /** 27 | * Handles registration of {@link Displayer} instances. 28 | *

29 | * Callers should use the singleton instance of this class, which is available 30 | * from {@link Displayers#registration()}. 31 | */ 32 | public class Registration { 33 | 34 | static final Registration INSTANCE = new Registration(); 35 | 36 | private final Map, Displayer> displayers = new HashMap<>(); 37 | private Displayer defaultDisplayer = ToStringDisplayer.get(); 38 | private String[] mimeTypes = null; 39 | 40 | private static Displayer asDisplayDataDisplayer = new Displayer() { 41 | public Map display(AsDisplayData obj) { 42 | return obj.display(); 43 | } 44 | }; 45 | 46 | private void init() { 47 | add(AsDisplayData.class, asDisplayDataDisplayer); 48 | } 49 | 50 | public Registration() { 51 | init(); 52 | } 53 | 54 | public Map, Displayer> getAll() { 55 | return Collections.unmodifiableMap(displayers); 56 | } 57 | 58 | /** 59 | * Sets the MIME type hint for all registered {@link Displayer} instances. 60 | * 61 | * @param types supported MIME types 62 | */ 63 | public void setMimeTypes(String... types) { 64 | this.mimeTypes = types; 65 | for (Displayer displayer : displayers.values()) { 66 | displayer.setMimeTypes(types); 67 | } 68 | if (defaultDisplayer != null) { 69 | defaultDisplayer.setMimeTypes(types); 70 | } 71 | } 72 | 73 | /** 74 | * Sets the default {@link Displayer} instance. This is used to display any 75 | * {@link Object} with no more specific displayer. 76 | * 77 | * @param displayer a Displayer for any object. 78 | */ 79 | public void setDefault(Displayer displayer) { 80 | this.defaultDisplayer = displayer; 81 | } 82 | 83 | /** 84 | * Registers a Displayer instance for a class. 85 | * 86 | * @param objClass the class of objects to display 87 | * @param displayer a Displayer instance 88 | */ 89 | public void add(Class objClass, Displayer displayer) { 90 | if (mimeTypes != null) { 91 | displayer.setMimeTypes(mimeTypes); 92 | } 93 | displayers.put(objClass, displayer); 94 | } 95 | 96 | /** 97 | * Finds the most specific Displayer instance for a class. 98 | *

99 | * A displayer is found by checking registrations for the given class, its interfaces, its 100 | * superclasses, and each superclass's interfaces using a breadth-first search. The search visits 101 | * a class, then its interfaces in left-to-right order, then its superclass, the superclass's 102 | * interfaces, and so on. 103 | *

104 | * The first displayer that can handle the class will be returned. 105 | * 106 | * @param objClass the class of objects to display 107 | * @return a Displayer instance for this class or one of its superclasses. 108 | */ 109 | @SuppressWarnings("unchecked") 110 | public Displayer find(Class objClass) { 111 | Set> visited = new HashSet<>(); 112 | visited.add(Object.class); // stop search with Object 113 | LinkedList> classes = new LinkedList<>(); 114 | classes.addLast(objClass); 115 | 116 | while (!classes.isEmpty()) { 117 | Class currentClass = classes.removeFirst(); 118 | Displayer displayer = displayers.get(currentClass); 119 | if (displayer != null) { 120 | return (Displayer) displayer; 121 | } 122 | 123 | for (Class iface : currentClass.getInterfaces()) { 124 | if (!visited.contains(iface)) { 125 | classes.add((Class) iface); 126 | } 127 | } 128 | 129 | Class superClass = currentClass.getSuperclass(); 130 | // interface superclasses can be null 131 | if (superClass != null && !visited.contains(superClass)) { 132 | classes.add(superClass); 133 | } 134 | } 135 | 136 | return defaultDisplayer; 137 | } 138 | 139 | // Visible for testing 140 | void clear() { 141 | displayers.clear(); 142 | defaultDisplayer = ToStringDisplayer.get(); 143 | init(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/jupyter/ToStringDisplayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import java.util.Arrays; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | class ToStringDisplayer extends Displayer { 24 | 25 | private static final ToStringDisplayer INSTANCE = new ToStringDisplayer(); 26 | 27 | public static Displayer get() { 28 | return INSTANCE; 29 | } 30 | 31 | @Override 32 | public Map display(Object obj) { 33 | Map result = new HashMap<>(); 34 | 35 | if (obj.getClass().isArray()) { 36 | result.put("text/plain", displayArray(obj)); 37 | } else { 38 | result.put("text/plain", obj.toString()); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | private String displayArray(Object obj) { 45 | Class type = obj.getClass().getComponentType(); 46 | if (type == Boolean.TYPE) { 47 | return Arrays.toString((boolean[]) obj); 48 | } else if (type == Byte.TYPE) { 49 | return Arrays.toString((byte[]) obj); 50 | } else if (type == Short.TYPE) { 51 | return Arrays.toString((short[]) obj); 52 | } else if (type == Integer.TYPE) { 53 | return Arrays.toString((int[]) obj); 54 | } else if (type == Long.TYPE) { 55 | return Arrays.toString((long[]) obj); 56 | } else if (type == Float.TYPE) { 57 | return Arrays.toString((float[]) obj); 58 | } else if (type == Double.TYPE) { 59 | return Arrays.toString((double[]) obj); 60 | } else if (type == Character.TYPE) { 61 | return Arrays.toString((char[]) obj); 62 | } else { 63 | Object[] arr = (Object[]) obj; 64 | StringBuilder sb = new StringBuilder(); 65 | 66 | sb.append("["); 67 | if (arr.length > 0) { 68 | sb.append(displayElement(arr[0])); 69 | for (int i = 1; i < arr.length; i += 1) { 70 | String asText = displayElement(arr[i]); 71 | sb.append(", ").append(asText); 72 | } 73 | } 74 | sb.append("]"); 75 | 76 | return sb.toString(); 77 | } 78 | } 79 | 80 | private String displayElement(Object obj) { 81 | // Displayers.display doesn't accept nulls 82 | return obj != null ? Displayers.display(obj).get("text/plain") : "null"; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/jupyter/TestAsDisplayData.java: -------------------------------------------------------------------------------- 1 | package jupyter; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class TestAsDisplayData { 10 | 11 | private class Thing implements AsDisplayData { 12 | private int n; 13 | public Thing(int n) { 14 | this.n = n; 15 | } 16 | public Map display() { 17 | Map result = new HashMap<>(); 18 | result.put(MIMETypes.TEXT, "Thing(" + n + ")"); 19 | return result; 20 | } 21 | } 22 | 23 | @Test 24 | public void simple() { 25 | Map expectedResult = new HashMap<>(); 26 | expectedResult.put(MIMETypes.TEXT, "Thing(2)"); 27 | 28 | Thing thing = new Thing(2); 29 | Map result = Displayers.display(thing); 30 | Assert.assertEquals("Should rely on AsDisplayData" + expectedResult + " " + result, expectedResult, result); 31 | 32 | Displayers.registration().clear(); 33 | Assert.assertEquals("Should rely on AsDisplayData" + expectedResult + " " + result, expectedResult, result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/jupyter/TestMimeTypeNotification.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import org.junit.After; 20 | import org.junit.Assert; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.Map; 28 | import java.util.Set; 29 | 30 | public class TestMimeTypeNotification { 31 | 32 | @Before 33 | @After 34 | public void clearGlobals() { 35 | Displayers.registration().clear(); 36 | } 37 | 38 | private static class LimitingDisplayer extends Displayer { 39 | private Set mimeTypes = new HashSet<>(Arrays.asList( 40 | MIMETypes.TEXT, MIMETypes.HTML)); 41 | 42 | @Override 43 | public Map display(String obj) { 44 | Map result = new HashMap<>(); 45 | if (mimeTypes.contains(MIMETypes.TEXT)) { 46 | result.put(MIMETypes.TEXT, obj); 47 | } 48 | if (mimeTypes.contains(MIMETypes.HTML)) { 49 | result.put(MIMETypes.HTML, "
" + obj + "
"); 50 | } 51 | return result; 52 | } 53 | 54 | @Override 55 | public void setMimeTypes(String... types) { 56 | mimeTypes = new HashSet<>(Arrays.asList(types)); 57 | } 58 | } 59 | 60 | @Test 61 | public void testNotificationAfterRegistration() { 62 | Set expectedTypes = new HashSet<>(Collections.singletonList( 63 | MIMETypes.TEXT)); 64 | 65 | LimitingDisplayer disp = new LimitingDisplayer(); 66 | Displayers.register(String.class, disp); 67 | Displayers.setMimeTypes(MIMETypes.TEXT); 68 | 69 | Assert.assertEquals("Should set mime types on registered displayer", 70 | expectedTypes, disp.mimeTypes); 71 | } 72 | 73 | @Test 74 | public void testNotificationBeforeRegistration() { 75 | Set expectedTypes = new HashSet<>(Collections.singletonList( 76 | MIMETypes.TEXT)); 77 | 78 | Displayers.setMimeTypes(MIMETypes.TEXT); 79 | 80 | LimitingDisplayer disp = new LimitingDisplayer(); 81 | Displayers.register(String.class, disp); 82 | 83 | Assert.assertEquals("Should set mime types when displayer registers", 84 | expectedTypes, disp.mimeTypes); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/jupyter/TestRegistration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import org.junit.After; 20 | import org.junit.Assert; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | 24 | import jupyter.Displayers; 25 | 26 | import java.util.Collections; 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | import java.util.Random; 30 | 31 | public class TestRegistration { 32 | 33 | private interface TestSuperInterface { 34 | } 35 | 36 | private interface TestInterface extends TestSuperInterface { 37 | } 38 | 39 | private static class TestObject implements TestInterface { 40 | } 41 | 42 | private static class TestObjectSubclass extends TestObject { 43 | } 44 | 45 | private interface TestGenericInterface { 46 | } 47 | 48 | @Before 49 | @After 50 | public void clearGlobals() { 51 | Displayers.registration().clear(); 52 | } 53 | 54 | @Test 55 | public void testClassRegistration() { 56 | // generate a random string to validate the display method. only the right 57 | // displayer will return this string. 58 | Random rand = new Random(); 59 | final String expectedString = Double.toString(rand.nextDouble()); 60 | 61 | Displayer expected = new Displayer() { 62 | @Override 63 | public Map display(TestObject obj) { 64 | return asMap(MIMETypes.TEXT, expectedString); 65 | } 66 | }; 67 | 68 | Displayers.register(TestObject.class, expected); 69 | 70 | Assert.assertEquals("Should return registered displayer for class", 71 | expected, Displayers.registration().find(TestObject.class)); 72 | Assert.assertEquals("Should return registered displayer for instance", 73 | asMap(MIMETypes.TEXT, expectedString), 74 | Displayers.display(new TestObject())); 75 | } 76 | 77 | @Test 78 | public void testWalkSuperclasses() { 79 | // generate a random string to validate the display method. only the right 80 | // displayer will return this string. 81 | Random rand = new Random(); 82 | final String expectedString = Double.toString(rand.nextDouble()); 83 | 84 | Displayer expected = new Displayer() { 85 | @Override 86 | public Map display(TestObject obj) { 87 | return asMap(MIMETypes.TEXT, expectedString); 88 | } 89 | }; 90 | 91 | Displayers.register(TestObject.class, expected); 92 | 93 | Assert.assertEquals("Should return registered displayer for sub-class", 94 | expected, Displayers.registration().find(TestObjectSubclass.class)); 95 | Assert.assertEquals("Should return registered displayer for instance", 96 | asMap(MIMETypes.TEXT, expectedString), 97 | Displayers.display(new TestObjectSubclass())); 98 | } 99 | 100 | @Test 101 | public void testWalkInterfaces() { 102 | // generate a random string to validate the display method. only the right 103 | // displayer will return this string. 104 | Random rand = new Random(); 105 | final String expectedString = Double.toString(rand.nextDouble()); 106 | 107 | Displayer expected = new Displayer() { 108 | @Override 109 | public Map display(TestInterface obj) { 110 | return asMap(MIMETypes.TEXT, expectedString); 111 | } 112 | }; 113 | 114 | Displayers.register(TestInterface.class, expected); 115 | 116 | Assert.assertEquals("Should return registered displayer for interface", 117 | expected, Displayers.registration().find(TestInterface.class)); 118 | Assert.assertEquals("Should return registered displayer for class", 119 | expected, Displayers.registration().find(TestObject.class)); 120 | Assert.assertEquals("Should return registered displayer for sub-class", 121 | expected, Displayers.registration().find(TestObjectSubclass.class)); 122 | Assert.assertEquals("Should return registered displayer for instance", 123 | asMap(MIMETypes.TEXT, expectedString), 124 | Displayers.display(new TestObjectSubclass())); 125 | } 126 | 127 | @Test 128 | public void testWalkSuperInterfaces() { 129 | // generate a random string to validate the display method. only the right 130 | // displayer will return this string. 131 | Random rand = new Random(); 132 | final String expectedString = Double.toString(rand.nextDouble()); 133 | 134 | Displayer expected = new Displayer() { 135 | @Override 136 | public Map display(TestSuperInterface obj) { 137 | return asMap(MIMETypes.TEXT, expectedString); 138 | } 139 | }; 140 | 141 | Displayers.register(TestSuperInterface.class, expected); 142 | 143 | Assert.assertEquals("Should return registered displayer for interface", 144 | expected, Displayers.registration().find(TestInterface.class)); 145 | Assert.assertEquals("Should return registered displayer for interface", 146 | expected, Displayers.registration().find(TestSuperInterface.class)); 147 | Assert.assertEquals("Should return registered displayer for class", 148 | expected, Displayers.registration().find(TestObject.class)); 149 | Assert.assertEquals("Should return registered displayer for sub-class", 150 | expected, Displayers.registration().find(TestObjectSubclass.class)); 151 | Assert.assertEquals("Should return registered displayer for instance", 152 | asMap(MIMETypes.TEXT, expectedString), 153 | Displayers.display(new TestObjectSubclass())); 154 | } 155 | 156 | @Test 157 | public void testGenericInterfaces() { 158 | Displayer> expected = new Displayer>() { 159 | @Override 160 | public Map display(TestGenericInterface obj) { 161 | return asMap(MIMETypes.TEXT, "foobar"); 162 | } 163 | }; 164 | 165 | Displayers.register(TestGenericInterface.class, expected); 166 | 167 | Assert.assertEquals("Should return registered displayer for generic interface", 168 | expected, Displayers.registration().find(TestGenericInterface.class)); 169 | } 170 | 171 | @Test 172 | public void testArrayRegistration() { 173 | // generate a random string to validate the display method. only the right 174 | // displayer will return this string. 175 | Random rand = new Random(); 176 | final String expectedString = Double.toString(rand.nextDouble()); 177 | 178 | Displayer expected = new Displayer() { 179 | @Override 180 | public Map display(TestObject[] obj) { 181 | return asMap(MIMETypes.TEXT, expectedString); 182 | } 183 | }; 184 | 185 | Displayers.register(TestObject[].class, expected); 186 | 187 | Assert.assertEquals("Should return registered displayer for array class", 188 | expected, Displayers.registration().find(TestObject[].class)); 189 | Assert.assertEquals("Should return registered displayer for array instance", 190 | asMap(MIMETypes.TEXT, expectedString), 191 | Displayers.display(new TestObject[] {})); 192 | } 193 | 194 | @Test 195 | public void testNullDefault() { 196 | Displayers.registration().setDefault(null); 197 | Assert.assertEquals("Should return null default displayer for class", 198 | null, Displayers.registration().find(TestObject.class)); 199 | Assert.assertEquals("Should return null default display for instance", 200 | Collections.emptyMap(), 201 | Displayers.display(new TestObject())); 202 | } 203 | 204 | private Map asMap(String mimeType, String asText) { 205 | Map result = new HashMap<>(); 206 | result.put(mimeType, asText); 207 | return result; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/test/java/jupyter/TestToStringDisplayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jupyter; 18 | 19 | import org.junit.After; 20 | import org.junit.Assert; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | public class TestToStringDisplayer { 27 | 28 | @Before 29 | @After 30 | public void clearGlobals() { 31 | Displayers.registration().clear(); 32 | } 33 | 34 | private final ToStringDisplayer toString = new ToStringDisplayer(); 35 | 36 | private static class CustomClass { 37 | private final String field; 38 | 39 | public CustomClass(String field) { 40 | this.field = field; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "CustomClass(" + field + ")"; 46 | } 47 | } 48 | 49 | @Test 50 | public void testToString() { 51 | Assert.assertEquals("Integer", 52 | asMap(MIMETypes.TEXT, "3"), 53 | toString.display(3)); 54 | Assert.assertEquals("Boolean", 55 | asMap(MIMETypes.TEXT, "false"), 56 | toString.display(false)); 57 | Assert.assertEquals("Double", 58 | asMap(MIMETypes.TEXT, "0.9"), 59 | toString.display(0.9)); 60 | Assert.assertEquals("String", 61 | asMap(MIMETypes.TEXT, "crunchy"), 62 | toString.display("crunchy")); 63 | Assert.assertEquals("Custom class", 64 | asMap(MIMETypes.TEXT, "CustomClass(sledding)"), 65 | toString.display(new CustomClass("sledding"))); 66 | Assert.assertEquals("int[]", 67 | asMap(MIMETypes.TEXT, "[34, 35, 36]"), 68 | toString.display(new int[] { 34, 35, 36 })); 69 | Assert.assertEquals("Object[]", 70 | asMap(MIMETypes.TEXT, "[CustomClass(tangerine), 34]"), 71 | toString.display(new Object[] { new CustomClass("tangerine"), 34 })); 72 | Assert.assertEquals("Object[]", 73 | asMap(MIMETypes.TEXT, "[CustomClass(tangerine), null, 34]"), 74 | toString.display(new Object[] { new CustomClass("tangerine"), null, 34 })); 75 | Assert.assertEquals("empty Object[]", 76 | asMap(MIMETypes.TEXT, "[]"), 77 | toString.display(new Object[] {})); 78 | } 79 | 80 | @Test 81 | public void testCustomDisplayer() { 82 | Displayers.register(CustomClass.class, new Displayer() { 83 | @Override 84 | public Map display(CustomClass obj) { 85 | return asMap(MIMETypes.TEXT, obj.field); 86 | } 87 | }); 88 | 89 | Assert.assertEquals("Object[] with custom displayer", 90 | asMap(MIMETypes.TEXT, "[tangerine, 34]"), 91 | toString.display(new Object[] { new CustomClass("tangerine"), 34 })); 92 | } 93 | 94 | private Map asMap(String mimeType, String asText) { 95 | Map result = new HashMap<>(); 96 | result.put(mimeType, asText); 97 | return result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/jupyter/Thing.java: -------------------------------------------------------------------------------- 1 | package jupyter; 2 | 3 | public class Thing { 4 | } 5 | --------------------------------------------------------------------------------