ParentReferenceChecker.java

/*******************************************************************************
 * Copyright (c) 2022 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.query.algebra.evaluation.optimizer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayDeque;

import org.eclipse.rdf4j.common.annotation.InternalUseOnly;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.Dataset;
import org.eclipse.rdf4j.query.algebra.QueryModelNode;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizer;
import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Checks {@link QueryModelNode#getParentNode()} references that have become inconsistent with the actual algebra tree
 * structure due to optimization operations. Used during testing.
 *
 * @author Jeen Broekstra
 * @author H��vard Ottestad
 */
@InternalUseOnly
public class ParentReferenceChecker implements QueryOptimizer {

	private static final Logger logger = LoggerFactory.getLogger(ParentReferenceChecker.class);

	public static boolean skip = false;

	private final QueryOptimizer previousOptimizerInPipeline;

	public ParentReferenceChecker(QueryOptimizer previousOptimizerInPipeline) {
		this.previousOptimizerInPipeline = previousOptimizerInPipeline;
	}

	@Override
	public void optimize(TupleExpr tupleExpr, Dataset dataset, BindingSet bindings) {
		if (skip) {
			return;
		}

		verifySerializable(tupleExpr);
		tupleExpr.visit(new ParentCheckingVisitor(ParentReferenceChecker.this.previousOptimizerInPipeline));
	}

	private void verifySerializable(QueryModelNode tupleExpr) {

		byte[] bytes = objectToBytes(tupleExpr);
		QueryModelNode parsed = (QueryModelNode) bytesToObject(bytes);
//		byte[] bytesAfterSecondSerialization = objectToBytes(parsed);
//		assert Arrays.equals(bytes, bytesAfterSecondSerialization);
		assert tupleExpr.equals(parsed);
	}

	private byte[] objectToBytes(Serializable object) {
		try (var byteArrayOutputStream = new ByteArrayOutputStream()) {
			try (var objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
				objectOutputStream.writeObject(object);
			}
			return byteArrayOutputStream.toByteArray();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private Object bytesToObject(byte[] str) {
		try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(str)) {
			try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
				return objectInputStream.readObject();
			}
		} catch (IOException | ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

	private final static class ParentCheckingVisitor extends AbstractQueryModelVisitor<RuntimeException> {

		private final ArrayDeque<QueryModelNode> ancestors = new ArrayDeque<>();
		private final String previousOptimizer;

		public ParentCheckingVisitor(QueryOptimizer previousOptimizerInPipeline) {
			if (previousOptimizerInPipeline != null) {
				this.previousOptimizer = previousOptimizerInPipeline.getClass().getSimpleName();
			} else
				this.previousOptimizer = "query parsing";
		}

		@Override
		protected void meetNode(QueryModelNode node) throws RuntimeException {
			QueryModelNode expectedParent = ancestors.peekLast();
			if (node.getParentNode() != expectedParent) {
				String message = "After " + previousOptimizer + " there was an unexpected parent for node \n" + node
						+ "\nwith parent: \n" + node.getParentNode() + "\nexpected: \n" + expectedParent;
				assert node.getParentNode() == expectedParent : message;
			}

			ancestors.addLast(node);
			super.meetNode(node);
			ancestors.pollLast();
		}
	}
}