ParameterBindingJsonReader.java

/*
 * Copyright 2008-present 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.mongodb.util.json;

import static java.lang.String.*;

import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeParseException;
import java.util.Base64;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.bson.*;
import org.bson.json.JsonParseException;
import org.bson.types.Decimal128;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;

/**
 * Reads a JSON and evaluates placehoders and SpEL expressions. Modified version of <a href=
 * "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/JsonReader.java">MongoDB Inc.
 * JsonReader</a> licensed under the Apache License, Version 2.0. <br />
 *
 * @author Jeff Yemin
 * @author Ross Lawley
 * @author Trisha Gee
 * @author Robert Guo
 * @author Florian Buecklers
 * @author Brendon Puntin
 * @author Christoph Strobl
 * @author Rocco Lagrotteria
 * @since 2.2
 */
@NullUnmarked
public class ParameterBindingJsonReader extends AbstractBsonReader {

	private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:][#$]\\{.*\\}$");
	private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
	private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:][#$]\\{.*\\}");
	private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");

	private static final String QUOTE_START = "\\Q";
	private static final String ESCAPED_QUOTE_START = "\\" + QUOTE_START;
	private static final String QUOTE_REPLACEMENT_QUOTE_START = Matcher.quoteReplacement(ESCAPED_QUOTE_START);
	private static final String QUOTE_END = "\\E";
	private static final String ESCAPED_QUOTE_END = "\\" + QUOTE_END;
	private static final String QUOTE_REPLACEMENT_QUOTE_END = Matcher.quoteReplacement(ESCAPED_QUOTE_END);

	private final ParameterBindingContext bindingContext;

	private final JsonScanner scanner;
	private JsonToken pushedToken;
	Object currentValue;

	/**
	 * Constructs a new instance with the given JSON string.
	 *
	 * @param json A string representation of a JSON.
	 */
	public ParameterBindingJsonReader(final String json) {
		this(json, new Object[] {});
	}

	// Spring Data Customization START

	/**
	 * Constructs a new instance with the given JSON string.
	 *
	 * @param json A string representation of a JSON.
	 */
	public ParameterBindingJsonReader(String json, Object[] values) {

		this(json, (index) -> values[index], new SpelExpressionParser(),
				EvaluationContextProvider.DEFAULT.getEvaluationContext(values));
	}

	public ParameterBindingJsonReader(String json, ValueProvider accessor, SpelExpressionParser spelExpressionParser,
			EvaluationContext evaluationContext) {

		this(json, accessor, spelExpressionParser, () -> evaluationContext);
	}

	/**
	 * @since 2.2.3
	 */
	public ParameterBindingJsonReader(String json, ValueProvider accessor, SpelExpressionParser spelExpressionParser,
			Supplier<EvaluationContext> evaluationContext) {

		this(json, new ParameterBindingContext(accessor, spelExpressionParser, evaluationContext));

	}

	public ParameterBindingJsonReader(String json, ParameterBindingContext bindingContext) {

		this.scanner = new JsonScanner(json);
		setContext(new Context(null, BsonContextType.TOP_LEVEL));

		this.bindingContext = bindingContext;

		Matcher matcher = ENTIRE_QUERY_BINDING_PATTERN.matcher(json);
		if (matcher.find()) {
			BindableValue bindingResult = bindableValueFor(new JsonToken(JsonTokenType.UNQUOTED_STRING, json));
			currentValue = bindingResult.getValue();
		}
	}

	// Spring Data Customization END

	@Override
	protected BsonBinary doReadBinaryData() {
		return (BsonBinary) currentValue;
	}

	@Override
	protected byte doPeekBinarySubType() {
		return doReadBinaryData().getType();
	}

	@Override
	protected int doPeekBinarySize() {
		return doReadBinaryData().getData().length;
	}

	@Override
	protected boolean doReadBoolean() {
		return (Boolean) currentValue;
	}

	// CHECKSTYLE:OFF
	@Override
	public BsonType readBsonType() {

		if (isClosed()) {
			throw new IllegalStateException("This instance has been closed");
		}
		if (getState() == State.INITIAL || getState() == State.DONE || getState() == State.SCOPE_DOCUMENT) {
			// in JSON the top level value can be of any type so fall through
			setState(State.TYPE);
		}
		if (getState() != State.TYPE) {
			throwInvalidState("readBSONType", State.TYPE);
		}

		if (getContext().getContextType() == BsonContextType.DOCUMENT) {
			JsonToken nameToken = popToken();
			switch (nameToken.getType()) {
				case STRING:
				case UNQUOTED_STRING:

					// Spring Data Customization START

					setCurrentName(bindableValueFor(nameToken).getValue().toString());

					// Spring Data Customization END
					break;
				case END_OBJECT:
					setState(State.END_OF_DOCUMENT);
					return BsonType.END_OF_DOCUMENT;
				default:
					throw new JsonParseException("JSON reader was expecting a name but found '%s'.", nameToken.getValue());
			}

			JsonToken colonToken = popToken();
			if (colonToken.getType() != JsonTokenType.COLON) {
				throw new JsonParseException("JSON reader was expecting ':' but found '%s'.", colonToken.getValue());
			}
		}

		JsonToken token = popToken();
		if (getContext().getContextType() == BsonContextType.ARRAY && token.getType() == JsonTokenType.END_ARRAY) {
			setState(State.END_OF_ARRAY);
			return BsonType.END_OF_DOCUMENT;
		}

		// Spring Data Customization START

		boolean noValueFound = false;
		BindableValue bindableValue = null;

		switch (token.getType()) {
			case BEGIN_ARRAY:
				setCurrentBsonType(BsonType.ARRAY);
				break;
			case BEGIN_OBJECT:
				visitExtendedJSON();
				break;
			case DOUBLE:
				setCurrentBsonType(BsonType.DOUBLE);
				currentValue = token.getValue();
				break;
			case END_OF_FILE:
				setCurrentBsonType(BsonType.END_OF_DOCUMENT);
				break;
			case INT32:
				setCurrentBsonType(BsonType.INT32);
				currentValue = token.getValue();
				break;
			case INT64:
				setCurrentBsonType(BsonType.INT64);
				currentValue = token.getValue();
				break;
			case REGULAR_EXPRESSION:

				setCurrentBsonType(BsonType.REGULAR_EXPRESSION);
				currentValue = bindableValueFor(token).getValue();
				break;
			case STRING:

				setCurrentBsonType(BsonType.STRING);
				currentValue = bindableValueFor(token).getValue().toString();
				break;
			case UNQUOTED_STRING:

				String value = token.getValue(String.class);

				if ("false".equals(value) || "true".equals(value)) {
					setCurrentBsonType(BsonType.BOOLEAN);
					currentValue = Boolean.parseBoolean(value);
				} else if ("Infinity".equals(value)) {
					setCurrentBsonType(BsonType.DOUBLE);
					currentValue = Double.POSITIVE_INFINITY;
				} else if ("NaN".equals(value)) {
					setCurrentBsonType(BsonType.DOUBLE);
					currentValue = Double.NaN;
				} else if ("null".equals(value)) {
					setCurrentBsonType(BsonType.NULL);
				} else if ("undefined".equals(value)) {
					setCurrentBsonType(BsonType.UNDEFINED);
				} else if ("MinKey".equals(value)) {
					visitEmptyConstructor();
					setCurrentBsonType(BsonType.MIN_KEY);
					currentValue = new MinKey();
				} else if ("MaxKey".equals(value)) {
					visitEmptyConstructor();
					setCurrentBsonType(BsonType.MAX_KEY);
					currentValue = new MaxKey();
				} else if ("BinData".equals(value)) {
					setCurrentBsonType(BsonType.BINARY);
					currentValue = visitBinDataConstructor();
				} else if ("Date".equals(value)) {
					currentValue = visitDateTimeConstructorWithOutNew();
					setCurrentBsonType(BsonType.STRING);
				} else if ("HexData".equals(value)) {
					setCurrentBsonType(BsonType.BINARY);
					currentValue = visitHexDataConstructor();
				} else if ("ISODate".equals(value)) {
					setCurrentBsonType(BsonType.DATE_TIME);
					currentValue = visitISODateTimeConstructor();
				} else if ("NumberInt".equals(value)) {
					setCurrentBsonType(BsonType.INT32);
					currentValue = visitNumberIntConstructor();
				} else if ("NumberLong".equals(value)) {
					setCurrentBsonType(BsonType.INT64);
					currentValue = visitNumberLongConstructor();
				} else if ("NumberDecimal".equals(value)) {
					setCurrentBsonType(BsonType.DECIMAL128);
					currentValue = visitNumberDecimalConstructor();
				} else if ("ObjectId".equals(value)) {
					setCurrentBsonType(BsonType.OBJECT_ID);
					currentValue = visitObjectIdConstructor();
				} else if ("Timestamp".equals(value)) {
					setCurrentBsonType(BsonType.TIMESTAMP);
					currentValue = visitTimestampConstructor();
				} else if ("RegExp".equals(value)) {
					setCurrentBsonType(BsonType.REGULAR_EXPRESSION);
					currentValue = visitRegularExpressionConstructor();
				} else if ("DBPointer".equals(value)) {
					setCurrentBsonType(BsonType.DB_POINTER);
					currentValue = visitDBPointerConstructor();
				} else if ("UUID".equals(value)) {
					setCurrentBsonType(BsonType.BINARY);
					currentValue = visitUUIDConstructor();
				} else if ("new".equals(value)) {
					visitNew();
				} else {

					bindableValue = bindableValueFor(token);
					if (bindableValue != null) {

						if (bindableValue.getIndex() != -1) {
							setCurrentBsonType(bindableValue.getType());
						} else {
							setCurrentBsonType(BsonType.STRING);
						}

						currentValue = bindableValue.getValue();
					} else {
						noValueFound = true;
					}
				}
				break;
			default:
				noValueFound = true;
				break;
		}

		// Spring Data Customization END

		if (noValueFound) {
			throw new JsonParseException("JSON reader was expecting a value but found '%s'.", token.getValue());
		}

		if (getContext().getContextType() == BsonContextType.ARRAY
				|| getContext().getContextType() == BsonContextType.DOCUMENT) {
			JsonToken commaToken = popToken();
			if (commaToken.getType() != JsonTokenType.COMMA) {
				pushToken(commaToken);
			}
		}

		switch (getContext().getContextType()) {
			case DOCUMENT:
			case SCOPE_DOCUMENT:
			default:
				setState(State.NAME);
				break;
			case ARRAY:
			case JAVASCRIPT_WITH_SCOPE:
			case TOP_LEVEL:
				setState(State.VALUE);
				break;
		}
		return getCurrentBsonType();
	}

	// Spring Data Customization START

	@Override
	public void setState(State newState) {
		super.setState(newState);
	}

	private BindableValue bindableValueFor(JsonToken token) {

		if (!JsonTokenType.STRING.equals(token.getType()) && !JsonTokenType.UNQUOTED_STRING.equals(token.getType())
				&& !JsonTokenType.REGULAR_EXPRESSION.equals(token.getType())) {
			return null;
		}

		boolean isRegularExpression = token.getType().equals(JsonTokenType.REGULAR_EXPRESSION);

		BindableValue bindableValue = new BindableValue();
		String tokenValue = isRegularExpression ? token.getValue(BsonRegularExpression.class).getPattern()
				: String.class.cast(token.getValue());
		Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(tokenValue);

		if (token.getType().equals(JsonTokenType.UNQUOTED_STRING)) {

			Matcher regexMatcher = EXPRESSION_BINDING_PATTERN.matcher(tokenValue);
			if (regexMatcher.find()) {

				String binding = regexMatcher.group();
				String expression = binding.substring(3, binding.length() - 1);
				String expressionString = binding.substring(1);

				Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
				Map<String, Object> innerSpelVariables = new HashMap<>();

				while (inSpelMatcher.find()) {

					String group = inSpelMatcher.group();
					int index = computeParameterIndex(group);
					Object value = getBindableValueForIndex(index);
					String varName = "__QVar" + innerSpelVariables.size();
					expression = expression.replace(group, "#" + varName);
					expressionString = expressionString.replace(group, "#" + varName);
					if(group.startsWith("'")) { // retain the string semantic
						innerSpelVariables.put(varName, nullSafeToString(value));
					} else {
						innerSpelVariables.put(varName, value);
					}
				}

				Object value = evaluateExpression(expressionString, innerSpelVariables);
				bindableValue.setValue(value);
				bindableValue.setType(bsonTypeForValue(value));
				return bindableValue;
			}

			if (matcher.find()) {

				int index = computeParameterIndex(matcher.group());
				Object bindableValueForIndex = getBindableValueForIndex(index);
				bindableValue.setValue(bindableValueForIndex);
				bindableValue.setType(bsonTypeForValue(bindableValueForIndex));
				return bindableValue;
			}

			bindableValue.setValue(tokenValue);
			bindableValue.setType(BsonType.STRING);
			return bindableValue;

		}

		String computedValue = tokenValue;

		Matcher regexMatcher = EXPRESSION_BINDING_PATTERN.matcher(computedValue);

		if (regexMatcher.find()) {

			String binding = regexMatcher.group();
			String expression = binding.substring(3, binding.length() - 1);
			String expressionString = binding.substring(1);

			Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression);
			Map<String, Object> innerSpelVariables = new HashMap<>();

			while (inSpelMatcher.find()) {

				String group = inSpelMatcher.group();
				int index = computeParameterIndex(group);
				Object value = getBindableValueForIndex(index);
				String varName = "__QVar" + innerSpelVariables.size();
				expression = expression.replace(group, "#" + varName);
				expressionString = expressionString.replace(group, "#" + varName);
				if(group.startsWith("'")) { // retain the string semantic
					innerSpelVariables.put(varName, nullSafeToString(value));
				} else {
					innerSpelVariables.put(varName, value);
				}
			}

			computedValue = computedValue.replace(binding,
					nullSafeToString(evaluateExpression(expressionString, innerSpelVariables)));

			bindableValue.setValue(computedValue);
			bindableValue.setType(BsonType.STRING);

			return bindableValue;
		}

		while (matcher.find()) {

			String group = matcher.group();
			int index = computeParameterIndex(group);

			String bindValue = nullSafeToString(getBindableValueForIndex(index));
			if(isQuoted(tokenValue)) {
				bindValue = bindValue.replaceAll(ESCAPED_QUOTE_START, QUOTE_REPLACEMENT_QUOTE_START) //
					.replaceAll(ESCAPED_QUOTE_END, QUOTE_REPLACEMENT_QUOTE_END);
			}
			computedValue = computedValue.replace(group, bindValue);
		}

		if (isRegularExpression) {

			BsonRegularExpression originalExpression = token.getValue(BsonRegularExpression.class);

			bindableValue.setValue(new BsonRegularExpression(computedValue, originalExpression.getOptions()));
			bindableValue.setType(BsonType.REGULAR_EXPRESSION);
		} else {

			bindableValue.setValue(computedValue);
			bindableValue.setType(BsonType.STRING);
		}
		return bindableValue;
	}

	private static String nullSafeToString(@Nullable Object value) {

		if (value instanceof Date date) {
			return DateTimeFormatter.format(date.getTime());
		}

		return ObjectUtils.nullSafeToString(value);
	}

	private static boolean isQuoted(String value) {
		return value.contains(QUOTE_START) || value.contains(QUOTE_END);
	}

	private static int computeParameterIndex(String parameter) {
		return NumberUtils.parseNumber(parameter.replace("?", "").replace("'", ""), Integer.class);
	}

	private Object getBindableValueForIndex(int index) {
		return bindingContext.bindableValueForIndex(index);
	}

	private BsonType bsonTypeForValue(Object value) {

		if (value == null) {
			return BsonType.NULL;
		}

		Class<?> type = value.getClass();

		if (ClassUtils.isAssignable(String.class, type)) {

			if (((String) value).startsWith("{")) {
				return BsonType.DOCUMENT;
			}
			return BsonType.STRING;
		}
		if (ClassUtils.isAssignable(Boolean.class, type)) {
			return BsonType.BOOLEAN;
		}
		if (ClassUtils.isAssignable(Document.class, type)) {
			return BsonType.DOCUMENT;
		}
		if (ClassUtils.isAssignable(Double.class, type)) {
			return BsonType.DOUBLE;
		}
		if (ClassUtils.isAssignable(Long.class, type)) {
			return BsonType.INT64;
		}
		if (ClassUtils.isAssignable(Integer.class, type)) {
			return BsonType.INT32;
		}
		if (ClassUtils.isAssignable(Pattern.class, type)) {
			return BsonType.REGULAR_EXPRESSION;
		}
		if (ClassUtils.isAssignable(Iterable.class, type)) {
			return BsonType.ARRAY;
		}
		if (ClassUtils.isAssignable(Map.class, type)) {
			return BsonType.DOCUMENT;
		}

		return BsonType.UNDEFINED;
	}

	private @Nullable Object evaluateExpression(String expressionString) {
		return bindingContext.evaluateExpression(expressionString, Collections.emptyMap());
	}

	private @Nullable Object evaluateExpression(String expressionString, Map<String,Object> variables) {
		return bindingContext.evaluateExpression(expressionString, variables);
	}

	// Spring Data Customization END
	// CHECKSTYLE:ON

	@Override
	public Decimal128 doReadDecimal128() {
		return (Decimal128) currentValue;
	}

	@Override
	protected long doReadDateTime() {
		return (Long) currentValue;
	}

	@Override
	protected double doReadDouble() {
		return (Double) currentValue;
	}

	@Override
	protected void doReadEndArray() {
		setContext(getContext().getParentContext());

		if (getContext().getContextType() == BsonContextType.ARRAY
				|| getContext().getContextType() == BsonContextType.DOCUMENT) {
			JsonToken commaToken = popToken();
			if (commaToken.getType() != JsonTokenType.COMMA) {
				pushToken(commaToken);
			}
		}
	}

	@Override
	protected void doReadEndDocument() {
		setContext(getContext().getParentContext());
		if (getContext() != null && getContext().getContextType() == BsonContextType.SCOPE_DOCUMENT) {
			setContext(getContext().getParentContext()); // JavaScriptWithScope
			verifyToken(JsonTokenType.END_OBJECT); // outermost closing bracket for JavaScriptWithScope
		}

		if (getContext() == null) {
			throw new JsonParseException("Unexpected end of document.");
		}

		if (getContext().getContextType() == BsonContextType.ARRAY
				|| getContext().getContextType() == BsonContextType.DOCUMENT) {
			JsonToken commaToken = popToken();
			if (commaToken.getType() != JsonTokenType.COMMA) {
				pushToken(commaToken);
			}
		}
	}

	@Override
	protected int doReadInt32() {
		return (Integer) currentValue;
	}

	@Override
	protected long doReadInt64() {
		return (Long) currentValue;
	}

	@Override
	protected String doReadJavaScript() {
		return (String) currentValue;
	}

	@Override
	protected String doReadJavaScriptWithScope() {
		return (String) currentValue;
	}

	@Override
	protected void doReadMaxKey() {}

	@Override
	protected void doReadMinKey() {}

	@Override
	protected void doReadNull() {}

	@Override
	protected ObjectId doReadObjectId() {
		return (ObjectId) currentValue;
	}

	@Override
	protected BsonRegularExpression doReadRegularExpression() {
		return (BsonRegularExpression) currentValue;
	}

	@Override
	protected BsonDbPointer doReadDBPointer() {
		return (BsonDbPointer) currentValue;
	}

	@Override
	protected void doReadStartArray() {
		setContext(new Context(getContext(), BsonContextType.ARRAY));
	}

	@Override
	protected void doReadStartDocument() {
		setContext(new Context(getContext(), BsonContextType.DOCUMENT));
	}

	@Override
	protected String doReadString() {
		return (String) currentValue;
	}

	@Override
	protected String doReadSymbol() {
		return (String) currentValue;
	}

	@Override
	protected BsonTimestamp doReadTimestamp() {
		return (BsonTimestamp) currentValue;
	}

	@Override
	protected void doReadUndefined() {}

	@Override
	protected void doSkipName() {}

	@Override
	protected void doSkipValue() {
		switch (getCurrentBsonType()) {
			case ARRAY:
				readStartArray();
				while (readBsonType() != BsonType.END_OF_DOCUMENT) {
					skipValue();
				}
				readEndArray();
				break;
			case BINARY:
				readBinaryData();
				break;
			case BOOLEAN:
				readBoolean();
				break;
			case DATE_TIME:
				readDateTime();
				break;
			case DOCUMENT:
				readStartDocument();
				while (readBsonType() != BsonType.END_OF_DOCUMENT) {
					skipName();
					skipValue();
				}
				readEndDocument();
				break;
			case DOUBLE:
				readDouble();
				break;
			case INT32:
				readInt32();
				break;
			case INT64:
				readInt64();
				break;
			case DECIMAL128:
				readDecimal128();
				break;
			case JAVASCRIPT:
				readJavaScript();
				break;
			case JAVASCRIPT_WITH_SCOPE:
				readJavaScriptWithScope();
				readStartDocument();
				while (readBsonType() != BsonType.END_OF_DOCUMENT) {
					skipName();
					skipValue();
				}
				readEndDocument();
				break;
			case MAX_KEY:
				readMaxKey();
				break;
			case MIN_KEY:
				readMinKey();
				break;
			case NULL:
				readNull();
				break;
			case OBJECT_ID:
				readObjectId();
				break;
			case REGULAR_EXPRESSION:
				readRegularExpression();
				break;
			case STRING:
				readString();
				break;
			case SYMBOL:
				readSymbol();
				break;
			case TIMESTAMP:
				readTimestamp();
				break;
			case UNDEFINED:
				readUndefined();
				break;
			default:
		}
	}

	private JsonToken popToken() {
		if (pushedToken != null) {
			JsonToken token = pushedToken;
			pushedToken = null;
			return token;
		} else {
			return scanner.nextToken();
		}
	}

	private void pushToken(final JsonToken token) {
		if (pushedToken == null) {
			pushedToken = token;
		} else {
			throw new BsonInvalidOperationException("There is already a pending token.");
		}
	}

	private void verifyToken(final JsonTokenType expectedType) {
		JsonToken token = popToken();
		if (expectedType != token.getType()) {
			throw new JsonParseException("JSON reader expected token type '%s' but found '%s'.", expectedType,
					token.getValue());
		}
	}

	private void verifyToken(final JsonTokenType expectedType, final Object expectedValue) {
		JsonToken token = popToken();
		if (expectedType != token.getType()) {
			throw new JsonParseException("JSON reader expected token type '%s' but found '%s'.", expectedType,
					token.getValue());
		}
		if (!expectedValue.equals(token.getValue())) {
			throw new JsonParseException("JSON reader expected '%s' but found '%s'.", expectedValue, token.getValue());
		}
	}

	private void verifyString(final String expected) {
		if (expected == null) {
			throw new IllegalArgumentException("Can't be null");
		}

		JsonToken token = popToken();
		JsonTokenType type = token.getType();

		if ((type != JsonTokenType.STRING && type != JsonTokenType.UNQUOTED_STRING) || !expected.equals(token.getValue())) {
			throw new JsonParseException("JSON reader expected '%s' but found '%s'.", expected, token.getValue());
		}
	}

	private void visitNew() {
		JsonToken typeToken = popToken();
		if (typeToken.getType() != JsonTokenType.UNQUOTED_STRING) {
			throw new JsonParseException("JSON reader expected a type name but found '%s'.", typeToken.getValue());
		}

		String value = typeToken.getValue(String.class);

		if ("MinKey".equals(value)) {
			visitEmptyConstructor();
			setCurrentBsonType(BsonType.MIN_KEY);
			currentValue = new MinKey();
		} else if ("MaxKey".equals(value)) {
			visitEmptyConstructor();
			setCurrentBsonType(BsonType.MAX_KEY);
			currentValue = new MaxKey();
		} else if ("BinData".equals(value)) {
			currentValue = visitBinDataConstructor();
			setCurrentBsonType(BsonType.BINARY);
		} else if ("Date".equals(value)) {
			currentValue = visitDateTimeConstructor();
			setCurrentBsonType(BsonType.DATE_TIME);
		} else if ("HexData".equals(value)) {
			currentValue = visitHexDataConstructor();
			setCurrentBsonType(BsonType.BINARY);
		} else if ("ISODate".equals(value)) {
			currentValue = visitISODateTimeConstructor();
			setCurrentBsonType(BsonType.DATE_TIME);
		} else if ("NumberInt".equals(value)) {
			currentValue = visitNumberIntConstructor();
			setCurrentBsonType(BsonType.INT32);
		} else if ("NumberLong".equals(value)) {
			currentValue = visitNumberLongConstructor();
			setCurrentBsonType(BsonType.INT64);
		} else if ("NumberDecimal".equals(value)) {
			currentValue = visitNumberDecimalConstructor();
			setCurrentBsonType(BsonType.DECIMAL128);
		} else if ("ObjectId".equals(value)) {
			currentValue = visitObjectIdConstructor();
			setCurrentBsonType(BsonType.OBJECT_ID);
		} else if ("RegExp".equals(value)) {
			currentValue = visitRegularExpressionConstructor();
			setCurrentBsonType(BsonType.REGULAR_EXPRESSION);
		} else if ("DBPointer".equals(value)) {
			currentValue = visitDBPointerConstructor();
			setCurrentBsonType(BsonType.DB_POINTER);
		} else if ("UUID".equals(value)) {
			currentValue = visitUUIDConstructor();
			setCurrentBsonType(BsonType.BINARY);
		} else {
			throw new JsonParseException("JSON reader expected a type name but found '%s'.", value);
		}
	}

	private void visitExtendedJSON() {
		JsonToken nameToken = popToken();
		String value = nameToken.getValue(String.class);
		JsonTokenType type = nameToken.getType();

		if (type == JsonTokenType.STRING || type == JsonTokenType.UNQUOTED_STRING) {

			if ("$binary".equals(value) || "$type".equals(value)) {
				currentValue = visitBinDataExtendedJson(value);
				if (currentValue != null) {
					setCurrentBsonType(BsonType.BINARY);
					return;
				}
			}
			if ("$uuid".equals(value)) {
				currentValue = visitUuidExtendedJson();
				setCurrentBsonType(BsonType.BINARY);
				return;
			}
			else if ("$regex".equals(value) || "$options".equals(value)) {
				currentValue = visitRegularExpressionExtendedJson(value);
				if (currentValue != null) {
					setCurrentBsonType(BsonType.REGULAR_EXPRESSION);
					return;
				}
			} else if ("$code".equals(value)) {
				visitJavaScriptExtendedJson();
				return;
			} else if ("$date".equals(value)) {
				currentValue = visitDateTimeExtendedJson();
				setCurrentBsonType(BsonType.DATE_TIME);
				return;
			} else if ("$maxKey".equals(value)) {
				currentValue = visitMaxKeyExtendedJson();
				setCurrentBsonType(BsonType.MAX_KEY);
				return;
			} else if ("$minKey".equals(value)) {
				currentValue = visitMinKeyExtendedJson();
				setCurrentBsonType(BsonType.MIN_KEY);
				return;
			} else if ("$oid".equals(value)) {
				currentValue = visitObjectIdExtendedJson();
				setCurrentBsonType(BsonType.OBJECT_ID);
				return;
			} else if ("$regularExpression".equals(value)) {
				currentValue = visitNewRegularExpressionExtendedJson();
				setCurrentBsonType(BsonType.REGULAR_EXPRESSION);
				return;
			} else if ("$symbol".equals(value)) {
				currentValue = visitSymbolExtendedJson();
				setCurrentBsonType(BsonType.SYMBOL);
				return;
			} else if ("$timestamp".equals(value)) {
				currentValue = visitTimestampExtendedJson();
				setCurrentBsonType(BsonType.TIMESTAMP);
				return;
			} else if ("$undefined".equals(value)) {
				currentValue = visitUndefinedExtendedJson();
				setCurrentBsonType(BsonType.UNDEFINED);
				return;
			} else if ("$numberLong".equals(value)) {
				currentValue = visitNumberLongExtendedJson();
				setCurrentBsonType(BsonType.INT64);
				return;
			} else if ("$numberInt".equals(value)) {
				currentValue = visitNumberIntExtendedJson();
				setCurrentBsonType(BsonType.INT32);
				return;
			} else if ("$numberDouble".equals(value)) {
				currentValue = visitNumberDoubleExtendedJson();
				setCurrentBsonType(BsonType.DOUBLE);
				return;
			} else if ("$numberDecimal".equals(value)) {
				currentValue = visitNumberDecimalExtendedJson();
				setCurrentBsonType(BsonType.DECIMAL128);
				return;
			} else if ("$dbPointer".equals(value)) {
				currentValue = visitDbPointerExtendedJson();
				setCurrentBsonType(BsonType.DB_POINTER);
				return;
			}
		}

		pushToken(nameToken);
		setCurrentBsonType(BsonType.DOCUMENT);
	}

	private void visitEmptyConstructor() {
		JsonToken nextToken = popToken();
		if (nextToken.getType() == JsonTokenType.LEFT_PAREN) {
			verifyToken(JsonTokenType.RIGHT_PAREN);
		} else {
			pushToken(nextToken);
		}
	}

	private BsonBinary visitBinDataConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken subTypeToken = popToken();
		if (subTypeToken.getType() != JsonTokenType.INT32) {
			throw new JsonParseException("JSON reader expected a binary subtype but found '%s'.", subTypeToken.getValue());
		}
		verifyToken(JsonTokenType.COMMA);
		JsonToken bytesToken = popToken();
		if (bytesToken.getType() != JsonTokenType.UNQUOTED_STRING && bytesToken.getType() != JsonTokenType.STRING) {
			throw new JsonParseException("JSON reader expected a string but found '%s'.", bytesToken.getValue());
		}
		verifyToken(JsonTokenType.RIGHT_PAREN);

		byte[] bytes = Base64.getDecoder().decode(bytesToken.getValue(String.class));
		return new BsonBinary(subTypeToken.getValue(Integer.class).byteValue(), bytes);
	}

	private BsonBinary visitUUIDConstructor() {
		this.verifyToken(JsonTokenType.LEFT_PAREN);
		String hexString = this.readStringFromExtendedJson().replace("-", "");

		this.verifyToken(JsonTokenType.RIGHT_PAREN);
		return new BsonBinary(BsonBinarySubType.UUID_STANDARD, decodeHex(hexString));
	}

	private BsonRegularExpression visitRegularExpressionConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		String pattern = readStringFromExtendedJson();
		String options = "";
		JsonToken commaToken = popToken();
		if (commaToken.getType() == JsonTokenType.COMMA) {
			options = readStringFromExtendedJson();
		} else {
			pushToken(commaToken);
		}
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return new BsonRegularExpression(pattern, options);
	}

	private ObjectId visitObjectIdConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		ObjectId objectId = new ObjectId(readStringFromExtendedJson());
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return objectId;
	}

	private BsonTimestamp visitTimestampConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken timeToken = popToken();
		int time;
		if (timeToken.getType() != JsonTokenType.INT32) {
			throw new JsonParseException("JSON reader expected an integer but found '%s'.", timeToken.getValue());
		} else {
			time = timeToken.getValue(Integer.class);
		}
		verifyToken(JsonTokenType.COMMA);
		JsonToken incrementToken = popToken();
		int increment;
		if (incrementToken.getType() != JsonTokenType.INT32) {
			throw new JsonParseException("JSON reader expected an integer but found '%s'.", timeToken.getValue());
		} else {
			increment = incrementToken.getValue(Integer.class);
		}

		verifyToken(JsonTokenType.RIGHT_PAREN);
		return new BsonTimestamp(time, increment);
	}

	private BsonDbPointer visitDBPointerConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		String namespace = readStringFromExtendedJson();
		verifyToken(JsonTokenType.COMMA);
		ObjectId id = new ObjectId(readStringFromExtendedJson());
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return new BsonDbPointer(namespace, id);
	}

	private int visitNumberIntConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken valueToken = popToken();
		int value;
		if (valueToken.getType() == JsonTokenType.INT32) {
			value = valueToken.getValue(Integer.class);
		} else if (valueToken.getType() == JsonTokenType.STRING) {
			value = Integer.parseInt(valueToken.getValue(String.class));
		} else {
			throw new JsonParseException("JSON reader expected an integer or a string but found '%s'.",
					valueToken.getValue());
		}
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return value;
	}

	private long visitNumberLongConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken valueToken = popToken();
		long value;
		if (valueToken.getType() == JsonTokenType.INT32 || valueToken.getType() == JsonTokenType.INT64) {
			value = valueToken.getValue(Long.class);
		} else if (valueToken.getType() == JsonTokenType.STRING) {
			value = Long.parseLong(valueToken.getValue(String.class));
		} else {
			throw new JsonParseException("JSON reader expected an integer or a string but found '%s'.",
					valueToken.getValue());
		}
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return value;
	}

	private Decimal128 visitNumberDecimalConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken valueToken = popToken();
		Decimal128 value;
		if (valueToken.getType() == JsonTokenType.INT32 || valueToken.getType() == JsonTokenType.INT64
				|| valueToken.getType() == JsonTokenType.DOUBLE) {
			value = valueToken.getValue(Decimal128.class);
		} else if (valueToken.getType() == JsonTokenType.STRING) {
			value = Decimal128.parse(valueToken.getValue(String.class));
		} else {
			throw new JsonParseException("JSON reader expected a number or a string but found '%s'.", valueToken.getValue());
		}
		verifyToken(JsonTokenType.RIGHT_PAREN);
		return value;
	}

	private long visitISODateTimeConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);

		JsonToken token = popToken();
		if (token.getType() == JsonTokenType.RIGHT_PAREN) {
			return new Date().getTime();
		} else if (token.getType() != JsonTokenType.STRING) {
			throw new JsonParseException("JSON reader expected a string but found '%s'.", token.getValue());
		}

		verifyToken(JsonTokenType.RIGHT_PAREN);

		String dateTimeString = token.getValue(String.class);

		try {
			return DateTimeFormatter.parse(dateTimeString);
		} catch (DateTimeParseException e) {
			throw new JsonParseException("Failed to parse string as a date: " + dateTimeString, e);
		}
	}

	private BsonBinary visitHexDataConstructor() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken subTypeToken = popToken();
		if (subTypeToken.getType() != JsonTokenType.INT32) {
			throw new JsonParseException("JSON reader expected a binary subtype but found '%s'.", subTypeToken.getValue());
		}
		verifyToken(JsonTokenType.COMMA);
		String hex = readStringFromExtendedJson();
		verifyToken(JsonTokenType.RIGHT_PAREN);

		if ((hex.length() & 1) != 0) {
			hex = "0" + hex;
		}

		for (final BsonBinarySubType subType : BsonBinarySubType.values()) {
			if (subType.getValue() == subTypeToken.getValue(Integer.class)) {
				return new BsonBinary(subType, decodeHex(hex));
			}
		}
		return new BsonBinary(decodeHex(hex));
	}

	private long visitDateTimeConstructor() {
		DateFormat format = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss z", Locale.ENGLISH);

		verifyToken(JsonTokenType.LEFT_PAREN);

		JsonToken token = popToken();
		if (token.getType() == JsonTokenType.RIGHT_PAREN) {
			return new Date().getTime();
		} else if (token.getType() == JsonTokenType.STRING) {
			verifyToken(JsonTokenType.RIGHT_PAREN);
			String s = token.getValue(String.class);
			ParsePosition pos = new ParsePosition(0);
			Date dateTime = format.parse(s, pos);
			if (dateTime != null && pos.getIndex() == s.length()) {
				return dateTime.getTime();
			} else {
				throw new JsonParseException(
						"JSON reader expected a date in 'EEE MMM dd yyyy HH:mm:ss z' format but found '%s'.", s);
			}

		} else if (token.getType() == JsonTokenType.INT32 || token.getType() == JsonTokenType.INT64) {
			long[] values = new long[7];
			int pos = 0;
			while (true) {
				if (pos < values.length) {
					values[pos++] = token.getValue(Long.class);
				}
				token = popToken();
				if (token.getType() == JsonTokenType.RIGHT_PAREN) {
					break;
				}
				if (token.getType() != JsonTokenType.COMMA) {
					throw new JsonParseException("JSON reader expected a ',' or a ')' but found '%s'.", token.getValue());
				}
				token = popToken();
				if (token.getType() != JsonTokenType.INT32 && token.getType() != JsonTokenType.INT64) {
					throw new JsonParseException("JSON reader expected an integer but found '%s'.", token.getValue());
				}
			}
			if (pos == 1) {
				return values[0];
			} else if (pos < 3 || pos > 7) {
				throw new JsonParseException("JSON reader expected 1 or 3-7 integers but found %d.", pos);
			}

			Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
			calendar.set(Calendar.YEAR, (int) values[0]);
			calendar.set(Calendar.MONTH, (int) values[1]);
			calendar.set(Calendar.DAY_OF_MONTH, (int) values[2]);
			calendar.set(Calendar.HOUR_OF_DAY, (int) values[3]);
			calendar.set(Calendar.MINUTE, (int) values[4]);
			calendar.set(Calendar.SECOND, (int) values[5]);
			calendar.set(Calendar.MILLISECOND, (int) values[6]);
			return calendar.getTimeInMillis();
		} else {
			throw new JsonParseException("JSON reader expected an integer or a string but found '%s'.", token.getValue());
		}
	}

	private String visitDateTimeConstructorWithOutNew() {
		verifyToken(JsonTokenType.LEFT_PAREN);
		JsonToken token = popToken();
		if (token.getType() != JsonTokenType.RIGHT_PAREN) {
			while (token.getType() != JsonTokenType.END_OF_FILE) {
				token = popToken();
				if (token.getType() == JsonTokenType.RIGHT_PAREN) {
					break;
				}
			}
			if (token.getType() != JsonTokenType.RIGHT_PAREN) {
				throw new JsonParseException("JSON reader expected a ')' but found '%s'.", token.getValue());
			}
		}

		DateFormat df = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss z", Locale.ENGLISH);
		return df.format(new Date());
	}

	private BsonBinary visitBinDataExtendedJson(final String firstKey) {

		Mark mark = new Mark();

		verifyToken(JsonTokenType.COLON);

		if (firstKey.equals("$binary")) {
			JsonToken nextToken = popToken();
			if (nextToken.getType() == JsonTokenType.BEGIN_OBJECT) {
				JsonToken nameToken = popToken();
				String firstNestedKey = nameToken.getValue(String.class);
				byte[] data;
				byte type;
				if (firstNestedKey.equals("base64")) {
					verifyToken(JsonTokenType.COLON);
					data = Base64.getDecoder().decode(readStringFromExtendedJson());
					verifyToken(JsonTokenType.COMMA);
					verifyString("subType");
					verifyToken(JsonTokenType.COLON);
					type = readBinarySubtypeFromExtendedJson();
				} else if (firstNestedKey.equals("subType")) {
					verifyToken(JsonTokenType.COLON);
					type = readBinarySubtypeFromExtendedJson();
					verifyToken(JsonTokenType.COMMA);
					verifyString("base64");
					verifyToken(JsonTokenType.COLON);
					data = Base64.getDecoder().decode(readStringFromExtendedJson());
				} else {
					throw new JsonParseException("Unexpected key for $binary: " + firstNestedKey);
				}
				verifyToken(JsonTokenType.END_OBJECT);
				verifyToken(JsonTokenType.END_OBJECT);
				return new BsonBinary(type, data);
			} else {
				mark.reset();
				return visitLegacyBinaryExtendedJson(firstKey);
			}
		} else {
			mark.reset();
			return visitLegacyBinaryExtendedJson(firstKey);
		}
	}

	private BsonBinary visitLegacyBinaryExtendedJson(final String firstKey) {

		Mark mark = new Mark();

		try {
			verifyToken(JsonTokenType.COLON);

			byte[] data;
			byte type;

			if (firstKey.equals("$binary")) {
				data = Base64.getDecoder().decode(readStringFromExtendedJson());
				verifyToken(JsonTokenType.COMMA);
				verifyString("$type");
				verifyToken(JsonTokenType.COLON);
				type = readBinarySubtypeFromExtendedJson();
			} else {
				type = readBinarySubtypeFromExtendedJson();
				verifyToken(JsonTokenType.COMMA);
				verifyString("$binary");
				verifyToken(JsonTokenType.COLON);
				data = Base64.getDecoder().decode(readStringFromExtendedJson());
			}
			verifyToken(JsonTokenType.END_OBJECT);

			return new BsonBinary(type, data);
		} catch (JsonParseException e) {
			mark.reset();
			return null;
		} catch (NumberFormatException e) {
			mark.reset();
			return null;
		}
	}

	private byte readBinarySubtypeFromExtendedJson() {
		JsonToken subTypeToken = popToken();
		if (subTypeToken.getType() != JsonTokenType.STRING && subTypeToken.getType() != JsonTokenType.INT32) {
			throw new JsonParseException("JSON reader expected a string or number but found '%s'.", subTypeToken.getValue());
		}

		if (subTypeToken.getType() == JsonTokenType.STRING) {
			return (byte) Integer.parseInt(subTypeToken.getValue(String.class), 16);
		} else {
			return subTypeToken.getValue(Integer.class).byteValue();
		}
	}

	private long visitDateTimeExtendedJson() {
		long value;
		verifyToken(JsonTokenType.COLON);
		JsonToken valueToken = popToken();
		if (valueToken.getType() == JsonTokenType.BEGIN_OBJECT) {
			JsonToken nameToken = popToken();
			String name = nameToken.getValue(String.class);
			if (!name.equals("$numberLong")) {
				throw new JsonParseException(
						String.format("JSON reader expected $numberLong within $date, but found %s", name));
			}
			value = visitNumberLongExtendedJson();
			verifyToken(JsonTokenType.END_OBJECT);
		} else {
			if (valueToken.getType() == JsonTokenType.INT32 || valueToken.getType() == JsonTokenType.INT64) {
				value = valueToken.getValue(Long.class);
			} else if (valueToken.getType() == JsonTokenType.STRING
					|| valueToken.getType() == JsonTokenType.UNQUOTED_STRING) {

				// Spring Data Customization START

				Object dt = bindableValueFor(valueToken).getValue();
				if (dt instanceof Date date) {
					value = date.getTime();
				} else if (dt instanceof Number numberValue) {
					value = NumberUtils.convertNumberToTargetClass(numberValue, Long.class);
				} else {
					try {
						value = DateTimeFormatter.parse(dt.toString());
					} catch (IllegalArgumentException e) {
						throw new JsonParseException(String.format("Failed to parse string '%s' as a date", dt), e);
					}
				}

				// Spring Data Customization END
			} else {
				throw new JsonParseException("JSON reader expected an integer or string but found '%s'.",
						valueToken.getValue());
			}
			verifyToken(JsonTokenType.END_OBJECT);
		}
		return value;
	}

	private MaxKey visitMaxKeyExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.INT32, 1);
		verifyToken(JsonTokenType.END_OBJECT);
		return new MaxKey();
	}

	private MinKey visitMinKeyExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.INT32, 1);
		verifyToken(JsonTokenType.END_OBJECT);
		return new MinKey();
	}

	private ObjectId visitObjectIdExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		ObjectId objectId = new ObjectId(readStringFromExtendedJson());
		verifyToken(JsonTokenType.END_OBJECT);
		return objectId;
	}

	private BsonRegularExpression visitNewRegularExpressionExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.BEGIN_OBJECT);

		String pattern;
		String options = "";

		String firstKey = readStringFromExtendedJson();
		if (firstKey.equals("pattern")) {
			verifyToken(JsonTokenType.COLON);
			pattern = readStringFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("options");
			verifyToken(JsonTokenType.COLON);
			options = readStringFromExtendedJson();
		} else if (firstKey.equals("options")) {
			verifyToken(JsonTokenType.COLON);
			options = readStringFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("pattern");
			verifyToken(JsonTokenType.COLON);
			pattern = readStringFromExtendedJson();
		} else {
			throw new JsonParseException("Expected 't' and 'i' fields in $timestamp document but found " + firstKey);
		}

		verifyToken(JsonTokenType.END_OBJECT);
		verifyToken(JsonTokenType.END_OBJECT);
		return new BsonRegularExpression(pattern, options);
	}

	private BsonRegularExpression visitRegularExpressionExtendedJson(final String firstKey) {
		Mark extendedJsonMark = new Mark();

		try {
			verifyToken(JsonTokenType.COLON);

			String pattern;
			String options = "";
			if (firstKey.equals("$regex")) {
				pattern = readStringFromExtendedJson();
				verifyToken(JsonTokenType.COMMA);
				verifyString("$options");
				verifyToken(JsonTokenType.COLON);
				options = readStringFromExtendedJson();
			} else {
				options = readStringFromExtendedJson();
				verifyToken(JsonTokenType.COMMA);
				verifyString("$regex");
				verifyToken(JsonTokenType.COLON);
				pattern = readStringFromExtendedJson();
			}
			verifyToken(JsonTokenType.END_OBJECT);
			return new BsonRegularExpression(pattern, options);
		} catch (JsonParseException e) {
			extendedJsonMark.reset();
			return null;
		}
	}

	private String readStringFromExtendedJson() {
		JsonToken patternToken = popToken();

		// Spring Data Customization START

		if (patternToken.getType() == JsonTokenType.STRING || patternToken.getType() == JsonTokenType.UNQUOTED_STRING) {
			Object value = bindableValueFor(patternToken).getValue();
			return value != null ? value.toString() : null;
		}

		throw new JsonParseException("JSON reader expected a string but found '%s'.", patternToken.getValue());

		// Spring Data Customization END
	}

	private String visitSymbolExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		String symbol = readStringFromExtendedJson();
		verifyToken(JsonTokenType.END_OBJECT);
		return symbol;
	}

	private BsonTimestamp visitTimestampExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.BEGIN_OBJECT);

		int time;
		int increment;

		String firstKey = readStringFromExtendedJson();
		if (firstKey.equals("t")) {
			verifyToken(JsonTokenType.COLON);
			time = readIntFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("i");
			verifyToken(JsonTokenType.COLON);
			increment = readIntFromExtendedJson();
		} else if (firstKey.equals("i")) {
			verifyToken(JsonTokenType.COLON);
			increment = readIntFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("t");
			verifyToken(JsonTokenType.COLON);
			time = readIntFromExtendedJson();
		} else {
			throw new JsonParseException("Expected 't' and 'i' fields in $timestamp document but found " + firstKey);
		}

		verifyToken(JsonTokenType.END_OBJECT);
		verifyToken(JsonTokenType.END_OBJECT);
		return new BsonTimestamp(time, increment);
	}

	private int readIntFromExtendedJson() {
		JsonToken nextToken = popToken();
		int value;
		if (nextToken.getType() == JsonTokenType.INT32) {
			value = nextToken.getValue(Integer.class);
		} else if (nextToken.getType() == JsonTokenType.INT64) {
			value = nextToken.getValue(Long.class).intValue();
		} else {
			throw new JsonParseException("JSON reader expected an integer but found '%s'.", nextToken.getValue());
		}
		return value;
	}

	private BsonBinary visitUuidExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		String hexString = this.readStringFromExtendedJson().replace("-", "");
		verifyToken(JsonTokenType.END_OBJECT);
		try {
			return new BsonBinary(BsonBinarySubType.UUID_STANDARD, decodeHex(hexString));
		} catch (IllegalArgumentException e) {
			throw new JsonParseException(e);
		}
	}

	private void visitJavaScriptExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		String code = readStringFromExtendedJson();
		JsonToken nextToken = popToken();
		switch (nextToken.getType()) {
			case COMMA:
				verifyString("$scope");
				verifyToken(JsonTokenType.COLON);
				setState(State.VALUE);
				currentValue = code;
				setCurrentBsonType(BsonType.JAVASCRIPT_WITH_SCOPE);
				setContext(new Context(getContext(), BsonContextType.SCOPE_DOCUMENT));
				break;
			case END_OBJECT:
				currentValue = code;
				setCurrentBsonType(BsonType.JAVASCRIPT);
				break;
			default:
				throw new JsonParseException("JSON reader expected ',' or '}' but found '%s'.", nextToken);
		}
	}

	private BsonUndefined visitUndefinedExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		JsonToken valueToken = popToken();
		if (!valueToken.getValue(String.class).equals("true")) {
			throw new JsonParseException("JSON reader requires $undefined to have the value of true but found '%s'.",
					valueToken.getValue());
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return new BsonUndefined();
	}

	private Long visitNumberLongExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		Long value;
		String longAsString = readStringFromExtendedJson();
		try {
			value = Long.valueOf(longAsString);
		} catch (NumberFormatException e) {
			throw new JsonParseException(
					format("Exception converting value '%s' to type %s", longAsString, Long.class.getName()), e);
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return value;
	}

	private Integer visitNumberIntExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		Integer value;
		String intAsString = readStringFromExtendedJson();
		try {
			value = Integer.valueOf(intAsString);
		} catch (NumberFormatException e) {
			throw new JsonParseException(
					format("Exception converting value '%s' to type %s", intAsString, Integer.class.getName()), e);
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return value;
	}

	private Double visitNumberDoubleExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		Double value;
		String doubleAsString = readStringFromExtendedJson();
		try {
			value = Double.valueOf(doubleAsString);
		} catch (NumberFormatException e) {
			throw new JsonParseException(
					format("Exception converting value '%s' to type %s", doubleAsString, Double.class.getName()), e);
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return value;
	}

	private Decimal128 visitNumberDecimalExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		Decimal128 value;
		String decimal128AsString = readStringFromExtendedJson();
		try {
			value = Decimal128.parse(decimal128AsString);
		} catch (NumberFormatException e) {
			throw new JsonParseException(
					format("Exception converting value '%s' to type %s", decimal128AsString, Decimal128.class.getName()), e);
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return value;
	}

	private BsonDbPointer visitDbPointerExtendedJson() {
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.BEGIN_OBJECT);

		String ref;
		ObjectId oid;

		String firstKey = readStringFromExtendedJson();
		if (firstKey.equals("$ref")) {
			verifyToken(JsonTokenType.COLON);
			ref = readStringFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("$id");
			oid = readDbPointerIdFromExtendedJson();
			verifyToken(JsonTokenType.END_OBJECT);
		} else if (firstKey.equals("$id")) {
			oid = readDbPointerIdFromExtendedJson();
			verifyToken(JsonTokenType.COMMA);
			verifyString("$ref");
			verifyToken(JsonTokenType.COLON);
			ref = readStringFromExtendedJson();

		} else {
			throw new JsonParseException("Expected $ref and $id fields in $dbPointer document but found " + firstKey);
		}
		verifyToken(JsonTokenType.END_OBJECT);
		return new BsonDbPointer(ref, oid);
	}

	private ObjectId readDbPointerIdFromExtendedJson() {
		ObjectId oid;
		verifyToken(JsonTokenType.COLON);
		verifyToken(JsonTokenType.BEGIN_OBJECT);
		verifyToken(JsonTokenType.STRING, "$oid");
		oid = visitObjectIdExtendedJson();
		return oid;
	}

	@Override
	public BsonReaderMark getMark() {
		return new Mark();
	}

	@Override
	protected Context getContext() {
		return (Context) super.getContext();
	}

	protected class Mark extends AbstractBsonReader.Mark {
		private final JsonToken pushedToken;
		private final Object currentValue;
		private final int position;

		protected Mark() {
			super();
			pushedToken = ParameterBindingJsonReader.this.pushedToken;
			currentValue = ParameterBindingJsonReader.this.currentValue;
			position = ParameterBindingJsonReader.this.scanner.getBufferPosition();
		}

		public void reset() {
			super.reset();
			ParameterBindingJsonReader.this.pushedToken = pushedToken;
			ParameterBindingJsonReader.this.currentValue = currentValue;
			ParameterBindingJsonReader.this.scanner.setBufferPosition(position);
			ParameterBindingJsonReader.this.setContext(new Context(getParentContext(), getContextType()));
		}
	}

	protected class Context extends AbstractBsonReader.Context {
		protected Context(final AbstractBsonReader.Context parentContext, final BsonContextType contextType) {
			super(parentContext, contextType);
		}

		protected Context getParentContext() {
			return (Context) super.getParentContext();
		}

		protected BsonContextType getContextType() {
			return super.getContextType();
		}
	}

	private static byte[] decodeHex(final String hex) {
		if (hex.length() % 2 != 0) {
			throw new IllegalArgumentException("A hex string must contain an even number of characters: " + hex);
		}

		byte[] out = new byte[hex.length() / 2];

		for (int i = 0; i < hex.length(); i += 2) {
			int high = Character.digit(hex.charAt(i), 16);
			int low = Character.digit(hex.charAt(i + 1), 16);
			if (high == -1 || low == -1) {
				throw new IllegalArgumentException("A hex string can only contain the characters 0-9, A-F, a-f: " + hex);
			}

			out[i / 2] = (byte) (high * 16 + low);
		}

		return out;
	}

	// Spring Data Customization START

	static class BindableValue {

		private BsonType type;
		private Object value;
		private int index;

		BindableValue() {}

		BsonType getType() {
			return type;
		}

		void setType(BsonType type) {
			this.type = type;
		}

		Object getValue() {
			return value;
		}

		void setValue(Object value) {
			this.value = value;
		}

		int getIndex() {
			return index;
		}

		void setIndex(int index) {
			this.index = index;
		}
	}

	// Spring Data Customization END
}