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.NoSuchElementException;
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.model.util.Values;
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 static final Value NULL_VALUE = Values
			.iri("urn:null:d57c56f3-41a9-468e-8dce-5706ebdef84c_e88d9e52-27cb-4056-a889-1ea353fa6f0c");

	private final String[] bindingNames;

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

	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.empty = true;
	}

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

		this.bindingNames = namesArray;
		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();
				if (this.values[i] == null) {
					this.values[i] = NULL_VALUE;
				}
			} else if (hasBinding(this.bindingNames[i])) {
				this.values[i] = NULL_VALUE;
			}
		}
		this.empty = toCopy.isEmpty();
		assert !this.empty || size() == 0;

	}

	public ArrayBindingSet(ArrayBindingSet toCopy, String... names) {
		this.bindingNames = names;

		this.values = Arrays.copyOf(toCopy.values, toCopy.values.length);
		this.empty = toCopy.empty;
		assert !this.empty || size() == 0;
	}

	/**
	 * 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 == null ? NULL_VALUE : v;
			a.empty = false;
			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.values[index] == null;
			a.values[index] = v == null ? NULL_VALUE : v;
			a.empty = false;
			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_VALUE) {
				value = null;
			}
			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] == NULL_VALUE ? null : a.values[index];

	}

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

	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 (isEmpty()) {
			return Collections.emptySet();
		}

		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 (values[i] != null) {
						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 (values[i] != null) {
						bindingNamesSetCache.add(bindingNames[i]);
					}
				}
				this.bindingNamesSetCache = Collections.unmodifiableSet(bindingNamesSetCache);
			}
		}

		return bindingNamesSetCache;
	}

	@Override
	public Value getValue(String bindingName) {
		if (isEmpty()) {
			return null;
		}

		for (int i = 0; i < bindingNames.length; i++) {
			if (bindingNames[i] == bindingName && values[i] != null) {
				return values[i] == NULL_VALUE ? null : values[i];
			}
		}

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

	@Override
	public Binding getBinding(String bindingName) {
		if (isEmpty()) {
			return null;
		}

		Value value = getValue(bindingName);
		if (value == NULL_VALUE) {
			value = null;
		}

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

		return null;
	}

	@Override
	public boolean hasBinding(String bindingName) {
		if (isEmpty()) {
			return false;
		}

		int index = getIndex(bindingName);
		if (index == -1) {
			return false;
		}
		return values[index] != null;
	}

	@Override
	public Iterator<Binding> iterator() {
		if (isEmpty()) {
			return Collections.emptyIterator();
		}

		return new ArrayBindingSetIterator();
	}

	@Override
	public int size() {
		if (isEmpty()) {
			return 0;
		}

		int size = 0;

		for (Value value : values) {
			if (value != null) {
				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 (values[i] != null) {
						sortedBindingNames = Collections.singletonList(bindingNames[i]);
					}
				}
			} else {
				ArrayList<String> names = new ArrayList<>(size);
				for (int i = 0; i < bindingNames.length; i++) {
					if (values[i] != null) {
						names.add(bindingNames[i]);
					}
				}
				names.sort(String::compareTo);
				sortedBindingNames = names;
			}
		}

		return sortedBindingNames;
	}

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

		assert this.values[index] == null;
		this.values[index] = value == null ? NULL_VALUE : value;
		empty = false;
		clearCache();
	}

	@Override
	public void setBinding(Binding binding) {
		int index = getIndex(binding.getName());
		if (index == -1) {
			return;
		}
		Value value = binding.getValue();
		this.values[index] = value == null ? NULL_VALUE : value;
		empty = false;
		clearCache();
	}

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

		this.values[index] = value;
		if (value == null) {
			this.empty = true;
			for (Value value1 : this.values) {
				if (value1 != null) {
					this.empty = false;
					break;
				}
			}
		} else {
			this.empty = false;
		}
		clearCache();
	}

	@Override
	public boolean isEmpty() {
		return empty;
	}

	private void clearCache() {
		bindingNamesSetCache = null;
	}

	public void addAll(ArrayBindingSet other) {
		if (other.bindingNames == bindingNames) {
			for (int i = 0; i < bindingNames.length; i++) {
				if (other.values[i] != null) {
					this.values[i] = other.values[i];
					this.empty = false;
				}
			}
		} else {
			for (int i = 0; i < bindingNames.length; i++) {
				if (other.hasBinding(bindingNames[i])) {
					Value value = other.getValue(bindingNames[i]);
					this.values[i] = value == null ? NULL_VALUE : value;
					this.empty = false;
				}
			}
		}

		clearCache();

	}

	private class ArrayBindingSetIterator implements Iterator<Binding> {

		private int index = 0;

		public ArrayBindingSetIterator() {
		}

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

		@Override
		public Binding next() {
			while (index < values.length) {
				if (values[index] != null) {
					String name = bindingNames[index];
					Value value = values[index++];
					if (value == NULL_VALUE) {
						value = null;
					}
					if (value != null) {
						return new SimpleBinding(name, value);
					} else {
						return null;
					}
				}
				index++;
			}

			throw new NoSuchElementException();
		}

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

}