ArrayBindingSet.java

/*******************************************************************************
 * Copyright (c) 2020 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;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.eclipse.rdf4j.common.annotation.InternalUseOnly;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.AbstractBindingSet;
import org.eclipse.rdf4j.query.Binding;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.MutableBindingSet;
import org.eclipse.rdf4j.query.impl.SimpleBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An array implementation of the {@link BindingSet} interface.
 *
 * @author Jerven Bolleman
 */
@InternalUseOnly
public class ArrayBindingSet extends AbstractBindingSet implements MutableBindingSet {

	private static final long serialVersionUID = -1L;

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

	private final String[] bindingNames;

	// Creating a LinkedHashSet is expensive, so we should cache the binding names set
	private Set<String> bindingNamesSetCache;

	private final boolean[] whichBindingsHaveBeenSet;

	private final Value[] values;

	/**
	 * Creates a new Array-based BindingSet for the supplied bindings names. <em>The supplied list of binding names is
	 * assumed to be constant</em>; care should be taken that the contents of this array doesn't change after supplying
	 * it to this solution.
	 *
	 * @param names The binding names.
	 */
	public ArrayBindingSet(String... names) {
		this.bindingNames = names;
		this.values = new Value[names.length];
		this.whichBindingsHaveBeenSet = new boolean[names.length];
	}

	public ArrayBindingSet(BindingSet toCopy, Set<String> names, String[] namesArray) {
		assert !(toCopy instanceof ArrayBindingSet);

		this.bindingNames = namesArray;
		this.whichBindingsHaveBeenSet = new boolean[this.bindingNames.length];
		this.values = new Value[this.bindingNames.length];
		for (int i = 0; i < this.bindingNames.length; i++) {
			Binding binding = toCopy.getBinding(this.bindingNames[i]);
			if (binding != null) {
				this.values[i] = binding.getValue();
				this.whichBindingsHaveBeenSet[i] = true;
			}
		}

	}

	public ArrayBindingSet(ArrayBindingSet toCopy, String... names) {
		this.bindingNames = names;
		this.values = Arrays.copyOf(toCopy.values, toCopy.values.length);
		this.whichBindingsHaveBeenSet = Arrays.copyOf(toCopy.whichBindingsHaveBeenSet,
				toCopy.whichBindingsHaveBeenSet.length);
	}

	/**
	 * This is used to generate a direct setter into the array to put a binding value into. Can be used to avoid many
	 * comparisons to the bindingNames.
	 *
	 * @param bindingName for which you want the setter
	 * @return the setter biconsumer which can operate on any ArrayBindingSet but should only be used on ones with an
	 *         identical bindingNames array. Otherwise returns null.
	 */
	public BiConsumer<Value, ArrayBindingSet> getDirectSetBinding(String bindingName) {
		int index = getIndex(bindingName);
		if (index == -1) {
			logger.error("Variable not known to ArrayBindingSet : " + bindingName);
			assert false : "Variable not known to ArrayBindingSet : " + bindingName;
			return null;
		}
		return (v, a) -> {
			a.values[index] = v;
			a.whichBindingsHaveBeenSet[index] = true;
			a.clearCache();
		};
	}

	public BiConsumer<Value, ArrayBindingSet> getDirectAddBinding(String bindingName) {
		int index = getIndex(bindingName);
		if (index == -1) {
			logger.error("Variable not known to ArrayBindingSet : " + bindingName);
			assert false : "Variable not known to ArrayBindingSet : " + bindingName;
			return null;
		}
		return (v, a) -> {
			assert !a.whichBindingsHaveBeenSet[index] : "variable already bound: " + bindingName;
			a.values[index] = v;
			a.whichBindingsHaveBeenSet[index] = true;
			a.clearCache();
		};

	}

	public Function<ArrayBindingSet, Binding> getDirectGetBinding(String bindingName) {

		int index = getIndex(bindingName);
		if (index == -1) {
			return null;
		}
		return (a) -> {
			Value value = a.values[index];
			if (value != null) {
				return new SimpleBinding(bindingName, value);
			} else {
				return null;
			}
		};

	}

	public Function<ArrayBindingSet, Value> getDirectGetValue(String bindingName) {
		int index = getIndex(bindingName);
		if (index == -1) {
			return null;
		}
		return (a) -> a.values[index];

	}

	public Function<ArrayBindingSet, Boolean> getDirectHasBinding(String bindingName) {
		int index = getIndex(bindingName);
		if (index == -1) {
			return null;
		}
		return (a) -> a.whichBindingsHaveBeenSet[index];
	}

	private int getIndex(String bindingName) {
		for (int i = 0; i < bindingNames.length; i++) {
			if (bindingNames[i] == bindingName) {
				return i;
			}
		}
		for (int i = 0; i < bindingNames.length; i++) {
			if (bindingNames[i].equals(bindingName)) {
				return i;
			}
		}
		return -1;
	}

	@Override
	public Set<String> getBindingNames() {
		if (bindingNamesSetCache == null) {
			int size = size();
			if (size == 0) {
				this.bindingNamesSetCache = Collections.emptySet();
			} else if (size == 1) {
				for (int i = 0; i < this.bindingNames.length; i++) {
					if (whichBindingsHaveBeenSet[i]) {
						this.bindingNamesSetCache = Collections.singleton(bindingNames[i]);
						break;
					}
				}
				assert this.bindingNamesSetCache != null;
			} else {
				LinkedHashSet<String> bindingNamesSetCache = new LinkedHashSet<>(size * 2);
				for (int i = 0; i < this.bindingNames.length; i++) {
					if (whichBindingsHaveBeenSet[i]) {
						bindingNamesSetCache.add(bindingNames[i]);
					}
				}
				this.bindingNamesSetCache = Collections.unmodifiableSet(bindingNamesSetCache);
			}
		}

		return bindingNamesSetCache;
	}

	@Override
	public Value getValue(String bindingName) {
		for (int i = 0; i < bindingNames.length; i++) {
			if (bindingNames[i] == bindingName && whichBindingsHaveBeenSet[i]) {
				return values[i];
			}
		}

		for (int i = 0; i < bindingNames.length; i++) {
			if (bindingNames[i].equals(bindingName) && whichBindingsHaveBeenSet[i]) {
				return values[i];
			}
		}
		return null;
	}

	@Override
	public Binding getBinding(String bindingName) {
		Value value = getValue(bindingName);

		if (value != null) {
			return new SimpleBinding(bindingName, value);
		}

		return null;
	}

	@Override
	public boolean hasBinding(String bindingName) {
		int index = getIndex(bindingName);
		if (index == -1) {
			return false;
		}
		return whichBindingsHaveBeenSet[index];
	}

	@Override
	public Iterator<Binding> iterator() {
		return new ArrayBindingSetIterator();
	}

	@Override
	public int size() {
		int size = 0;

		for (boolean value : whichBindingsHaveBeenSet) {
			if (value) {
				size++;
			}
		}

		return size;
	}

	List<String> sortedBindingNames = null;

	public List<String> getSortedBindingNames() {
		if (sortedBindingNames == null) {
			int size = size();

			if (size == 1) {
				for (int i = 0; i < bindingNames.length; i++) {
					if (whichBindingsHaveBeenSet[i]) {
						sortedBindingNames = Collections.singletonList(bindingNames[i]);
					}
				}
			} else {
				ArrayList<String> names = new ArrayList<>(size);
				for (int i = 0; i < bindingNames.length; i++) {
					if (whichBindingsHaveBeenSet[i]) {
						names.add(bindingNames[i]);
					}
				}
				names.sort(String::compareTo);
				sortedBindingNames = names;
			}
		}

		return sortedBindingNames;
	}

	/*------------------------------------*
	 * Inner class ArrayBindingSetIterator *
	 *------------------------------------*/

	private class ArrayBindingSetIterator implements Iterator<Binding> {

		private int index = 0;

		public ArrayBindingSetIterator() {
		}

		@Override
		public boolean hasNext() {
			for (; index < values.length; index++) {
				if (whichBindingsHaveBeenSet[index] && values[index] != null) {
					return true;
				}
			}
			return false;
		}

		@Override
		public Binding next() {
			for (; index < values.length; index++) {
				if (whichBindingsHaveBeenSet[index] && values[index] != null) {
					break;
				}
			}

			String name = bindingNames[index];
			Value value = values[index++];
			if (value != null) {
				return new SimpleBinding(name, value);
			} else {
				return null;
			}
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	@Override
	public void addBinding(Binding binding) {
		int index = getIndex(binding.getName());
		if (index == -1) {
			logger.error(
					"We don't actually support adding a binding. " + binding.getName() + " : " + binding.getValue());
			assert false
					: "We don't actually support adding a binding. " + binding.getName() + " : " + binding.getValue();
			return;
		}

		assert !this.whichBindingsHaveBeenSet[index];
		this.values[index] = binding.getValue();
		this.whichBindingsHaveBeenSet[index] = true;
		clearCache();

	}

	@Override
	public void setBinding(Binding binding) {
		int index = getIndex(binding.getName());
		if (index == -1) {
			return;
		}
		this.values[index] = binding.getValue();
		this.whichBindingsHaveBeenSet[index] = true;
		clearCache();
	}

	@Override
	public void setBinding(String name, Value value) {
		int index = getIndex(name);
		if (index == -1) {
			return;
		}

		this.values[index] = value;
		this.whichBindingsHaveBeenSet[index] = value != null;
		clearCache();
	}

	@Override
	public boolean isEmpty() {
		for (int index = 0; index < values.length; index++) {
			if (whichBindingsHaveBeenSet[index]) {
				return false;
			}
		}
		return true;
	}

	private void clearCache() {
		bindingNamesSetCache = null;
	}
}