SpringJsonWriter.java
/*
* Copyright 2025-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;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import org.bson.BsonBinary;
import org.bson.BsonDbPointer;
import org.bson.BsonReader;
import org.bson.BsonRegularExpression;
import org.bson.BsonTimestamp;
import org.bson.BsonWriter;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.jspecify.annotations.NullUnmarked;
import org.springframework.util.StringUtils;
/**
* Internal {@link BsonWriter} implementation that allows to render {@link #writePlaceholder(String) placeholders} as
* {@code ?0}.
*
* @author Christoph Strobl
* @since 5.0
*/
@NullUnmarked
class SpringJsonWriter implements BsonWriter {
private final StringBuffer buffer;
private enum JsonContextType {
TOP_LEVEL, DOCUMENT, ARRAY,
}
private enum State {
INITIAL, NAME, VALUE, DONE
}
private static class JsonContext {
private final JsonContext parentContext;
private final JsonContextType contextType;
private boolean hasElements;
JsonContext(final JsonContext parentContext, final JsonContextType contextType) {
this.parentContext = parentContext;
this.contextType = contextType;
}
JsonContext nestedDocument() {
return new JsonContext(this, JsonContextType.DOCUMENT);
}
JsonContext nestedArray() {
return new JsonContext(this, JsonContextType.ARRAY);
}
}
private JsonContext context = new JsonContext(null, JsonContextType.TOP_LEVEL);
private State state = State.INITIAL;
public SpringJsonWriter(StringBuffer buffer) {
this.buffer = buffer;
}
@Override
public void flush() {}
@Override
public void writeBinaryData(BsonBinary binary) {
preWriteValue();
writeStartDocument();
writeName("$binary");
writeStartDocument();
writeName("base64");
writeString(Base64.getEncoder().encodeToString(binary.getData()));
writeName("subType");
writeInt32(binary.getBsonType().getValue());
writeEndDocument();
writeEndDocument();
}
@Override
public void writeBinaryData(String name, BsonBinary binary) {
writeName(name);
writeBinaryData(binary);
}
@Override
public void writeBoolean(boolean value) {
preWriteValue();
write(value ? "true" : "false");
setNextState();
}
@Override
public void writeBoolean(String name, boolean value) {
writeName(name);
writeBoolean(value);
}
@Override
public void writeDateTime(long value) {
// "$date": "2018-11-10T22:26:12.111Z"
writeStartDocument();
writeName("$date");
writeString(ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME));
writeEndDocument();
}
@Override
public void writeDateTime(String name, long value) {
writeName(name);
writeDateTime(value);
}
@Override
public void writeDBPointer(BsonDbPointer value) {
}
@Override
public void writeDBPointer(String name, BsonDbPointer value) {
}
@Override // {"$numberDouble":"10.5"}
public void writeDouble(double value) {
writeStartDocument();
writeName("$numberDouble");
writeString(Double.valueOf(value).toString());
writeEndDocument();
}
@Override
public void writeDouble(String name, double value) {
writeName(name);
writeDouble(value);
}
@Override
public void writeEndArray() {
write("]");
context = context.parentContext;
if (context.contextType == JsonContextType.TOP_LEVEL) {
state = State.DONE;
} else {
setNextState();
}
}
@Override
public void writeEndDocument() {
buffer.append("}");
context = context.parentContext;
if (context.contextType == JsonContextType.TOP_LEVEL) {
state = State.DONE;
} else {
setNextState();
}
}
@Override
public void writeInt32(int value) {
writeStartDocument();
writeName("$numberInt");
writeString(Integer.valueOf(value).toString());
writeEndDocument();
}
@Override
public void writeInt32(String name, int value) {
writeName(name);
writeInt32(value);
}
@Override
public void writeInt64(long value) {
writeStartDocument();
writeName("$numberLong");
writeString(Long.valueOf(value).toString());
writeEndDocument();
}
@Override
public void writeInt64(String name, long value) {
writeName(name);
writeInt64(value);
}
@Override
public void writeDecimal128(Decimal128 value) {
// { "$numberDecimal": "<number>" }
writeStartDocument();
writeName("$numberDecimal");
writeString(value.toString());
writeEndDocument();
}
@Override
public void writeDecimal128(String name, Decimal128 value) {
writeName(name);
writeDecimal128(value);
}
@Override
public void writeJavaScript(String code) {
writeStartDocument();
writeName("$code");
writeString(code);
writeEndDocument();
}
@Override
public void writeJavaScript(String name, String code) {
writeName(name);
writeJavaScript(code);
}
@Override
public void writeJavaScriptWithScope(String code) {
}
@Override
public void writeJavaScriptWithScope(String name, String code) {
}
@Override
public void writeMaxKey() {
writeStartDocument();
writeName("$maxKey");
buffer.append(1);
writeEndDocument();
}
@Override
public void writeMaxKey(String name) {
writeName(name);
writeMaxKey();
}
@Override
public void writeMinKey() {
writeStartDocument();
writeName("$minKey");
buffer.append(1);
writeEndDocument();
}
@Override
public void writeMinKey(String name) {
writeName(name);
writeMinKey();
}
@Override
public void writeName(String name) {
if (context.hasElements) {
write(",");
} else {
context.hasElements = true;
}
writeString(name);
buffer.append(":");
state = State.VALUE;
}
@Override
public void writeNull() {
buffer.append("null");
}
@Override
public void writeNull(String name) {
writeName(name);
writeNull();
}
@Override
public void writeObjectId(ObjectId objectId) {
writeStartDocument();
writeName("$oid");
writeString(objectId.toHexString());
writeEndDocument();
}
@Override
public void writeObjectId(String name, ObjectId objectId) {
writeName(name);
writeObjectId(objectId);
}
@Override
public void writeRegularExpression(BsonRegularExpression regularExpression) {
writeStartDocument();
writeName("$regex");
write("/");
write(regularExpression.getPattern());
write("/");
if (StringUtils.hasText(regularExpression.getOptions())) {
writeName("$options");
writeString(regularExpression.getOptions());
}
writeEndDocument();
}
@Override
public void writeRegularExpression(String name, BsonRegularExpression regularExpression) {
writeName(name);
writeRegularExpression(regularExpression);
}
@Override
public void writeStartArray() {
preWriteValue();
write("[");
context = context.nestedArray();
}
@Override
public void writeStartArray(String name) {
writeName(name);
writeStartArray();
}
@Override
public void writeStartDocument() {
preWriteValue();
write("{");
context = context.nestedDocument();
state = State.NAME;
}
@Override
public void writeStartDocument(String name) {
writeName(name);
writeStartDocument();
}
@Override
public void writeString(String value) {
write("'");
write(value);
write("'");
}
@Override
public void writeString(String name, String value) {
writeName(name);
writeString(value);
}
@Override
public void writeSymbol(String value) {
writeStartDocument();
writeName("$symbol");
writeString(value);
writeEndDocument();
}
@Override
public void writeSymbol(String name, String value) {
writeName(name);
writeSymbol(value);
}
@Override // {"$timestamp": {"t": <t>, "i": <i>}}
public void writeTimestamp(BsonTimestamp value) {
preWriteValue();
writeStartDocument();
writeName("$timestamp");
writeStartDocument();
writeName("t");
buffer.append(value.getTime());
writeName("i");
buffer.append(value.getInc());
writeEndDocument();
writeEndDocument();
}
@Override
public void writeTimestamp(String name, BsonTimestamp value) {
writeName(name);
writeTimestamp(value);
}
@Override
public void writeUndefined() {
writeStartDocument();
writeName("$undefined");
writeBoolean(true);
writeEndDocument();
}
@Override
public void writeUndefined(String name) {
writeName(name);
writeUndefined();
}
@Override
public void pipe(BsonReader reader) {
}
/**
* @param placeholder
*/
public void writePlaceholder(String placeholder) {
write(placeholder);
}
public void write(String str) {
buffer.append(str);
}
private void preWriteValue() {
if (context.contextType == JsonContextType.ARRAY) {
if (context.hasElements) {
write(",");
}
}
context.hasElements = true;
}
private void setNextState() {
if (context.contextType == JsonContextType.ARRAY) {
state = State.VALUE;
} else {
state = State.NAME;
}
}
}