/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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
*
* http://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.elasticsearch.common.xcontent;
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.compress.CompressedStreamInput;
import org.elasticsearch.common.compress.Compressor;
import org.elasticsearch.common.compress.CompressorFactory;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.io.stream.BytesStreamInput;
import org.elasticsearch.common.xcontent.ToXContent.Params;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS;
/**
*
*/
@SuppressWarnings("unchecked")
public class XContentHelper {
public static XContentParser createParser(BytesReference bytes) throws IOException {
if (bytes.hasArray()) {
return createParser(bytes.array(), bytes.arrayOffset(), bytes.length());
}
Compressor compressor = CompressorFactory.compressor(bytes);
if (compressor != null) {
CompressedStreamInput compressedInput = compressor.streamInput(bytes.streamInput());
XContentType contentType = XContentFactory.xContentType(compressedInput);
compressedInput.resetToBufferStart();
return XContentFactory.xContent(contentType).createParser(compressedInput);
} else {
return XContentFactory.xContent(bytes).createParser(bytes.streamInput());
}
}
public static XContentParser createParser(byte[] data, int offset, int length) throws IOException {
Compressor compressor = CompressorFactory.compressor(data, offset, length);
if (compressor != null) {
CompressedStreamInput compressedInput = compressor.streamInput(new BytesStreamInput(data, offset, length, false));
XContentType contentType = XContentFactory.xContentType(compressedInput);
compressedInput.resetToBufferStart();
return XContentFactory.xContent(contentType).createParser(compressedInput);
} else {
return XContentFactory.xContent(data, offset, length).createParser(data, offset, length);
}
}
public static Tuple<XContentType, Map<String, Object>> convertToMap(BytesReference bytes, boolean ordered) throws ElasticsearchParseException {
if (bytes.hasArray()) {
return convertToMap(bytes.array(), bytes.arrayOffset(), bytes.length(), ordered);
}
try {
XContentParser parser;
XContentType contentType;
Compressor compressor = CompressorFactory.compressor(bytes);
if (compressor != null) {
CompressedStreamInput compressedStreamInput = compressor.streamInput(bytes.streamInput());
contentType = XContentFactory.xContentType(compressedStreamInput);
compressedStreamInput.resetToBufferStart();
parser = XContentFactory.xContent(contentType).createParser(compressedStreamInput);
} else {
contentType = XContentFactory.xContentType(bytes);
parser = XContentFactory.xContent(contentType).createParser(bytes.streamInput());
}
if (ordered) {
return Tuple.tuple(contentType, parser.mapOrderedAndClose());
} else {
return Tuple.tuple(contentType, parser.mapAndClose());
}
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
public static Tuple<XContentType, Map<String, Object>> convertToMap(byte[] data, boolean ordered) throws ElasticsearchParseException {
return convertToMap(data, 0, data.length, ordered);
}
public static Tuple<XContentType, Map<String, Object>> convertToMap(byte[] data, int offset, int length, boolean ordered) throws ElasticsearchParseException {
try {
XContentParser parser;
XContentType contentType;
Compressor compressor = CompressorFactory.compressor(data, offset, length);
if (compressor != null) {
CompressedStreamInput compressedStreamInput = compressor.streamInput(new BytesStreamInput(data, offset, length, false));
contentType = XContentFactory.xContentType(compressedStreamInput);
compressedStreamInput.resetToBufferStart();
parser = XContentFactory.xContent(contentType).createParser(compressedStreamInput);
} else {
contentType = XContentFactory.xContentType(data, offset, length);
parser = XContentFactory.xContent(contentType).createParser(data, offset, length);
}
if (ordered) {
return Tuple.tuple(contentType, parser.mapOrderedAndClose());
} else {
return Tuple.tuple(contentType, parser.mapAndClose());
}
} catch (IOException e) {
throw new ElasticsearchParseException("Failed to parse content to map", e);
}
}
public static String convertToJson(BytesReference bytes, boolean reformatJson) throws IOException {
return convertToJson(bytes, reformatJson, false);
}
public static String convertToJson(BytesReference bytes, boolean reformatJson, boolean prettyPrint) throws IOException {
if (bytes.hasArray()) {
return convertToJson(bytes.array(), bytes.arrayOffset(), bytes.length(), reformatJson, prettyPrint);
}
XContentType xContentType = XContentFactory.xContentType(bytes);
if (xContentType == XContentType.JSON && !reformatJson) {
BytesArray bytesArray = bytes.toBytesArray();
return new String(bytesArray.array(), bytesArray.arrayOffset(), bytesArray.length(), Charsets.UTF_8);
}
XContentParser parser = null;
try {
parser = XContentFactory.xContent(xContentType).createParser(bytes.streamInput());
parser.nextToken();
XContentBuilder builder = XContentFactory.jsonBuilder();
if (prettyPrint) {
builder.prettyPrint();
}
builder.copyCurrentStructure(parser);
return builder.string();
} finally {
if (parser != null) {
parser.close();
}
}
}
public static String convertToJson(byte[] data, int offset, int length, boolean reformatJson) throws IOException {
return convertToJson(data, offset, length, reformatJson, false);
}
public static String convertToJson(byte[] data, int offset, int length, boolean reformatJson, boolean prettyPrint) throws IOException {
XContentType xContentType = XContentFactory.xContentType(data, offset, length);
if (xContentType == XContentType.JSON && !reformatJson) {
return new String(data, offset, length, Charsets.UTF_8);
}
XContentParser parser = null;
try {
parser = XContentFactory.xContent(xContentType).createParser(data, offset, length);
parser.nextToken();
XContentBuilder builder = XContentFactory.jsonBuilder();
if (prettyPrint) {
builder.prettyPrint();
}
builder.copyCurrentStructure(parser);
return builder.string();
} finally {
if (parser != null) {
parser.close();
}
}
}
/**
* Writes serialized toXContent to pretty-printed JSON string.
*
* @param toXContent object to be pretty printed
* @return pretty-printed JSON serialization
*/
public static String toString(ToXContent toXContent) {
return toString(toXContent, EMPTY_PARAMS);
}
/**
* Writes serialized toXContent to pretty-printed JSON string.
*
* @param toXContent object to be pretty printed
* @param params serialization parameters
* @return pretty-printed JSON serialization
*/
public static String toString(ToXContent toXContent, Params params) {
try {
XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint();
builder.startObject();
toXContent.toXContent(builder, params);
builder.endObject();
return builder.string();
} catch (IOException e) {
try {
XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint();
builder.startObject();
builder.field("error", e.getMessage());
builder.endObject();
return builder.string();
} catch (IOException e2) {
throw new ElasticsearchException("cannot generate error message for deserialization", e);
}
}
}
/**
* 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 recuersively 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<String, Object> source, Map<String, Object> changes, boolean checkUpdatesAreUnequal) {
boolean modified = false;
for (Map.Entry<String, Object> changesEntry : changes.entrySet()) {
if (!source.containsKey(changesEntry.getKey())) {
// 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<String, Object>) source.get(changesEntry.getKey()),
(Map<String, Object>) changesEntry.getValue(), checkUpdatesAreUnequal && !modified);
continue;
}
// update the field
source.put(changesEntry.getKey(), changesEntry.getValue());
if (modified) {
continue;
}
if (!checkUpdatesAreUnequal || old == null) {
modified = true;
continue;
}
modified = !old.equals(changesEntry.getValue());
}
return modified;
}
/**
* Merges the defaults provided as the second parameter into the content of the first. Only does recursive merge
* for inner maps.
*/
@SuppressWarnings({"unchecked"})
public static void mergeDefaults(Map<String, Object> content, Map<String, Object> defaults) {
for (Map.Entry<String, Object> defaultEntry : defaults.entrySet()) {
if (!content.containsKey(defaultEntry.getKey())) {
// 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<String, Object>) content.get(defaultEntry.getKey()), (Map<String, Object>) defaultEntry.getValue());
} else if (content.get(defaultEntry.getKey()) instanceof List && defaultEntry.getValue() instanceof List) {
List defaultList = (List) defaultEntry.getValue();
List contentList = (List) content.get(defaultEntry.getKey());
List mergedList = new ArrayList();
if (allListValuesAreMapsOfOne(defaultList) && allListValuesAreMapsOfOne(contentList)) {
// all are in the form of [ {"key1" : {}}, {"key2" : {}} ], merge based on keys
Map<String, Map<String, Object>> processed = Maps.newLinkedHashMap();
for (Object o : contentList) {
Map<String, Object> map = (Map<String, Object>) o;
Map.Entry<String, Object> entry = map.entrySet().iterator().next();
processed.put(entry.getKey(), map);
}
for (Object o : defaultList) {
Map<String, Object> map = (Map<String, Object>) o;
Map.Entry<String, Object> entry = map.entrySet().iterator().next();
if (processed.containsKey(entry.getKey())) {
mergeDefaults(processed.get(entry.getKey()), map);
} else {
// put the default entries after the content ones.
processed.put(entry.getKey(), map);
}
}
for (Map<String, Object> map : processed.values()) {
mergedList.add(map);
}
} else {
// if both are lists, simply combine them, first the defaults, then the content
// just make sure not to add the same value twice
mergedList.addAll(defaultList);
for (Object o : contentList) {
if (!mergedList.contains(o)) {
mergedList.add(o);
}
}
}
content.put(defaultEntry.getKey(), mergedList);
}
}
}
}
private static boolean allListValuesAreMapsOfOne(List list) {
for (Object o : list) {
if (!(o instanceof Map)) {
return false;
}
if (((Map) o).size() != 1) {
return false;
}
}
return true;
}
public static void copyCurrentStructure(XContentGenerator generator, XContentParser parser) throws IOException {
XContentParser.Token token = parser.currentToken();
// Let's handle field-name separately first
if (token == XContentParser.Token.FIELD_NAME) {
generator.writeFieldName(parser.currentName());
token = parser.nextToken();
// fall-through to copy the associated value
}
switch (token) {
case START_ARRAY:
generator.writeStartArray();
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
copyCurrentStructure(generator, parser);
}
generator.writeEndArray();
break;
case START_OBJECT:
generator.writeStartObject();
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
copyCurrentStructure(generator, parser);
}
generator.writeEndObject();
break;
default: // others are simple:
copyCurrentEvent(generator, parser);
}
}
public static void copyCurrentEvent(XContentGenerator generator, XContentParser parser) throws IOException {
switch (parser.currentToken()) {
case START_OBJECT:
generator.writeStartObject();
break;
case END_OBJECT:
generator.writeEndObject();
break;
case START_ARRAY:
generator.writeStartArray();
break;
case END_ARRAY:
generator.writeEndArray();
break;
case FIELD_NAME:
generator.writeFieldName(parser.currentName());
break;
case VALUE_STRING:
if (parser.hasTextCharacters()) {
generator.writeString(parser.textCharacters(), parser.textOffset(), parser.textLength());
} else {
generator.writeString(parser.text());
}
break;
case VALUE_NUMBER:
switch (parser.numberType()) {
case INT:
generator.writeNumber(parser.intValue());
break;
case LONG:
generator.writeNumber(parser.longValue());
break;
case FLOAT:
generator.writeNumber(parser.floatValue());
break;
case DOUBLE:
generator.writeNumber(parser.doubleValue());
break;
}
break;
case VALUE_BOOLEAN:
generator.writeBoolean(parser.booleanValue());
break;
case VALUE_NULL:
generator.writeNull();
break;
case VALUE_EMBEDDED_OBJECT:
generator.writeBinary(parser.binaryValue());
}
}
/**
* Directly writes the source to the output builder
*/
public static void writeDirect(BytesReference source, XContentBuilder rawBuilder, ToXContent.Params params) throws IOException {
Compressor compressor = CompressorFactory.compressor(source);
if (compressor != null) {
CompressedStreamInput compressedStreamInput = compressor.streamInput(source.streamInput());
XContentType contentType = XContentFactory.xContentType(compressedStreamInput);
compressedStreamInput.resetToBufferStart();
if (contentType == rawBuilder.contentType()) {
Streams.copy(compressedStreamInput, rawBuilder.stream());
} else {
try (XContentParser parser = XContentFactory.xContent(contentType).createParser(compressedStreamInput)) {
parser.nextToken();
rawBuilder.copyCurrentStructure(parser);
}
}
} else {
XContentType contentType = XContentFactory.xContentType(source);
if (contentType == rawBuilder.contentType()) {
source.writeTo(rawBuilder.stream());
} else {
try (XContentParser parser = XContentFactory.xContent(contentType).createParser(source)) {
parser.nextToken();
rawBuilder.copyCurrentStructure(parser);
}
}
}
}
/**
* Writes a "raw" (bytes) field, handling cases where the bytes are compressed, and tries to optimize writing using
* {@link XContentBuilder#rawField(String, org.elasticsearch.common.bytes.BytesReference)}.
*/
public static void writeRawField(String field, BytesReference source, XContentBuilder builder, ToXContent.Params params) throws IOException {
Compressor compressor = CompressorFactory.compressor(source);
if (compressor != null) {
CompressedStreamInput compressedStreamInput = compressor.streamInput(source.streamInput());
XContentType contentType = XContentFactory.xContentType(compressedStreamInput);
compressedStreamInput.resetToBufferStart();
if (contentType == builder.contentType()) {
builder.rawField(field, compressedStreamInput);
} else {
try (XContentParser parser = XContentFactory.xContent(contentType).createParser(compressedStreamInput)) {
parser.nextToken();
builder.field(field);
builder.copyCurrentStructure(parser);
}
}
} else {
XContentType contentType = XContentFactory.xContentType(source);
if (contentType == builder.contentType()) {
builder.rawField(field, source);
} else {
try (XContentParser parser = XContentFactory.xContent(contentType).createParser(source)) {
parser.nextToken();
builder.field(field);
builder.copyCurrentStructure(parser);
}
}
}
}
}