org.dflib.jjava.jupyter.kernel.display.Renderer Maven / Gradle / Ivy
package org.dflib.jjava.jupyter.kernel.display;
import org.dflib.jjava.jupyter.kernel.display.mime.MIMEType;
import org.dflib.jjava.jupyter.kernel.util.InheritanceIterator;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A default renderer may be set and maps a group to a specific subtype.
*
* A suffix may be mapped to a type.
*
* A type has a default mime type (may be a list) and is always also rendered
* as text/plain with toString().
*
* A type must also have other supported types.
*
* Objects that implement the render interface override their default renders
* but in the event that renderAs (or displayAs) is invoked the specified types
* override the defaults.
*/
public class Renderer {
private static class RenderFunctionProps {
private final RenderFunction function;
private final Set supportedTypes;
private final Set preferredTypes;
public RenderFunctionProps(RenderFunction function, Set supportedTypes, Set preferredTypes) {
this.function = function;
this.supportedTypes = supportedTypes;
this.preferredTypes = preferredTypes;
}
public RenderFunction getFunction() {
return function;
}
public Set getSupportedTypes() {
return supportedTypes;
}
public Set getPreferredTypes() {
return preferredTypes;
}
}
public class RenderRegistration {
private final Set supported;
private final Set preferred;
private final Set> types;
public RenderRegistration(Class extends T> type) {
this.supported = new LinkedHashSet<>();
this.preferred = new LinkedHashSet<>();
this.types = new LinkedHashSet<>();
this.types.add(type);
}
public RenderRegistration supporting(MIMEType... types) {
Collections.addAll(this.supported, types);
return this;
}
public RenderRegistration preferring(MIMEType... types) {
supporting(types);
Collections.addAll(this.preferred, types);
return this;
}
public RenderRegistration supporting(String... types) {
for (String type : types)
this.supported.add(MIMEType.parse(type));
return this;
}
public RenderRegistration preferring(String... types) {
supporting(types);
for (String type : types)
this.preferred.add(MIMEType.parse(type));
return this;
}
public RenderRegistration onType(Class extends T> type) {
this.types.add(type);
return this;
}
public void register(RenderFunction function) {
Set supported = this.supported.isEmpty() ? DisplayDataRenderable.ANY : this.supported;
Set preferred = this.preferred.isEmpty() ? supported : this.preferred;
Renderer.this.register(supported, preferred, types, function);
}
}
private final Map> renderFunctions;
private final Map suffixMappings;
public Renderer() {
this.renderFunctions = new HashMap<>();
this.suffixMappings = new HashMap<>();
}
public RenderRegistration createRegistration(Class type) {
return new RenderRegistration<>(type);
}
public void register(Set supported, Set preferred, Set> types, RenderFunction function) {
RenderFunctionProps props = new RenderFunctionProps(function, supported, preferred);
types.forEach(c -> this.renderFunctions.compute(c, (k, v) -> {
List functions = v != null ? v : new LinkedList<>();
functions.add(props);
return functions;
}));
}
private static DisplayData finalizeDisplayData(DisplayData data, Object value) {
if (!data.hasDataForType(MIMEType.TEXT_PLAIN))
data.putText(String.valueOf(value));
return data;
}
/**
* Render the object with the preferred render type.
*
* The rendering algorithm is as follows:
*
* -
* The object is rendered as {@code text/plain} with {@link String#valueOf(Object)}.
*
* -
* If the object is {@link DisplayDataRenderable} ask it to render itself as the {@link DisplayDataRenderable#getPreferredRenderTypes() preferred types}.
*
* -
* Else iterate over the implemented with the {@link InheritanceIterator} until a render function is found. Use this
* function to render the object.
*
*
*
* @param value the object to render.
* @param params a map of parameters that render functions may use.
*
* @return the data container holding the rendered view of the {@code value}.
*/
@SuppressWarnings("unchecked")
public DisplayData render(Object value, Map params) {
DisplayData out = new DisplayData();
if (value instanceof DisplayDataRenderable) {
DisplayDataRenderable renderable = (DisplayDataRenderable) value;
RenderRequestTypes.Builder requestTypes = new RenderRequestTypes.Builder(this.suffixMappings::get);
requestTypes.withType(MIMEType.TEXT_PLAIN);
renderable.getPreferredRenderTypes().forEach(requestTypes::withType);
renderable.render(new RenderContext(requestTypes.build(), this, params, out));
return finalizeDisplayData(out, value);
}
Iterator inheritedTypes = new InheritanceIterator(value.getClass());
while (inheritedTypes.hasNext()) {
Class type = inheritedTypes.next();
List allRenderFunctionProps = this.renderFunctions.get(type);
if (allRenderFunctionProps != null && !allRenderFunctionProps.isEmpty()) {
for (RenderFunctionProps renderFunctionProps : allRenderFunctionProps) {
RenderRequestTypes.Builder requestTypes = new RenderRequestTypes.Builder(this.suffixMappings::get);
requestTypes.withType(MIMEType.TEXT_PLAIN);
renderFunctionProps.getPreferredTypes().forEach(requestTypes::withType);
renderFunctionProps.getFunction().render(
value,
new RenderContext(requestTypes.build(), this, params, out)
);
}
return finalizeDisplayData(out, value);
}
}
return finalizeDisplayData(out, value);
}
/**
* A {@link #render(Object, Map)} variant that supplies an empty parameter map.
*
* @param value the object to render.
*
* @return a {@link DisplayData} container with all the rendered data.
*/
public DisplayData render(Object value) {
return render(value, new LinkedHashMap<>());
}
/**
* Render the object as the specified types if possible.
*
* The rendering algorithm is as follows:
*
* -
* The object is rendered as {@code text/plain} with {@link String#valueOf(Object)} no
* matter what types are requested.
*
* -
* If the object is {@link DisplayDataRenderable} and any of it's {@link DisplayDataRenderable#getSupportedRenderTypes() supported types}
* are requested, it is asked to render itself.
*
* -
* While all of the requested types have not be rendered yet:
*
* -
* For every type in the {@link InheritanceIterator}, apply the same scheme as step 2.
*
* -
* Remove all rendered types from the request.
*
*
*
*
*
* @param value the object to render.
* @param params a map of parameters that render functions may use.
* @param types the {@link MIMEType#parse(String) MIME types} to render the object as.
*
* @return a {@link DisplayData} container with all the rendered data.
*/
@SuppressWarnings("unchecked")
public DisplayData renderAs(Object value, Map params, String... types) {
DisplayData out = new DisplayData();
RenderRequestTypes.Builder builder = new RenderRequestTypes.Builder(this.suffixMappings::get);
builder.withType(MIMEType.TEXT_PLAIN);
for (String type : types)
builder.withType(type);
RenderRequestTypes requestTypes = builder.build();
RenderContext context = new RenderContext(requestTypes, this, params, out);
if (value instanceof DisplayDataRenderable) {
DisplayDataRenderable renderable = (DisplayDataRenderable) value;
if (requestTypes.anyRequestedIsSupported(renderable.getSupportedRenderTypes())) {
renderable.render(context);
requestTypes.removeFulfilledRequests(out);
}
}
Iterator inheritedTypes = new InheritanceIterator(value.getClass());
while (inheritedTypes.hasNext() && !requestTypes.isEmpty()) {
Class type = inheritedTypes.next();
List allRenderFunctionProps = this.renderFunctions.get(type);
if (allRenderFunctionProps != null) {
for (RenderFunctionProps renderFunctionProps : allRenderFunctionProps) {
if (requestTypes.anyRequestedIsSupported(renderFunctionProps.getSupportedTypes())) {
renderFunctionProps.getFunction().render(value, context);
requestTypes.removeFulfilledRequests(out);
}
}
}
}
return finalizeDisplayData(out, value);
}
/**
* A {@link #renderAs(Object, Map, String...)} variant that supplies an empty parameter map.
*
* @param value the object to render.
* @param types the {@link MIMEType#parse(String) MIME types} to render the object as.
*
* @return a {@link DisplayData} container with all the rendered data.
*/
public DisplayData renderAs(Object value, String... types) {
return this.renderAs(value, new LinkedHashMap<>(), types);
}
}