AbstractRestChannel.java 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /*
  2. * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  3. * or more contributor license agreements. Licensed under the Elastic License
  4. * 2.0 and the Server Side Public License, v 1; you may not use this file except
  5. * in compliance with, at your election, the Elastic License 2.0 or the Server
  6. * Side Public License, v 1.
  7. */
  8. package org.elasticsearch.rest;
  9. import org.elasticsearch.common.Nullable;
  10. import org.elasticsearch.common.Strings;
  11. import org.elasticsearch.common.io.Streams;
  12. import org.elasticsearch.common.io.stream.BytesStreamOutput;
  13. import org.elasticsearch.common.xcontent.ParsedMediaType;
  14. import org.elasticsearch.common.xcontent.XContentBuilder;
  15. import org.elasticsearch.common.xcontent.XContentFactory;
  16. import org.elasticsearch.common.xcontent.XContentType;
  17. import java.io.IOException;
  18. import java.io.OutputStream;
  19. import java.util.Collections;
  20. import java.util.Map;
  21. import java.util.Set;
  22. import java.util.function.Predicate;
  23. import static java.util.stream.Collectors.toSet;
  24. public abstract class AbstractRestChannel implements RestChannel {
  25. private static final Predicate<String> INCLUDE_FILTER = f -> f.charAt(0) != '-';
  26. private static final Predicate<String> EXCLUDE_FILTER = INCLUDE_FILTER.negate();
  27. protected final RestRequest request;
  28. private final boolean detailedErrorsEnabled;
  29. private final String format;
  30. private final String filterPath;
  31. private final boolean pretty;
  32. private final boolean human;
  33. private final String acceptHeader;
  34. private BytesStreamOutput bytesOut;
  35. /**
  36. * Construct a channel for handling the request.
  37. *
  38. * @param request the request
  39. * @param detailedErrorsEnabled if detailed errors should be reported to the channel
  40. * @throws IllegalArgumentException if parsing the pretty or human parameters fails
  41. */
  42. protected AbstractRestChannel(RestRequest request, boolean detailedErrorsEnabled) {
  43. this.request = request;
  44. this.detailedErrorsEnabled = detailedErrorsEnabled;
  45. this.format = request.param("format");
  46. this.acceptHeader = request.header("Accept");
  47. this.filterPath = request.param("filter_path", null);
  48. this.pretty = request.paramAsBoolean("pretty", false);
  49. this.human = request.paramAsBoolean("human", false);
  50. }
  51. @Override
  52. public XContentBuilder newBuilder() throws IOException {
  53. return newBuilder(request.getXContentType(), true);
  54. }
  55. @Override
  56. public XContentBuilder newErrorBuilder() throws IOException {
  57. // Disable filtering when building error responses
  58. return newBuilder(request.getXContentType(), false);
  59. }
  60. /**
  61. * Creates a new {@link XContentBuilder} for a response to be sent using this channel. The builder's type is determined by the following
  62. * logic. If the request has a format parameter that will be used to attempt to map to an {@link XContentType}. If there is no format
  63. * parameter, the HTTP Accept header is checked to see if it can be matched to a {@link XContentType}. If this first attempt to map
  64. * fails, the request content type will be used if the value is not {@code null}; if the value is {@code null} the output format falls
  65. * back to JSON.
  66. */
  67. @Override
  68. public XContentBuilder newBuilder(@Nullable XContentType requestContentType, boolean useFiltering) throws IOException {
  69. return newBuilder(requestContentType, null, useFiltering);
  70. }
  71. /**
  72. * Creates a new {@link XContentBuilder} for a response to be sent using this channel. The builder's type can be sent as a parameter,
  73. * through {@code responseContentType} or it can fallback to {@link #newBuilder(XContentType, boolean)} logic if the sent type value
  74. * is {@code null}.
  75. */
  76. @Override
  77. public XContentBuilder newBuilder(@Nullable XContentType requestContentType, @Nullable XContentType responseContentType,
  78. boolean useFiltering) throws IOException {
  79. if (responseContentType == null) {
  80. if (Strings.hasText(format)) {
  81. responseContentType = XContentType.fromFormat(format);
  82. }
  83. if (responseContentType == null && Strings.hasText(acceptHeader)) {
  84. responseContentType = XContentType.fromMediaType(acceptHeader);
  85. }
  86. }
  87. // try to determine the response content type from the media type or the format query string parameter, with the format parameter
  88. // taking precedence over the Accept header
  89. if (responseContentType == null) {
  90. if (requestContentType != null) {
  91. // if there was a parsed content-type for the incoming request use that since no format was specified using the query
  92. // string parameter or the HTTP Accept header
  93. responseContentType = requestContentType;
  94. } else {
  95. // default to JSON output when all else fails
  96. responseContentType = XContentType.JSON;
  97. }
  98. }
  99. Set<String> includes = Collections.emptySet();
  100. Set<String> excludes = Collections.emptySet();
  101. if (useFiltering) {
  102. Set<String> filters = Strings.tokenizeByCommaToSet(filterPath);
  103. includes = filters.stream().filter(INCLUDE_FILTER).collect(toSet());
  104. excludes = filters.stream().filter(EXCLUDE_FILTER).map(f -> f.substring(1)).collect(toSet());
  105. }
  106. OutputStream unclosableOutputStream = Streams.flushOnCloseStream(bytesOutput());
  107. Map<String, String> parameters = request.getParsedAccept() != null ?
  108. request.getParsedAccept().getParameters() : Collections.emptyMap();
  109. ParsedMediaType responseMediaType = ParsedMediaType.parseMediaType(responseContentType, parameters);
  110. XContentBuilder builder =
  111. new XContentBuilder(XContentFactory.xContent(responseContentType), unclosableOutputStream,
  112. includes, excludes, responseMediaType, request.getRestApiVersion());
  113. if (pretty) {
  114. builder.prettyPrint().lfAtEnd();
  115. }
  116. builder.humanReadable(human);
  117. return builder;
  118. }
  119. /**
  120. * A channel level bytes output that can be reused. The bytes output is lazily instantiated
  121. * by a call to {@link #newBytesOutput()}. Once the stream is created, it gets reset on each
  122. * call to this method.
  123. */
  124. @Override
  125. public final BytesStreamOutput bytesOutput() {
  126. if (bytesOut == null) {
  127. bytesOut = newBytesOutput();
  128. } else {
  129. bytesOut.reset();
  130. }
  131. return bytesOut;
  132. }
  133. /**
  134. * An accessor to the raw value of the channel bytes output. This method will not instantiate
  135. * a new stream if one does not exist and this method will not reset the stream.
  136. */
  137. protected final BytesStreamOutput bytesOutputOrNull() {
  138. return bytesOut;
  139. }
  140. protected BytesStreamOutput newBytesOutput() {
  141. return new BytesStreamOutput();
  142. }
  143. @Override
  144. public RestRequest request() {
  145. return this.request;
  146. }
  147. @Override
  148. public boolean detailedErrorsEnabled() {
  149. return detailedErrorsEnabled;
  150. }
  151. }