org.elasticsearch.common.xcontent.XContentHelper Maven / Gradle / Ivy
Show all versions of elasticsearch Show documentation
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.common.xcontent;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.Compressor;
import org.elasticsearch.common.compress.CompressorFactory;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.xcontent.DeprecationHandler;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.ToXContent.Params;
import org.elasticsearch.xcontent.XContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentParseException;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentType;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@SuppressWarnings("unchecked")
public class XContentHelper {
/**
* Creates a parser based on the bytes provided
* @deprecated use {@link #createParser(NamedXContentRegistry, DeprecationHandler, BytesReference, XContentType)}
* to avoid content type auto-detection
*/
@Deprecated
public static XContentParser createParser(
NamedXContentRegistry xContentRegistry,
DeprecationHandler deprecationHandler,
BytesReference bytes
) throws IOException {
Compressor compressor = CompressorFactory.compressor(bytes);
if (compressor != null) {
InputStream compressedInput = compressor.threadLocalInputStream(bytes.streamInput());
if (compressedInput.markSupported() == false) {
compressedInput = new BufferedInputStream(compressedInput);
}
final XContentType contentType = XContentFactory.xContentType(compressedInput);
return XContentFactory.xContent(contentType).createParser(xContentRegistry, deprecationHandler, compressedInput);
} else {
return XContentFactory.xContent(xContentType(bytes)).createParser(xContentRegistry, deprecationHandler, bytes.streamInput());
}
}
/**
* Creates a parser for the bytes using the supplied content-type
*/
public static XContentParser createParser(
NamedXContentRegistry xContentRegistry,
DeprecationHandler deprecationHandler,
BytesReference bytes,
XContentType xContentType
) throws IOException {
Objects.requireNonNull(xContentType);
Compressor compressor = CompressorFactory.compressor(bytes);
if (compressor != null) {
InputStream compressedInput = compressor.threadLocalInputStream(bytes.streamInput());
if (compressedInput.markSupported() == false) {
compressedInput = new BufferedInputStream(compressedInput);
}
return XContentFactory.xContent(xContentType).createParser(xContentRegistry, deprecationHandler, compressedInput);
} else {
if (bytes.hasArray()) {
return xContentType.xContent()
.createParser(xContentRegistry, deprecationHandler, bytes.array(), bytes.arrayOffset(), bytes.length());
}
return xContentType.xContent().createParser(xContentRegistry, deprecationHandler, bytes.streamInput());
}
}
/**
* Converts the given bytes into a map that is optionally ordered.
*
* Important: This can lose precision on numbers with a decimal point. It
* converts numbers like {@code "n": 1234.567} to a {@code double} which
* only has 52 bits of precision in the mantissa. This will come up most
* frequently when folks write nanosecond precision dates as a decimal
* number.
* @deprecated this method relies on auto-detection of content type. Use {@link #convertToMap(BytesReference, boolean, XContentType)}
* instead with the proper {@link XContentType}
*/
@Deprecated
public static Tuple> convertToMap(BytesReference bytes, boolean ordered)
throws ElasticsearchParseException {
return convertToMap(bytes, ordered, null);
}
/**
* Converts the given bytes into a map that is optionally ordered. The provided {@link XContentType} must be non-null.
*
* Important: This can lose precision on numbers with a decimal point. It
* converts numbers like {@code "n": 1234.567} to a {@code double} which
* only has 52 bits of precision in the mantissa. This will come up most
* frequently when folks write nanosecond precision dates as a decimal
* number.
*/
public static Tuple> convertToMap(BytesReference bytes, boolean ordered, XContentType xContentType)
throws ElasticsearchParseException {
try {
final XContentType contentType;
InputStream input;
Compressor compressor = CompressorFactory.compressor(bytes);
if (compressor != null) {
InputStream compressedStreamInput = compressor.threadLocalInputStream(bytes.streamInput());
if (compressedStreamInput.markSupported() == false) {
compressedStreamInput = new BufferedInputStream(compressedStreamInput);
}
input = compressedStreamInput;
contentType = xContentType != null ? xContentType : XContentFactory.xContentType(input);
} else if (bytes.hasArray()) {
final byte[] raw = bytes.array();
final int offset = bytes.arrayOffset();
final int length = bytes.length();
contentType = xContentType != null ? xContentType : XContentFactory.xContentType(raw, offset, length);
return new Tuple<>(
Objects.requireNonNull(contentType),
convertToMap(XContentFactory.xContent(contentType), raw, offset, length, ordered)
);
} else {
input = bytes.streamInput();
contentType = xContentType != null ? xContentType : XContentFactory.xContentType(input);
}
try (InputStream stream = input) {
return new Tuple<>(
Objects.requireNonNull(contentType),
convertToMap(XContentFactory.xContent(contentType), stream, ordered)
);
}
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
/**
* Convert a string in some {@link XContent} format to a {@link Map}. Throws an {@link ElasticsearchParseException} if there is any
* error.
*/
public static Map convertToMap(XContent xContent, String string, boolean ordered) throws ElasticsearchParseException {
// It is safe to use EMPTY here because this never uses namedObject
try (
XContentParser parser = xContent.createParser(
NamedXContentRegistry.EMPTY,
DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
string
)
) {
return ordered ? parser.mapOrdered() : parser.map();
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
/**
* Convert a string in some {@link XContent} format to a {@link Map}. Throws an {@link ElasticsearchParseException} if there is any
* error. Note that unlike {@link #convertToMap(BytesReference, boolean)}, this doesn't automatically uncompress the input.
*/
public static Map convertToMap(XContent xContent, InputStream input, boolean ordered)
throws ElasticsearchParseException {
// It is safe to use EMPTY here because this never uses namedObject
try (
XContentParser parser = xContent.createParser(
NamedXContentRegistry.EMPTY,
DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
input
)
) {
return ordered ? parser.mapOrdered() : parser.map();
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
/**
* Convert a byte array in some {@link XContent} format to a {@link Map}. Throws an {@link ElasticsearchParseException} if there is any
* error. Note that unlike {@link #convertToMap(BytesReference, boolean)}, this doesn't automatically uncompress the input.
*/
public static Map convertToMap(XContent xContent, byte[] bytes, int offset, int length, boolean ordered)
throws ElasticsearchParseException {
// It is safe to use EMPTY here because this never uses namedObject
try (
XContentParser parser = xContent.createParser(
NamedXContentRegistry.EMPTY,
DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
bytes,
offset,
length
)
) {
return ordered ? parser.mapOrdered() : parser.map();
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
@Deprecated
public static String convertToJson(BytesReference bytes, boolean reformatJson) throws IOException {
return convertToJson(bytes, reformatJson, false);
}
@Deprecated
public static String convertToJson(BytesReference bytes, boolean reformatJson, boolean prettyPrint) throws IOException {
return convertToJson(bytes, reformatJson, prettyPrint, XContentFactory.xContentType(bytes.toBytesRef().bytes));
}
public static String convertToJson(BytesReference bytes, boolean reformatJson, XContentType xContentType) throws IOException {
return convertToJson(bytes, reformatJson, false, xContentType);
}
/**
* Accepts a JSON string, parses it and prints it without pretty-printing it. This is useful
* where a piece of JSON is formatted for legibility, but needs to be stripped of unnecessary
* whitespace e.g. for comparison in a test.
*
* @param json the JSON to format
* @return reformatted JSON
* @throws IOException if the reformatting fails, e.g. because the JSON is not well-formed
*/
public static String stripWhitespace(String json) throws IOException {
return convertToJson(new BytesArray(json), true, XContentType.JSON);
}
public static String convertToJson(BytesReference bytes, boolean reformatJson, boolean prettyPrint, XContentType xContentType)
throws IOException {
Objects.requireNonNull(xContentType);
if (xContentType == XContentType.JSON && reformatJson == false) {
return bytes.utf8ToString();
}
// It is safe to use EMPTY here because this never uses namedObject
if (bytes.hasArray()) {
try (
XContentParser parser = XContentFactory.xContent(xContentType)
.createParser(
NamedXContentRegistry.EMPTY,
DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
bytes.array(),
bytes.arrayOffset(),
bytes.length()
)
) {
return toJsonString(prettyPrint, parser);
}
} else {
try (
InputStream stream = bytes.streamInput();
XContentParser parser = XContentFactory.xContent(xContentType)
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, stream)
) {
return toJsonString(prettyPrint, parser);
}
}
}
private static String toJsonString(boolean prettyPrint, XContentParser parser) throws IOException {
parser.nextToken();
XContentBuilder builder = XContentFactory.jsonBuilder();
if (prettyPrint) {
builder.prettyPrint();
}
builder.copyCurrentStructure(parser);
return Strings.toString(builder);
}
/**
* Updates the provided changes into the source. If the key exists in the changes, it overrides the one in source
* unless both are Maps, in which case it recursively updated it.
*
* @param source the original map to be updated
* @param changes the changes to update into updated
* @param checkUpdatesAreUnequal should this method check if updates to the same key (that are not both maps) are
* unequal? This is just a .equals check on the objects, but that can take some time on long strings.
* @return true if the source map was modified
*/
public static boolean update(Map source, Map changes, boolean checkUpdatesAreUnequal) {
boolean modified = false;
for (Map.Entry changesEntry : changes.entrySet()) {
if (source.containsKey(changesEntry.getKey()) == false) {
// safe to copy, change does not exist in source
source.put(changesEntry.getKey(), changesEntry.getValue());
modified = true;
continue;
}
Object old = source.get(changesEntry.getKey());
if (old instanceof Map && changesEntry.getValue() instanceof Map) {
// recursive merge maps
modified |= update(
(Map) source.get(changesEntry.getKey()),
(Map) changesEntry.getValue(),
checkUpdatesAreUnequal && modified == false
);
continue;
}
// update the field
source.put(changesEntry.getKey(), changesEntry.getValue());
if (modified) {
continue;
}
if (checkUpdatesAreUnequal == false) {
modified = true;
continue;
}
modified = Objects.equals(old, changesEntry.getValue()) == false;
}
return modified;
}
/**
* Merges the defaults provided as the second parameter into the content of the first. Only does recursive merge
* for inner maps.
*/
public static void mergeDefaults(Map content, Map defaults) {
for (Map.Entry defaultEntry : defaults.entrySet()) {
if (content.containsKey(defaultEntry.getKey()) == false) {
// copy it over, it does not exists in the content
content.put(defaultEntry.getKey(), defaultEntry.getValue());
} else {
// in the content and in the default, only merge compound ones (maps)
if (content.get(defaultEntry.getKey()) instanceof Map && defaultEntry.getValue() instanceof Map) {
mergeDefaults((Map) content.get(defaultEntry.getKey()), (Map) defaultEntry.getValue());
} else if (content.get(defaultEntry.getKey()) instanceof List && defaultEntry.getValue() instanceof List) {
List