QueryRenderer.java

/*
 * Copyright 2024-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository.query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;

import org.jspecify.annotations.Nullable;

import org.springframework.util.Assert;
import org.springframework.util.CompositeIterator;

/**
 * Abstraction to encapsulate query expressions and render a query.
 * <p>
 * Query rendering consists of multiple building blocks:
 * <ul>
 * <li>{@link QueryTokens.SimpleQueryToken tokens} and {@link QueryTokens.ExpressionToken expression tokens}</li>
 * <li>{@link QueryRenderer compositions} such as a composition of multiple tokens.</li>
 * <li>{@link QueryRenderer expressions} that are individual parts such as {@code SELECT} or {@code ORDER BY ���}</li>
 * <li>{@link QueryRenderer inline expressions} such as composition of tokens and expressions such as function calls
 * with parenthesis {@code SOME_FUNCTION(ARGS)}</li>
 * </ul>
 *
 * @author Mark Paluch
 * @author Christoph Strobl
 */
abstract class QueryRenderer implements QueryTokenStream {

	/**
	 * Creates a QueryRenderer from a collection of {@link QueryToken}.
	 */
	static QueryRenderer from(Collection<? extends QueryToken> tokens) {
		List<QueryToken> tokensToUse = new ArrayList<>(Math.max(tokens.size(), 32));
		tokensToUse.addAll(tokens);
		return new TokenRenderer(tokensToUse);
	}

	/**
	 * Creates a QueryRenderer from a {@link QueryTokenStream}.
	 */
	static QueryRenderer from(QueryTokenStream tokens) {

		if (tokens instanceof QueryRendererBuilder builder) {
			tokens = builder.current;
		}

		if (tokens instanceof QueryRenderer renderer) {
			return renderer;
		}

		return new QueryStreamRenderer(tokens);
	}

	/**
	 * Creates a new empty {@link QueryRenderer}.
	 */
	public static QueryRenderer empty() {
		return EmptyQueryRenderer.INSTANCE;
	}

	/**
	 * Creates a new {@link QueryRendererBuilder}.
	 */
	static QueryRendererBuilder builder() {
		return new QueryRendererBuilder();
	}

	/**
	 * @return the rendered query.
	 */
	abstract String render();

	/**
	 * @return the rendered query.
	 */
	static String render(Iterable<QueryToken> tokenStream) {

		if (tokenStream instanceof QueryRendererBuilder qrb) {
			tokenStream = qrb.current;
		}

		if (tokenStream instanceof QueryRenderer qr) {
			return qr.render();
		}

		StringBuilder results = null;
		boolean previousExpression = false;

		Iterator<QueryToken> iterator = tokenStream.iterator();
		while (iterator.hasNext()) {
			QueryToken token = iterator.next();

			if (results == null) {
				if (iterator.hasNext()) {
					results = new StringBuilder();
				} else {
					return token.value();
				}
			}

			if (previousExpression) {
				if (!results.isEmpty() && results.charAt(results.length() - 1) != ' ') {
					results.append(' ');
				}
			}

			previousExpression = token.isExpression();
			results.append(token.value());
		}

		return results != null ? results.toString() : "";
	}

	/**
	 * Append a {@link QueryRenderer} to create a composed renderer.
	 */
	QueryRenderer append(QueryTokenStream tokens) {

		if (tokens instanceof QueryRendererBuilder builder) {
			tokens = builder.current;
		}

		if (tokens instanceof QueryRenderer qr) {

			if (isEmpty()) {
				return qr;
			}

			return CompositeRenderer.combine(this, qr);
		}

		if (isEmpty()) {
			return QueryRenderer.from(tokens);
		}

		return CompositeRenderer.combine(this, QueryRenderer.from(tokens));
	}

	@Override
	public String toString() {
		return render();
	}

	public static QueryRenderer ofExpression(QueryTokenStream tokenStream) {

		if (tokenStream instanceof ExpressionRenderer er) {
			return er;
		}

		if (tokenStream instanceof QueryRendererBuilder builder) {
			tokenStream = builder.current;
		}

		if (tokenStream.isEmpty()) {
			return EmptyQueryRenderer.INSTANCE;
		}

		if (!(tokenStream instanceof QueryRenderer)) {
			tokenStream = QueryRenderer.from(tokenStream);
		}

		if (tokenStream.isExpression()) {
			return (QueryRenderer) tokenStream;
		}

		return new ExpressionRenderer((QueryRenderer) tokenStream);
	}

	public static QueryRenderer inline(QueryTokenStream tokenStream) {

		Assert.notNull(tokenStream, "QueryTokenStream must not be null!");

		if (tokenStream instanceof InlineRenderer ilr) {
			return ilr;
		}

		if (tokenStream instanceof QueryRendererBuilder builder) {
			tokenStream = builder.current;
		}

		if (tokenStream.isEmpty()) {
			return EmptyQueryRenderer.INSTANCE;
		}

		if (!(tokenStream instanceof QueryRenderer)) {
			tokenStream = QueryRenderer.from(tokenStream);
		}

		return new InlineRenderer((QueryRenderer) tokenStream);
	}

	/**
	 * Composed renderer consisting of one or more QueryRenderers.
	 */
	static class CompositeRenderer extends QueryRenderer {

		private final List<QueryRenderer> nested;

		static CompositeRenderer combine(QueryRenderer root, QueryRenderer nested) {

			List<QueryRenderer> queryRenderers = new ArrayList<>(32);
			queryRenderers.add(root);
			queryRenderers.add(nested);

			return new CompositeRenderer(queryRenderers);
		}

		private CompositeRenderer(List<QueryRenderer> nested) {
			this.nested = nested;
		}

		@Override
		String render() {

			StringBuilder builder = new StringBuilder(64);
			String lastAppended = null;

			boolean lastExpression = false;
			for (QueryRenderer queryRenderer : nested) {

				if (lastAppended != null && (lastExpression || queryRenderer.isExpression()) && !builder.isEmpty()
						&& (!lastAppended.endsWith(" ") && !lastAppended.endsWith("("))) {
					builder.append(' ');
				}

				lastAppended = queryRenderer.render();
				builder.append(lastAppended);
				lastExpression = queryRenderer.isExpression();
			}

			return builder.toString();
		}

		/**
		 * Append a {@link QueryRenderer} to create a composed renderer.
		 */
		QueryRenderer append(QueryTokenStream tokens) {

			if (tokens instanceof QueryRendererBuilder builder) {
				tokens = builder.current;
			}

			if (tokens instanceof QueryRenderer qr) {

				if (isEmpty()) {
					return this;
				}

				if (qr.isEmpty()) {
					return qr;
				}

				if (tokens instanceof CompositeRenderer cr) {
					this.nested.addAll(cr.nested);

					return this;
				}

			}

			return super.append(tokens);
		}

		@Override
		public @Nullable QueryToken getLast() {

			for (int i = nested.size() - 1; i > -1; i--) {

				QueryRenderer renderer = nested.get(i);

				if (!renderer.isEmpty()) {
					return renderer.getLast();
				}
			}

			return null;
		}

		@Override
		public Iterator<QueryToken> iterator() {

			CompositeIterator<QueryToken> iterator = new CompositeIterator<>();
			for (QueryTokenStream stream : nested) {
				iterator.add(stream.iterator());
			}
			return iterator;
		}

		@Override
		public boolean isEmpty() {

			for (QueryRenderer renderer : nested) {
				if (!renderer.isEmpty()) {
					return false;
				}
			}

			return true;
		}

		@Override
		public int size() {

			int size = 0;

			for (QueryTokenStream stream : nested) {
				size += stream.size();
			}
			return size;
		}

		@Override
		public boolean isExpression() {
			return !nested.isEmpty() && nested.get(nested.size() - 1).isExpression();
		}

	}

	/**
	 * Renderer using {@link QueryTokens.SimpleQueryToken}.
	 */
	static class TokenRenderer extends QueryRenderer {

		private final List<QueryToken> tokens;

		TokenRenderer(List<QueryToken> tokens) {
			this.tokens = tokens;
		}

		@Override
		String render() {
			return render(tokens);
		}

		@Override
		public Stream<QueryToken> stream() {
			return tokens.stream();
		}

		@Override
		public Iterator<QueryToken> iterator() {
			return tokens.iterator();
		}

		@Override
		public List<QueryToken> toList() {
			return tokens;
		}

		@Override
		public @Nullable QueryToken getFirst() {
			return tokens.isEmpty() ? null : tokens.get(0);
		}

		@Override
		public @Nullable QueryToken getLast() {
			return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1);
		}

		@Override
		public int size() {
			return tokens.size();
		}

		@Override
		public boolean isEmpty() {
			return tokens.isEmpty();
		}

		@Override
		public boolean isExpression() {
			return !tokens.isEmpty() && getRequiredLast().isExpression();
		}

	}

	static class QueryStreamRenderer extends QueryRenderer {

		private final QueryTokenStream tokens;

		public QueryStreamRenderer(QueryTokenStream tokens) {
			this.tokens = tokens;
		}

		@Override
		String render() {
			return render(tokens);
		}

		@Override
		public Iterator<QueryToken> iterator() {
			return tokens.iterator();
		}

		@Override
		public @Nullable QueryToken getFirst() {
			return tokens.getFirst();
		}

		@Override
		public @Nullable QueryToken getLast() {
			return tokens.getLast();
		}

		@Override
		public int size() {
			return tokens.size();
		}

		@Override
		public boolean isEmpty() {
			return tokens.isEmpty();
		}

		@Override
		public boolean isExpression() {
			return !tokens.isEmpty() && tokens.getRequiredLast().isExpression();
		}
	}

	/**
	 * Builder for {@link QueryRenderer}.
	 */
	static class QueryRendererBuilder implements QueryTokenStream {

		protected QueryRenderer current = QueryRenderer.empty();

		/**
		 * Append a collection of {@link QueryToken}s.
		 *
		 * @param tokens
		 * @return {@code this} builder.
		 */
		QueryRendererBuilder append(List<? extends QueryToken> tokens) {
			return append(QueryRenderer.from(tokens));
		}

		/**
		 * Append a QueryRenderer.
		 *
		 * @param stream
		 * @return {@code this} builder.
		 */
		QueryRendererBuilder append(QueryTokenStream stream) {

			if (stream.isEmpty()) {
				return this;
			}

			current = current.append(stream);

			return this;
		}

		/**
		 * Append a QueryRenderer inline.
		 *
		 * @param stream
		 * @return {@code this} builder.
		 */
		QueryRendererBuilder appendInline(QueryTokenStream stream) {

			if (stream.isEmpty()) {
				return this;
			}

			current = current.append(QueryRenderer.inline(stream));

			return this;
		}

		/**
		 * Append a QueryRendererBuilder as expression.
		 *
		 * @param builder
		 * @return {@code this} builder.
		 */
		QueryRendererBuilder appendExpression(QueryRendererBuilder builder) {
			return appendExpression(builder.current);
		}

		/**
		 * Append a QueryRenderer as expression.
		 *
		 * @param tokens
		 * @return {@code this} builder.
		 */
		QueryRendererBuilder appendExpression(QueryTokenStream tokens) {

			if (tokens.isEmpty()) {
				return this;
			}

			current = current.append(QueryRenderer.ofExpression(tokens));

			return this;
		}

		@Override
		public List<QueryToken> toList() {
			return current.toList();
		}

		@Override
		public Stream<QueryToken> stream() {
			return current.stream();
		}

		@Override
		public @Nullable QueryToken getFirst() {
			return current.getFirst();
		}

		@Override
		public @Nullable QueryToken getLast() {
			return current.getLast();
		}

		@Override
		public boolean isExpression() {
			return current.isExpression();
		}

		@Override
		public boolean isEmpty() {
			return current.isEmpty();
		}

		@Override
		public int size() {
			return current.size();
		}

		@Override
		public Iterator<QueryToken> iterator() {
			return current.iterator();
		}

		@Override
		public String toString() {
			return current.render();
		}

		public QueryRenderer build() {
			return current;
		}

		public QueryRenderer toInline() {
			return new InlineRenderer(current);
		}
	}

	private static class InlineRenderer extends QueryRenderer {

		private final QueryRenderer delegate;

		private InlineRenderer(QueryRenderer delegate) {
			this.delegate = delegate;
		}

		@Override
		String render() {
			return delegate.render();
		}

		@Override
		public Stream<QueryToken> stream() {
			return delegate.stream();
		}

		@Override
		public List<QueryToken> toList() {
			return delegate.toList();
		}

		@Override
		public Iterator<QueryToken> iterator() {
			return delegate.iterator();
		}

		@Override
		public @Nullable QueryToken getFirst() {
			return delegate.getFirst();
		}

		@Override
		public @Nullable QueryToken getLast() {
			return delegate.getLast();
		}

		@Override
		public boolean isEmpty() {
			return delegate.isEmpty();
		}

		@Override
		public int size() {
			return delegate.size();
		}

		@Override
		public boolean isExpression() {
			return false;
		}
	}

	private static class ExpressionRenderer extends QueryRenderer {

		private final QueryRenderer delegate;

		private ExpressionRenderer(QueryRenderer delegate) {
			this.delegate = delegate;
		}

		@Override
		String render() {
			return delegate.render();
		}

		@Override
		public Stream<QueryToken> stream() {
			return delegate.stream();
		}

		@Override
		public List<QueryToken> toList() {
			return delegate.toList();
		}

		@Override
		public Iterator<QueryToken> iterator() {
			return delegate.iterator();
		}

		@Override
		public @Nullable QueryToken getFirst() {
			return delegate.getFirst();
		}

		@Override
		public @Nullable QueryToken getLast() {
			return delegate.getLast();
		}

		@Override
		public boolean isEmpty() {
			return delegate.isEmpty();
		}

		@Override
		public int size() {
			return delegate.size();
		}

		@Override
		public boolean isExpression() {
			return true;
		}

	}

	private static class EmptyQueryRenderer extends QueryRenderer {

		public static final QueryRenderer INSTANCE = new EmptyQueryRenderer();

		@Override
		String render() {
			return "";
		}

		@Override
		QueryRenderer append(QueryTokenStream tokens) {

			if (tokens.isEmpty()) {
				return this;
			}

			if (tokens instanceof QueryRenderer qr) {
				return qr;
			}

			return QueryRenderer.from(tokens);
		}

		@Override
		public List<QueryToken> toList() {
			return Collections.emptyList();
		}

		@Override
		public Stream<QueryToken> stream() {
			return Stream.empty();
		}

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

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

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

		@Override
		public boolean isExpression() {
			return false;
		}
	}
}