001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.master.http;
019
020import java.io.UnsupportedEncodingException;
021import java.net.URLDecoder;
022import java.net.URLEncoder;
023import java.nio.charset.StandardCharsets;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Iterator;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.stream.StreamSupport;
030import javax.servlet.http.HttpServletRequest;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.builder.ToStringBuilder;
033import org.apache.commons.lang3.builder.ToStringStyle;
034import org.apache.hadoop.hbase.CompareOperator;
035import org.apache.hadoop.hbase.HConstants;
036import org.apache.hadoop.hbase.TableName;
037import org.apache.hadoop.hbase.client.AdvancedScanResultConsumer;
038import org.apache.hadoop.hbase.client.AsyncConnection;
039import org.apache.hadoop.hbase.client.AsyncTable;
040import org.apache.hadoop.hbase.client.ResultScanner;
041import org.apache.hadoop.hbase.client.Scan;
042import org.apache.hadoop.hbase.filter.Filter;
043import org.apache.hadoop.hbase.filter.FilterList;
044import org.apache.hadoop.hbase.filter.PrefixFilter;
045import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
046import org.apache.hadoop.hbase.master.RegionState;
047import org.apache.hadoop.hbase.util.Bytes;
048import org.apache.yetus.audience.InterfaceAudience;
049
050import org.apache.hbase.thirdparty.com.google.common.collect.Iterators;
051import org.apache.hbase.thirdparty.io.netty.handler.codec.http.QueryStringEncoder;
052
053/**
054 * <p>
055 * Support class for the "Meta Entries" section in {@code resources/hbase-webapps/master/table.jsp}.
056 * </p>
057 * <p>
058 * <b>Interface</b>. This class's intended consumer is {@code table.jsp}. As such, it's primary
059 * interface is the active {@link HttpServletRequest}, from which it uses the {@code scan_*} request
060 * parameters. This class supports paging through an optionally filtered view of the contents of
061 * {@code hbase:meta}. Those filters and the pagination offset are specified via these request
062 * parameters. It provides helper methods for constructing pagination links.
063 * <ul>
064 * <li>{@value #NAME_PARAM} - the name of the table requested. The only table of our concern here is
065 * {@code hbase:meta}; any other value is effectively ignored by the giant conditional in the
066 * jsp.</li>
067 * <li>{@value #SCAN_LIMIT_PARAM} - specifies a limit on the number of region (replicas) rendered on
068 * the by the table in a single request -- a limit on page size. This corresponds to the number of
069 * {@link RegionReplicaInfo} objects produced by {@link Results#iterator()}. When a value for
070 * {@code scan_limit} is invalid or not specified, the default value of {@value #SCAN_LIMIT_DEFAULT}
071 * is used. In order to avoid excessive resource consumption, a maximum value of
072 * {@value #SCAN_LIMIT_MAX} is enforced.</li>
073 * <li>{@value #SCAN_REGION_STATE_PARAM} - an optional filter on {@link RegionState}.</li>
074 * <li>{@value #SCAN_START_PARAM} - specifies the rowkey at which a scan should start. For usage
075 * details, see the below section on <b>Pagination</b>.</li>
076 * <li>{@value #SCAN_TABLE_PARAM} - specifies a filter on the values returned, limiting them to
077 * regions from a specified table. This parameter is implemented as a prefix filter on the
078 * {@link Scan}, so in effect it can be used for simple namespace and multi-table matches.</li>
079 * </ul>
080 * </p>
081 * <p>
082 * <b>Pagination</b>. A single page of results are made available via {@link #getResults()} / an
083 * instance of {@link Results}. Callers use its {@link Iterator} consume the page of
084 * {@link RegionReplicaInfo} instances, each of which represents a region or region replica. Helper
085 * methods are provided for building page navigation controls preserving the user's selected filter
086 * set: {@link #buildFirstPageUrl()}, {@link #buildNextPageUrl(byte[])}. Pagination is implemented
087 * using a simple offset + limit system. Offset is provided by the {@value #SCAN_START_PARAM}, limit
088 * via {@value #SCAN_LIMIT_PARAM}. Under the hood, the {@link Scan} is constructed with
089 * {@link Scan#setMaxResultSize(long)} set to ({@value SCAN_LIMIT_PARAM} +1), while the
090 * {@link Results} {@link Iterator} honors {@value #SCAN_LIMIT_PARAM}. The +1 allows the caller to
091 * know if a "next page" is available via {@link Results#hasMoreResults()}. Note that this
092 * pagination strategy is incomplete when it comes to region replicas and can potentially omit
093 * rendering replicas that fall between the last rowkey offset and {@code replicaCount % page size}.
094 * </p>
095 * <p>
096 * <b>Error Messages</b>. Any time there's an error parsing user input, a message will be populated
097 * in {@link #getErrorMessages()}. Any fields which produce an error will have their filter values
098 * set to the default, except for a value of {@value #SCAN_LIMIT_PARAM} that exceeds
099 * {@value #SCAN_LIMIT_MAX}, in which case {@value #SCAN_LIMIT_MAX} is used.
100 * </p>
101 */
102@InterfaceAudience.Private
103public class MetaBrowser {
104  public static final String NAME_PARAM = "name";
105  public static final String SCAN_LIMIT_PARAM = "scan_limit";
106  public static final String SCAN_REGION_STATE_PARAM = "scan_region_state";
107  public static final String SCAN_START_PARAM = "scan_start";
108  public static final String SCAN_TABLE_PARAM = "scan_table";
109
110  public static final int SCAN_LIMIT_DEFAULT = 10;
111  public static final int SCAN_LIMIT_MAX = 10_000;
112
113  private final AsyncConnection connection;
114  private final HttpServletRequest request;
115  private final List<String> errorMessages;
116  private final String name;
117  private final Integer scanLimit;
118  private final RegionState.State scanRegionState;
119  private final byte[] scanStart;
120  private final TableName scanTable;
121
122  public MetaBrowser(final AsyncConnection connection, final HttpServletRequest request) {
123    this.connection = connection;
124    this.request = request;
125    this.errorMessages = new LinkedList<>();
126    this.name = resolveName(request);
127    this.scanLimit = resolveScanLimit(request);
128    this.scanRegionState = resolveScanRegionState(request);
129    this.scanStart = resolveScanStart(request);
130    this.scanTable = resolveScanTable(request);
131  }
132
133  public List<String> getErrorMessages() {
134    return errorMessages;
135  }
136
137  public String getName() {
138    return name;
139  }
140
141  public Integer getScanLimit() {
142    return scanLimit;
143  }
144
145  public byte[] getScanStart() {
146    return scanStart;
147  }
148
149  public RegionState.State getScanRegionState() {
150    return scanRegionState;
151  }
152
153  public TableName getScanTable() {
154    return scanTable;
155  }
156
157  public Results getResults() {
158    final AsyncTable<AdvancedScanResultConsumer> asyncTable =
159      connection.getTable(TableName.META_TABLE_NAME);
160    return new Results(asyncTable.getScanner(buildScan()));
161  }
162
163  @Override
164  public String toString() {
165    return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
166      .append("scanStart", scanStart).append("scanLimit", scanLimit).append("scanTable", scanTable)
167      .append("scanRegionState", scanRegionState).toString();
168  }
169
170  private static String resolveName(final HttpServletRequest request) {
171    return resolveRequestParameter(request, NAME_PARAM);
172  }
173
174  private Integer resolveScanLimit(final HttpServletRequest request) {
175    final String requestValueStr = resolveRequestParameter(request, SCAN_LIMIT_PARAM);
176    if (StringUtils.isBlank(requestValueStr)) {
177      return null;
178    }
179
180    final Integer requestValue = tryParseInt(requestValueStr);
181    if (requestValue == null) {
182      errorMessages.add(buildScanLimitMalformedErrorMessage(requestValueStr));
183      return null;
184    }
185    if (requestValue <= 0) {
186      errorMessages.add(buildScanLimitLTEQZero(requestValue));
187      return SCAN_LIMIT_DEFAULT;
188    }
189
190    final int truncatedValue = Math.min(requestValue, SCAN_LIMIT_MAX);
191    if (requestValue != truncatedValue) {
192      errorMessages.add(buildScanLimitExceededErrorMessage(requestValue));
193    }
194    return truncatedValue;
195  }
196
197  private RegionState.State resolveScanRegionState(final HttpServletRequest request) {
198    final String requestValueStr = resolveRequestParameter(request, SCAN_REGION_STATE_PARAM);
199    if (requestValueStr == null) {
200      return null;
201    }
202    final RegionState.State requestValue = tryValueOf(RegionState.State.class, requestValueStr);
203    if (requestValue == null) {
204      errorMessages.add(buildScanRegionStateMalformedErrorMessage(requestValueStr));
205      return null;
206    }
207    return requestValue;
208  }
209
210  private static byte[] resolveScanStart(final HttpServletRequest request) {
211    // TODO: handle replicas that fall between the last rowkey and pagination limit.
212    final String requestValue = resolveRequestParameter(request, SCAN_START_PARAM);
213    if (requestValue == null) {
214      return null;
215    }
216    return Bytes.toBytesBinary(requestValue);
217  }
218
219  private static TableName resolveScanTable(final HttpServletRequest request) {
220    final String requestValue = resolveRequestParameter(request, SCAN_TABLE_PARAM);
221    if (requestValue == null) {
222      return null;
223    }
224    return TableName.valueOf(requestValue);
225  }
226
227  private static String resolveRequestParameter(final HttpServletRequest request,
228    final String param) {
229    if (request == null) {
230      return null;
231    }
232    final String requestValueStrEnc = request.getParameter(param);
233    if (StringUtils.isBlank(requestValueStrEnc)) {
234      return null;
235    }
236    return urlDecode(requestValueStrEnc);
237  }
238
239  private static Filter buildTableFilter(final TableName tableName) {
240    return new PrefixFilter(tableName.toBytes());
241  }
242
243  private static Filter buildScanRegionStateFilter(final RegionState.State state) {
244    return new SingleColumnValueFilter(HConstants.CATALOG_FAMILY, HConstants.STATE_QUALIFIER,
245      CompareOperator.EQUAL,
246      // use the same serialization strategy as found in MetaTableAccessor#addRegionStateToPut
247      Bytes.toBytes(state.name()));
248  }
249
250  private Filter buildScanFilter() {
251    if (scanTable == null && scanRegionState == null) {
252      return null;
253    }
254
255    final List<Filter> filters = new ArrayList<>(2);
256    if (scanTable != null) {
257      filters.add(buildTableFilter(scanTable));
258    }
259    if (scanRegionState != null) {
260      filters.add(buildScanRegionStateFilter(scanRegionState));
261    }
262    if (filters.size() == 1) {
263      return filters.get(0);
264    }
265    return new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);
266  }
267
268  private Scan buildScan() {
269    final Scan metaScan = new Scan().addFamily(HConstants.CATALOG_FAMILY).readVersions(1)
270      .setLimit((scanLimit != null ? scanLimit : SCAN_LIMIT_DEFAULT) + 1);
271    if (scanStart != null) {
272      metaScan.withStartRow(scanStart, false);
273    }
274    final Filter filter = buildScanFilter();
275    if (filter != null) {
276      metaScan.setFilter(filter);
277    }
278    return metaScan;
279  }
280
281  /**
282   * Adds {@code value} to {@code encoder} under {@code paramName} when {@code value} is non-null.
283   */
284  private void addParam(final QueryStringEncoder encoder, final String paramName,
285    final Object value) {
286    if (value != null) {
287      encoder.addParam(paramName, value.toString());
288    }
289  }
290
291  private QueryStringEncoder buildFirstPageEncoder() {
292    final QueryStringEncoder encoder = new QueryStringEncoder(request.getRequestURI());
293    addParam(encoder, NAME_PARAM, name);
294    addParam(encoder, SCAN_LIMIT_PARAM, scanLimit);
295    addParam(encoder, SCAN_REGION_STATE_PARAM, scanRegionState);
296    addParam(encoder, SCAN_TABLE_PARAM, scanTable);
297    return encoder;
298  }
299
300  public String buildFirstPageUrl() {
301    return buildFirstPageEncoder().toString();
302  }
303
304  static String buildStartParamFrom(final byte[] lastRow) {
305    if (lastRow == null) {
306      return null;
307    }
308    return urlEncode(Bytes.toStringBinary(lastRow));
309  }
310
311  public String buildNextPageUrl(final byte[] lastRow) {
312    final QueryStringEncoder encoder = buildFirstPageEncoder();
313    final String startRow = buildStartParamFrom(lastRow);
314    addParam(encoder, SCAN_START_PARAM, startRow);
315    return encoder.toString();
316  }
317
318  private static String urlEncode(final String val) {
319    if (StringUtils.isEmpty(val)) {
320      return null;
321    }
322    try {
323      return URLEncoder.encode(val, StandardCharsets.UTF_8.toString());
324    } catch (UnsupportedEncodingException e) {
325      return null;
326    }
327  }
328
329  private static String urlDecode(final String val) {
330    if (StringUtils.isEmpty(val)) {
331      return null;
332    }
333    try {
334      return URLDecoder.decode(val, StandardCharsets.UTF_8.toString());
335    } catch (UnsupportedEncodingException e) {
336      return null;
337    }
338  }
339
340  private static Integer tryParseInt(final String val) {
341    if (StringUtils.isEmpty(val)) {
342      return null;
343    }
344    try {
345      return Integer.parseInt(val);
346    } catch (NumberFormatException e) {
347      return null;
348    }
349  }
350
351  private static <T extends Enum<T>> T tryValueOf(final Class<T> clazz, final String value) {
352    if (clazz == null || value == null) {
353      return null;
354    }
355    try {
356      return Enum.valueOf(clazz, value);
357    } catch (IllegalArgumentException e) {
358      return null;
359    }
360  }
361
362  private static String buildScanLimitExceededErrorMessage(final int requestValue) {
363    return String.format("Requested SCAN_LIMIT value %d exceeds maximum value %d.", requestValue,
364      SCAN_LIMIT_MAX);
365  }
366
367  private static String buildScanLimitMalformedErrorMessage(final String requestValue) {
368    return String.format("Requested SCAN_LIMIT value '%s' cannot be parsed as an integer.",
369      requestValue);
370  }
371
372  private static String buildScanLimitLTEQZero(final int requestValue) {
373    return String.format("Requested SCAN_LIMIT value %d is <= 0.", requestValue);
374  }
375
376  private static String buildScanRegionStateMalformedErrorMessage(final String requestValue) {
377    return String.format(
378      "Requested SCAN_REGION_STATE value '%s' cannot be parsed as a RegionState.", requestValue);
379  }
380
381  /**
382   * Encapsulates the results produced by this {@link MetaBrowser} instance.
383   */
384  public final class Results implements AutoCloseable, Iterable<RegionReplicaInfo> {
385
386    private final ResultScanner resultScanner;
387    private final Iterator<RegionReplicaInfo> sourceIterator;
388
389    private Results(final ResultScanner resultScanner) {
390      this.resultScanner = resultScanner;
391      this.sourceIterator = StreamSupport.stream(resultScanner.spliterator(), false)
392        .map(RegionReplicaInfo::from).flatMap(Collection::stream).iterator();
393    }
394
395    /**
396     * @return {@code true} when the underlying {@link ResultScanner} is not yet exhausted,
397     *         {@code false} otherwise.
398     */
399    public boolean hasMoreResults() {
400      return sourceIterator.hasNext();
401    }
402
403    @Override
404    public void close() {
405      if (resultScanner != null) {
406        resultScanner.close();
407      }
408    }
409
410    @Override
411    public Iterator<RegionReplicaInfo> iterator() {
412      return Iterators.limit(sourceIterator, scanLimit != null ? scanLimit : SCAN_LIMIT_DEFAULT);
413    }
414  }
415}