Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pluggable XContentBuilder writers and human readable writers #29120

Merged
merged 8 commits into from
Mar 20, 2018
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field(DELAYED_UNASSIGNED_SHARDS, getDelayedUnassignedShards());
builder.field(NUMBER_OF_PENDING_TASKS, getNumberOfPendingTasks());
builder.field(NUMBER_OF_IN_FLIGHT_FETCH, getNumberOfInFlightFetch());
builder.timeValueField(TASK_MAX_WAIT_TIME_IN_QUEUE_IN_MILLIS, TASK_MAX_WAIT_TIME_IN_QUEUE, getTaskMaxWaitingTime());
builder.humanReadableField(TASK_MAX_WAIT_TIME_IN_QUEUE_IN_MILLIS, TASK_MAX_WAIT_TIME_IN_QUEUE, getTaskMaxWaitingTime());
builder.percentageField(ACTIVE_SHARDS_PERCENT_AS_NUMBER, ACTIVE_SHARDS_PERCENT, getActiveShardsPercent());

String level = params.param("level", "cluster");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field("version", nodeInfo.getVersion());
builder.field("build_hash", nodeInfo.getBuild().shortHash());
if (nodeInfo.getTotalIndexingBuffer() != null) {
builder.byteSizeField("total_indexing_buffer", "total_indexing_buffer_in_bytes", nodeInfo.getTotalIndexingBuffer());
builder.humanReadableField("total_indexing_buffer", "total_indexing_buffer_in_bytes", nodeInfo.getTotalIndexingBuffer());
}

builder.startArray("roles");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Streamable;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.common.xcontent.XContentBuilder;
Expand Down Expand Up @@ -143,7 +144,7 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par
builder.byteSizeField(Fields.TOTAL_SIZE_IN_BYTES, Fields.TOTAL_SIZE, getTotalSize());
builder.byteSizeField(Fields.PROCESSED_SIZE_IN_BYTES, Fields.PROCESSED_SIZE, getProcessedSize());
builder.field(Fields.START_TIME_IN_MILLIS, getStartTime());
builder.timeValueField(Fields.TIME_IN_MILLIS, Fields.TIME, getTime());
builder.humanReadableField(Fields.TIME_IN_MILLIS, Fields.TIME, new TimeValue(getTime()));
builder.endObject();
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ static final class Fields {
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params)
throws IOException {
builder.timeValueField(Fields.MAX_UPTIME_IN_MILLIS, Fields.MAX_UPTIME, maxUptime);
builder.humanReadableField(Fields.MAX_UPTIME_IN_MILLIS, Fields.MAX_UPTIME, new TimeValue(maxUptime));
builder.startArray(Fields.VERSIONS);
for (ObjectIntCursor<JvmVersion> v : versions) {
builder.startObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ static void toXContent(XContentBuilder builder, Sort sort) throws IOException {
static void toXContent(XContentBuilder builder, Accountable tree) throws IOException {
builder.startObject();
builder.field(Fields.DESCRIPTION, tree.toString());
builder.byteSizeField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(tree.ramBytesUsed()));
builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(tree.ramBytesUsed()));
Collection<Accountable> children = tree.getChildResources();
if (children.isEmpty() == false) {
builder.startArray(Fields.CHILDREN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.snapshots.Snapshot;

Expand Down Expand Up @@ -145,7 +146,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
{
builder.field("repository", entry.snapshot.getRepository());
builder.field("snapshot", entry.snapshot.getSnapshotId().getName());
builder.timeValueField("start_time_millis", "start_time", entry.startTime);
builder.humanReadableField("start_time_millis", "start_time", new TimeValue(entry.startTime));
builder.field("repository_state_id", entry.repositoryStateId);
}
builder.endObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.shard.ShardId;
Expand Down Expand Up @@ -512,7 +513,7 @@ public void toXContent(Entry entry, XContentBuilder builder, ToXContent.Params p
}
}
builder.endArray();
builder.timeValueField(START_TIME_MILLIS, START_TIME, entry.startTime());
builder.humanReadableField(START_TIME_MILLIS, START_TIME, new TimeValue(entry.startTime()));
builder.field(REPOSITORY_STATE_ID, entry.getRepositoryStateId());
builder.startArray(SHARDS);
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field("allocation_id", allocationId);
}
if (allocationStatus == AllocationStatus.DELAYED_ALLOCATION) {
builder.timeValueField("configured_delay_in_millis", "configured_delay", TimeValue.timeValueMillis(configuredDelayInMillis));
builder.timeValueField("remaining_delay_in_millis", "remaining_delay", TimeValue.timeValueMillis(remainingDelayInMillis));
builder.humanReadableField("configured_delay_in_millis", "configured_delay",
TimeValue.timeValueMillis(configuredDelayInMillis));
builder.humanReadableField("remaining_delay_in_millis", "remaining_delay",
TimeValue.timeValueMillis(remainingDelayInMillis));
}
nodeDecisionsToXContent(nodeDecisions, builder, params);
return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -87,6 +88,7 @@ public static XContentBuilder builder(XContent xContent, Set<String> includes, S
public static final DateTimeFormatter DEFAULT_DATE_PRINTER = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);

private static final Map<Class<?>, Writer> WRITERS;
private static final Map<Class<?>, HumanReadableTransformer> HUMAN_READABLE_TRANSFORMERS;
static {
Map<Class<?>, Writer> writers = new HashMap<>();
writers.put(Boolean.class, (b, v) -> b.value((Boolean) v));
Expand All @@ -110,13 +112,28 @@ public static XContentBuilder builder(XContent xContent, Set<String> includes, S
writers.put(Text.class, (b, v) -> b.value((Text) v));

WRITERS = Collections.unmodifiableMap(writers);

Map<Class<?>, HumanReadableTransformer> humanReadableTransformer = new HashMap<>();
// These will be moved to a different class at a later time to decouple them from XContentBuilder
humanReadableTransformer.put(TimeValue.class, v -> ((TimeValue) v).millis());
humanReadableTransformer.put(ByteSizeValue.class, v -> ((ByteSizeValue) v).getBytes());

HUMAN_READABLE_TRANSFORMERS = Collections.unmodifiableMap(humanReadableTransformer);
}

@FunctionalInterface
private interface Writer {
interface Writer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need to be public right? Otherwise they are package private?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't planning on making them public until they were needed to be, but I can make them public now

void write(XContentBuilder builder, Object value) throws IOException;
}

/**
* Interface for transforming complex objects into their "raw" equivalents for human-readable fields
*/
@FunctionalInterface
interface HumanReadableTransformer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this should be an interface the thing implements rather than a transformer to look up? That feels a little more explicit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an interface we can't extend classes we don't control (Lucene or Joda ones)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we could add both if we wanted? I can do that in subsequent work if you think it'd be worth adding

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I don't think it is worth adding multiple ways to do this.

Object rawValue(Object value) throws IOException;
}

/**
* XContentGenerator used to build the XContent object
*/
Expand All @@ -132,6 +149,16 @@ private interface Writer {
*/
private boolean humanReadable = false;

/**
* Additional pluggable writers for classes outside of XContent's control
*/
private final Map<Class<?>, Writer> additionalWriters;

/**
* Additional transformers for human readable fields
*/
private final Map<Class<?>, HumanReadableTransformer> additionalHumanReadableTransformers;

/**
* Constructs a new builder using the provided XContent and an OutputStream. Make sure
* to call {@link #close()} when the builder is done with.
Expand Down Expand Up @@ -164,6 +191,14 @@ public XContentBuilder(XContent xContent, OutputStream bos, Set<String> includes
public XContentBuilder(XContent xContent, OutputStream os, Set<String> includes, Set<String> excludes) throws IOException {
this.bos = os;
this.generator = xContent.createGenerator(bos, includes, excludes);
Map<Class<?>, Writer> writers = new HashMap<>();
Map<Class<?>, HumanReadableTransformer> transformers = new HashMap<>();
for (XContentBuilderProvider service : ServiceLoader.load(XContentBuilderProvider.class)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do the service loading in a static block? Or during startup? It feels like we could still have one map if this were static. It wouldn't work with plugins but I think it'd work fine otherwise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should definitely be static. Calling ServiceLoader.load is not "cheap" (must iterate classloaders/resources). The writers and transformers should be static members, and initialized in a static block which calls ServiceLoader. You can explicitly add writers and transformers for the known types there, in addition to the ones coming from the extensions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll move this to a static block for now, and move to passing it in at in a different place when the separation is made.

writers.putAll(service.getXContentWriters());
transformers.putAll(service.getXContentHumanReadableTransformers());
}
additionalWriters = Collections.unmodifiableMap(writers);
additionalHumanReadableTransformers = Collections.unmodifiableMap(transformers);
}

public XContentType contentType() {
Expand Down Expand Up @@ -798,6 +833,13 @@ private void unknownValue(Object value, boolean ensureNoSelfReferences) throws I
value((ReadableInstant) value);
} else if (value instanceof ToXContent) {
value((ToXContent) value);
} else if (additionalWriters.containsKey(value.getClass())) {
Writer additionalWriter = additionalWriters.get(value.getClass());
if (additionalWriter == null) {
throw new NullPointerException("xcontent writer for class " + value.getClass() + " cannot be null");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use Objects.requireNonNull(additionalWriters.get(value.getClass()); above?

} else {
additionalWriter.write(this, value);
}
} else {
// This is a "value" object (like enum, DistanceUnit, etc) just toString() it
// (yes, it can be misleading when toString a Java class, but really, jackson should be used in that case)
Expand Down Expand Up @@ -891,33 +933,37 @@ private XContentBuilder value(Iterable<?> values, boolean ensureNoSelfReferences
}

////////////////////////////////////////////////////////////////////////////
// Misc.
// Human readable fields
//
// These are fields that have a "raw" value and a "human readable" value,
// such as time values or byte sizes. The human readable variant is only
// used if the humanReadable flag has been set
//////////////////////////////////

public XContentBuilder timeValueField(String rawFieldName, String readableFieldName, TimeValue timeValue) throws IOException {
public XContentBuilder humanReadableField(String rawFieldName, String readableFieldName, Object value) throws IOException {
if (humanReadable) {
field(readableFieldName, timeValue.toString());
field(readableFieldName, Objects.toString(value));
}
field(rawFieldName, timeValue.millis());
return this;
}

public XContentBuilder timeValueField(String rawFieldName, String readableFieldName, long rawTime) throws IOException {
if (humanReadable) {
field(readableFieldName, new TimeValue(rawTime).toString());
HumanReadableTransformer transformer = HUMAN_READABLE_TRANSFORMERS.get(value.getClass());
if (transformer != null) {
Object rawValue = transformer.rawValue(value);
field(rawFieldName, rawValue);
} else if (additionalHumanReadableTransformers.containsKey(value.getClass())) {
HumanReadableTransformer additionalTransformer = additionalHumanReadableTransformers.get(value.getClass());
if (additionalTransformer == null) {
throw new NullPointerException("xcontent transformer for class " + value.getClass() + " cannot be null");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, you can use requiresNonNull. But I think it would be better to validate these are non null when loading the extensions (ie statically) rather than at usage time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll validate these when loading rather then usage, good idea

} else {
field(rawFieldName, additionalTransformer.rawValue(value));
}
} else {
throw new IllegalArgumentException("no raw transformer found for class " + value.getClass());
}
field(rawFieldName, rawTime);
return this;
}

public XContentBuilder timeValueField(String rawFieldName, String readableFieldName, long rawTime, TimeUnit timeUnit) throws
IOException {
if (humanReadable) {
field(readableFieldName, new TimeValue(rawTime, timeUnit).toString());
}
field(rawFieldName, rawTime);
return this;
}
////////////////////////////////////////////////////////////////////////////
// Misc.
//////////////////////////////////


public XContentBuilder percentageField(String rawFieldName, String readableFieldName, double percentage) throws IOException {
Expand All @@ -928,14 +974,6 @@ public XContentBuilder percentageField(String rawFieldName, String readableField
return this;
}

public XContentBuilder byteSizeField(String rawFieldName, String readableFieldName, ByteSizeValue byteSizeValue) throws IOException {
if (humanReadable) {
field(readableFieldName, byteSizeValue.toString());
}
field(rawFieldName, byteSizeValue.getBytes());
return this;
}

public XContentBuilder byteSizeField(String rawFieldName, String readableFieldName, long rawSize) throws IOException {
if (humanReadable) {
field(readableFieldName, new ByteSizeValue(rawSize).toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 java.util.Map;

/**
* This interface provides a way for non-JDK classes to plug in a way to serialize to xcontent.
*
* It is <b>greatly</b> preferred that you implement {@link ToXContentFragment}
* in the class for encoding, however, in some situations you may not own the
* class, in which case you can add an implementation here for encoding it.
*/
public interface XContentBuilderProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been naming these class with the Extension suffix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll rename this


/**
* Used for plugging in a generic writer for a class, for example, an example implementation:
*
* <pre>
* Map<Class<?>, XContentBuilder.Writer> addlWriters = new HashMap<>();
* addlWriters.put(BytesRef.class, (builder, value) -> b.value(((BytesRef) value).utf8String()));
* return addlWriters;
* </pre>
*
* @return a map of class name to writer
*/
Map<Class<?>, XContentBuilder.Writer> getXContentWriters();

/**
* Used for plugging in a human readable version of a class's encoding. It is assumed that
* the human readable equivalent is <b>always</b> behind the {@code toString()} method, so
* this transformer returns the raw value to be used.
*
* An example implementation:
*
* <pre>
* Map<Class<?>, XContentBuilder.HumanReadableTransformer> transformers = new HashMap<>();
* transformers.put(ByteSizeValue.class, (value) -> ((ByteSizeValue) value).bytes());
* </pre>
* @return a map of class name to transformer used to retrieve raw value
*/
Map<Class<?>, XContentBuilder.HumanReadableTransformer> getXContentHumanReadableTransformers();
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public TimeValue getTotalTime() {
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(Fields.FLUSH);
builder.field(Fields.TOTAL, total);
builder.timeValueField(Fields.TOTAL_TIME_IN_MILLIS, Fields.TOTAL_TIME, totalTimeInMillis);
builder.humanReadableField(Fields.TOTAL_TIME_IN_MILLIS, Fields.TOTAL_TIME, getTotalTime());
builder.endObject();
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ public long current() {
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(Fields.GET);
builder.field(Fields.TOTAL, getCount());
builder.timeValueField(Fields.TIME_IN_MILLIS, Fields.TIME, getTimeInMillis());
builder.humanReadableField(Fields.TIME_IN_MILLIS, Fields.TIME, getTime());
builder.field(Fields.EXISTS_TOTAL, existsCount);
builder.timeValueField(Fields.EXISTS_TIME_IN_MILLIS, Fields.EXISTS_TIME, existsTimeInMillis);
builder.humanReadableField(Fields.EXISTS_TIME_IN_MILLIS, Fields.EXISTS_TIME, getExistsTime());
builder.field(Fields.MISSING_TOTAL, missingCount);
builder.timeValueField(Fields.MISSING_TIME_IN_MILLIS, Fields.MISSING_TIME, missingTimeInMillis);
builder.humanReadableField(Fields.MISSING_TIME_IN_MILLIS, Fields.MISSING_TIME, getMissingTime());
builder.field(Fields.CURRENT, current);
builder.endObject();
return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field(Fields.CURRENT_DOCS, currentNumDocs);
builder.byteSizeField(Fields.CURRENT_SIZE_IN_BYTES, Fields.CURRENT_SIZE, currentSizeInBytes);
builder.field(Fields.TOTAL, total);
builder.timeValueField(Fields.TOTAL_TIME_IN_MILLIS, Fields.TOTAL_TIME, totalTimeInMillis);
builder.humanReadableField(Fields.TOTAL_TIME_IN_MILLIS, Fields.TOTAL_TIME, getTotalTime());
builder.field(Fields.TOTAL_DOCS, totalNumDocs);
builder.byteSizeField(Fields.TOTAL_SIZE_IN_BYTES, Fields.TOTAL_SIZE, totalSizeInBytes);
builder.timeValueField(Fields.TOTAL_STOPPED_TIME_IN_MILLIS, Fields.TOTAL_STOPPED_TIME, totalStoppedTimeInMillis);
builder.timeValueField(Fields.TOTAL_THROTTLED_TIME_IN_MILLIS, Fields.TOTAL_THROTTLED_TIME, totalThrottledTimeInMillis);
builder.humanReadableField(Fields.TOTAL_STOPPED_TIME_IN_MILLIS, Fields.TOTAL_STOPPED_TIME, getTotalStoppedTime());
builder.humanReadableField(Fields.TOTAL_THROTTLED_TIME_IN_MILLIS, Fields.TOTAL_THROTTLED_TIME, getTotalThrottledTime());
builder.byteSizeField(Fields.TOTAL_THROTTLE_BYTES_PER_SEC_IN_BYTES, Fields.TOTAL_THROTTLE_BYTES_PER_SEC, totalBytesPerSecAutoThrottle);
builder.endObject();
return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.startObject(Fields.RECOVERY);
builder.field(Fields.CURRENT_AS_SOURCE, currentAsSource());
builder.field(Fields.CURRENT_AS_TARGET, currentAsTarget());
builder.timeValueField(Fields.THROTTLE_TIME_IN_MILLIS, Fields.THROTTLE_TIME, throttleTime());
builder.humanReadableField(Fields.THROTTLE_TIME_IN_MILLIS, Fields.THROTTLE_TIME, throttleTime());
builder.endObject();
return builder;
}
Expand Down
Loading