IrDebug.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.queryrender.sparql.ir.util;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrNode;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

/**
 * Lightweight IR debug printer using Gson pretty printing.
 *
 * Produces objects of the form {"class": "<FQN>", "data": {...}} so it is easy to see the concrete IR node type in
 * dumps. Several noisy fields from RDF4J algebra nodes are excluded to keep output focused on relevant structure.
 */
public final class IrDebug {
	private final static Set<String> ignore = Set.of("parent", "costEstimate", "totalTimeNanosActual", "cardinality",
			"cachedHashCode", "isVariableScopeChange", "resultSizeEstimate", "resultSizeActual");

	private IrDebug() {
	}

	public static String dump(IrNode node) {

		Gson gson = new GsonBuilder().setPrettyPrinting()
				.registerTypeAdapter(Var.class, new VarSerializer())
//				.registerTypeAdapter(IrNode.class, new ClassNameAdapter<IrNode>())
				.registerTypeAdapterFactory(new OrderedAdapterFactory())
				.setExclusionStrategies(new ExclusionStrategy() {
					@Override
					public boolean shouldSkipField(FieldAttributes f) {
						// Exclude noisy fields that do not help understanding the IR shape
						return ignore.contains(f.getName());

					}

					@Override
					public boolean shouldSkipClass(Class<?> clazz) {
						// We don't want to skip entire classes, so return false
						return false;
					}
				})

				.create();
		return gson.toJson(node);
	}

	static class VarSerializer implements JsonSerializer<Var> {
		@Override
		public JsonElement serialize(Var src, Type typeOfSrc, JsonSerializationContext context) {
			// Turn Var into a JSON string using its toString()
			return new JsonPrimitive(src.toString().replace("=", ": "));
		}
	}

//	static class ClassNameAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
//		@Override
//		public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
//			JsonObject obj = new JsonObject();
//			obj.addProperty("class", src.getClass().getName());
//			obj.add("data", context.serialize(src));
//			return obj;
//		}
//
//		@Override
//		public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
//				throws JsonParseException {
//			JsonObject obj = json.getAsJsonObject();
//			String className = obj.get("class").getAsString();
//			try {
//				Class<?> clazz = Class.forName(className);
//				return context.deserialize(obj.get("data"), clazz);
//			} catch (ClassNotFoundException e) {
//				throw new JsonParseException(e);
//			}
//		}
//	}

	static class OrderedAdapterFactory implements TypeAdapterFactory {
		@Override
		public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
			Class<? super T> raw = type.getRawType();

			// Only wrap bean-like classes
			if (raw.isPrimitive()
					|| Number.class.isAssignableFrom(raw)
					|| CharSequence.class.isAssignableFrom(raw)
					|| Boolean.class.isAssignableFrom(raw)
					|| raw.isEnum()
					|| Collection.class.isAssignableFrom(raw)
					|| Map.class.isAssignableFrom(raw)) {
				return null;
			}

			final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

			return new TypeAdapter<T>() {
				@Override
				public void write(JsonWriter out, T value) throws IOException {
					if (value == null) {
						out.nullValue();
						return;
					}

					// Produce a detached tree
					JsonElement tree = delegate.toJsonTree(value);

					if (tree.isJsonObject()) {
						JsonObject obj = tree.getAsJsonObject();
						JsonObject reordered = new JsonObject();

						// primitives
						obj.entrySet()
								.stream()
								.filter(e -> e.getValue().isJsonPrimitive())
								.forEach(e -> reordered.add(e.getKey(), e.getValue()));

						// arrays
						obj.entrySet()
								.stream()
								.filter(e -> e.getValue().isJsonArray())
								.forEach(e -> reordered.add(e.getKey(), e.getValue()));

						// objects
						obj.entrySet()
								.stream()
								.filter(e -> e.getValue().isJsonObject())
								.forEach(e -> reordered.add(e.getKey(), e.getValue()));

						// Directly dump reordered element into the writer
						Streams.write(reordered, out);
					} else {
						// Non-object ��� just dump as is
						Streams.write(tree, out);
					}
				}

				@Override
				public T read(JsonReader in) throws IOException {
					return delegate.read(in);
				}
			};
		}
	}
}