/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.apache.hbase;

import static org.apache.hadoop.hbase.HConstants.CATALOG_FAMILY;
import static org.apache.hadoop.hbase.HConstants.REGIONINFO_QUALIFIER;
import static org.apache.hadoop.hbase.HConstants.TABLE_FAMILY;
import static org.apache.hadoop.hbase.HConstants.TABLE_STATE_QUALIFIER;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.SortedMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellBuilder;
import org.apache.hadoop.hbase.CellBuilderFactory;
import org.apache.hadoop.hbase.CellBuilderType;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.RegionLocations;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.RegionInfoBuilder;
import org.apache.hadoop.hbase.client.RegionLocator;
import org.apache.hadoop.hbase.client.RegionReplicaUtil;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.client.TableState;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.master.RegionState;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.hbase.util.PairOfSameType;

import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;

/**
 * hbck's local version of the MetaTableAccessor from the hbase repo
 * A Utility class to facilitate hbck2's access to Meta table.
 */
@InterfaceAudience.Private
public final class HBCKMetaTableAccessor {

  /** The delimiter for meta columns for replicaIds > 0 */
  static final char META_REPLICA_ID_DELIMITER = '_';

  /** A regex for parsing server columns from meta. See above javadoc for meta layout */
  private static final Pattern SERVER_COLUMN_PATTERN
    = Pattern.compile("^server(_[0-9a-fA-F]{4})?$");

  /**
   * Private constructor to keep this class from being instantiated.
   */
  private HBCKMetaTableAccessor() {
  }

  private static final Logger LOG = LoggerFactory.getLogger(
      HBCKMetaTableAccessor.class);

  // Constants
  private static final byte[] MERGE_QUALIFIER_PREFIX = Bytes.toBytes("merge");

  public static List<RegionInfo> getMergeRegions(Cell[] cells) {
    if (cells == null) {
      return null;
    }
    List<RegionInfo> regionsToMerge = null;
    for (Cell cell : cells) {
      if (!isMergeQualifierPrefix(cell)) {
        continue;
      }
      // Ok. This cell is that of a info:merge* column.
      RegionInfo ri = RegionInfo
          .parseFromOrNull(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
      if (ri != null) {
        if (regionsToMerge == null) {
          regionsToMerge = new ArrayList<>();
        }
        regionsToMerge.add(ri);
      }
    }
    return regionsToMerge;
  }

  /**
   * Delete the passed <code>RegionInfo</code> from the <code>hbase:meta</code> table.
   *
   * @param connection connection we're using
   * @param regionInfo the regionInfo to delete from the meta table
   * @throws IOException if it's not able to delete the regionInfo
   */
  public static void deleteRegionInfo(Connection connection, RegionInfo regionInfo)
      throws IOException {
    Delete delete = new Delete(regionInfo.getRegionName());
    delete.addFamily(HConstants.CATALOG_FAMILY, HConstants.LATEST_TIMESTAMP);
    deleteFromMetaTable(connection, delete);
    LOG.info("Deleted {}", regionInfo.getRegionNameAsString());
  }

  // Private helper methods

  // COPIED from MetaTableAccessor.isMergeQualifierPrefix()
  private static boolean isMergeQualifierPrefix(Cell cell) {
    // Check to see if has family and that qualifier starts with the MERGE_QUALIFIER_PREFIX
    return CellUtil.matchingFamily(cell, HConstants.CATALOG_FAMILY) && qualifierStartsWith(cell,
        MERGE_QUALIFIER_PREFIX);
  }

  /**
   * Finds if the start of the qualifier part of the Cell matches 'startsWith'
   *
   * COPIED from PrivateCellUtil.qualifierStartsWith()
   * @param left       the cell with which we need to match the qualifier
   * @param startsWith the serialized keyvalue format byte[]
   * @return true if the qualifier have same staring characters, false otherwise
   */
  private static boolean qualifierStartsWith(final Cell left, final byte[] startsWith) {
    if (startsWith == null || startsWith.length == 0) {
      throw new IllegalArgumentException("Cannot pass an empty startsWith");
    }
    return Bytes
        .equals(left.getQualifierArray(), left.getQualifierOffset(), startsWith.length, startsWith,
            0, startsWith.length);
  }

  /**
   * Delete the passed <code>d</code> from the <code>hbase:meta</code> table.
   *
   * COPIED MetaTableAccessor.deleteFromMetaTable()
   * @param connection connection we're using
   * @param d          Delete to add to hbase:meta
   */
  private static void deleteFromMetaTable(final Connection connection, final Delete d)
      throws IOException {
    if (connection == null) {
      throw new NullPointerException("No connection");
    } else if (connection.isClosed()) {
      throw new IOException("connection is closed");
    }
    try (Table t = connection.getTable(TableName.META_TABLE_NAME)) {
      List<Delete> deletes = new ArrayList<>();
      deletes.add(d);
      LOG.debug("Add {} delete to meta table", deletes);
      t.delete(deletes);
    }
  }

  /**
   * Returns all regions in meta for the given table.
   * @param conn a valid, open connection.
   * @param table the table to list regions in meta.
   * @return a list of <code>RegionInfo</code> for all table regions present in meta.
   * @throws IOException on any issues related with scanning meta table
   */
  public static List<RegionInfo> getTableRegions(final Connection conn, final TableName table)
      throws IOException {
    final MetaScanner<RegionInfo> scanner = new MetaScanner<>();
    final String startRow = Bytes.toString(table.getName()) + ",,";
    final String stopRow = Bytes.toString(table.getName()) + " ,,";
    return scanner.scanMeta(conn,
      scan -> {
        scan.withStartRow(Bytes.toBytes(startRow));
        scan.withStopRow(Bytes.toBytes(stopRow));
      },
      r -> {
        Cell cell = r.getColumnLatestCell(CATALOG_FAMILY, REGIONINFO_QUALIFIER);
        if(cell != null) {
          RegionInfo info = RegionInfo
            .parseFromOrNull(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
          return info;
        }
        return null;
      });
  }

  /**
   * Returns a list of all tables having related entries on meta.
   * @param conn a valid, open connection.
   * @return a list of <code>TableName</code> for each table having at least one entry in meta.
   * @throws IOException on any issues related with scanning meta table
   */
  public static List<TableName> getTables(final Connection conn) throws IOException {
    MetaScanner<TableName> scanner = new MetaScanner<>();
    return scanner.scanMeta(conn,
      scan -> scan.addColumn(TABLE_FAMILY, TABLE_STATE_QUALIFIER),
      r -> {
        final byte[] rowBytes = r.getRow();
        String table = Bytes.toString(rowBytes);
        if(table.lastIndexOf(HConstants.DELIMITER)>0) {
          table = table.substring(0, table.lastIndexOf(HConstants.DELIMITER));
        }
        return TableName.valueOf(table);
      });
  }

  /**
   * Converts and adds the passed <code>RegionInfo</code> parameter into a valid 'info:regioninfo'
   * cell value in 'hbase:meta'.
   * @param conn a valid, open connection.
   * @param region the region to be inserted in meta.
   * @throws IOException on any issues related with scanning meta table
   */
  public static void addRegionToMeta(Connection conn, RegionInfo region) throws IOException {
    Put put = makePutFromRegionInfo(region,
      System.currentTimeMillis());
    addRegionStateToPut(put, RegionState.State.CLOSED);
    conn.getTable(TableName.META_TABLE_NAME).put(put);
  }

  /**
   * List all valid regions currently in META, excluding parent whoese been completely split.
   * @param conn a valid, open connection.
   * @return a list of all regions in META, excluding split parents.
   * @throws IOException on any issues related with scanning meta table
   */
  public static List<RegionInfo> getAllRegions(Connection conn) throws IOException {
    MetaScanner<RegionInfo> scanner = new MetaScanner<>();
    return scanner.scanMeta(conn,
      scan -> scan.addColumn(HConstants.CATALOG_FAMILY, HConstants.STATE_QUALIFIER),
      r -> {
        Cell cell = r.getColumnLatestCell(HConstants.CATALOG_FAMILY,
          HConstants.REGIONINFO_QUALIFIER);
        RegionInfo info = RegionInfo.parseFromOrNull(cell.getValueArray(),
          cell.getValueOffset(), cell.getValueLength());
        return info.isSplit() ? null : info;
      });
  }

  /**
   * Scans all "table:state" cell values existing in meta and returns as a map of
   * <code>TableName</code> as key and <code>TableState</code> as the value.
   * @param conn a valid, open connection.
   * @return a Map of <code>TableName</code> as key and <code>TableState</code> as the value.
   * @throws IOException on any issues related with scanning meta table
   */
  public static Map<TableName, TableState> getAllTablesStates(Connection conn) throws IOException {
    final MetaScanner<Pair<TableName, TableState>> scanner = new MetaScanner<>();
    final Map<TableName, TableState> resultMap = new HashMap<>();
    scanner.scanMeta(conn,
      scan -> scan.addColumn(TABLE_FAMILY, TABLE_STATE_QUALIFIER),
      r -> {
        try {
          TableState state = getTableState(r);
          TableName name = TableName.valueOf(r.getRow());
          resultMap.put(name, state);
        } catch (IOException e) {
          LOG.error(e.getMessage());
        }
        return null;
      });
    return resultMap;
  }


  /**
   * (Copied partially from MetaTableAccessor)
   * @param tableName table we're working with
   * @return start row for scanning META according to query type
   */
  public static byte[] getTableStartRowForMeta(TableName tableName) {
    if (tableName == null) {
      return null;
    }
    byte[] startRow = new byte[tableName.getName().length + 2];
    System.arraycopy(tableName.getName(), 0, startRow, 0,
      tableName.getName().length);
    startRow[startRow.length - 2] = HConstants.DELIMITER;
    startRow[startRow.length - 1] = HConstants.DELIMITER;
    return startRow;
  }

  /**
   * @param tableName table we're working with
   * @return stop row for scanning META according to query type
   */
  public static byte[] getTableStopRowForMeta(TableName tableName) {
    if (tableName == null) {
      return null;
    }
    final byte[] stopRow;
    stopRow = new byte[tableName.getName().length + 3];
    System.arraycopy(tableName.getName(), 0, stopRow, 0,
      tableName.getName().length);
    stopRow[stopRow.length - 3] = ' ';
    stopRow[stopRow.length - 2] = HConstants.DELIMITER;
    stopRow[stopRow.length - 1] = HConstants.DELIMITER;
    return stopRow;
  }

  /** Returns the row key to use for this regionInfo */
  public static byte[] getMetaKeyForRegion(RegionInfo regionInfo) {
    if (regionInfo.isMetaRegion()) {
      return RegionInfoBuilder.newBuilder(regionInfo.getTable())
        .setRegionId(regionInfo.getRegionId())
        .setReplicaId(0)
        .setOffline(regionInfo.isOffline())
        .build().getRegionName();
    } else {
      return RegionInfoBuilder.newBuilder(regionInfo.getTable())
        .setStartKey(regionInfo.getStartKey())
        .setEndKey(regionInfo.getEndKey())
        .setSplit(regionInfo.isSplit())
        .setRegionId(regionInfo.getRegionId())
        .setReplicaId(0)
        .setOffline(regionInfo.isOffline())
        .build().getRegionName();
    }
  }

  public static Put addLocation(Put p, ServerName sn, long openSeqNum, int replicaId)
    throws IOException {
    CellBuilder builder = CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY);
    return p.add(builder.clear()
      .setRow(p.getRow())
      .setFamily(CATALOG_FAMILY)
      .setQualifier(getServerColumn(replicaId))
      .setTimestamp(p.getTimestamp())
      .setType(Cell.Type.Put)
      .setValue(Bytes.toBytes(sn.getAddress().toString()))
      .build())
      .add(builder.clear()
        .setRow(p.getRow())
        .setFamily(CATALOG_FAMILY)
        .setQualifier(getStartCodeColumn(replicaId))
        .setTimestamp(p.getTimestamp())
        .setType(Cell.Type.Put)
        .setValue(Bytes.toBytes(sn.getStartcode()))
        .build())
      .add(builder.clear()
        .setRow(p.getRow())
        .setFamily(CATALOG_FAMILY)
        .setQualifier(getSeqNumColumn(replicaId))
        .setTimestamp(p.getTimestamp())
        .setType(Cell.Type.Put)
        .setValue(Bytes.toBytes(openSeqNum))
        .build());
  }

  /**
   * Count regions in <code>hbase:meta</code> for passed table.
   * @param connection Connection object
   * @param tableName table name to count regions for
   * @return Count or regions in table <code>tableName</code>
   */
  public static int getRegionCount(final Connection connection, final TableName tableName)
    throws IOException {
    try (RegionLocator locator = connection.getRegionLocator(tableName)) {
      List<HRegionLocation> locations = locator.getAllRegionLocations();
      return locations == null ? 0 : locations.size();
    }
  }

  /**
   * Decode table state from META Result.
   * Should contain cell from HConstants.TABLE_FAMILY
   * (Copied from MetaTableAccessor)
   * @return null if not found
   */
  public static TableState getTableState(Result r) throws IOException {
    Cell cell = r.getColumnLatestCell(TABLE_FAMILY, TABLE_STATE_QUALIFIER);
    if (cell == null) {
      return null;
    }
    try {
      return TableState.parseFrom(TableName.valueOf(r.getRow()),
        Arrays.copyOfRange(cell.getValueArray(), cell.getValueOffset(),
          cell.getValueOffset() + cell.getValueLength()));
    } catch (DeserializationException e) {
      throw new IOException(e);
    }
  }

  /**
   * Fetch table state for given table from META table
   * @param conn connection to use
   * @param tableName table to fetch state for
   */
  public static TableState getTableState(Connection conn, TableName tableName)
    throws IOException {
    if (tableName.equals(TableName.META_TABLE_NAME)) {
      return new TableState(tableName, TableState.State.ENABLED);
    }
    Table metaHTable = conn.getTable(TableName.META_TABLE_NAME);
    Get get = new Get(tableName.getName()).addColumn(TABLE_FAMILY, TABLE_STATE_QUALIFIER);
    Result result = metaHTable.get(get);
    return getTableState(result);
  }

  /**
   * Construct PUT for given state
   * @param state new state
   */
  public static Put makePutFromTableState(TableState state, long ts) {
    Put put = new Put(state.getTableName().getName(), ts);
    put.addColumn(TABLE_FAMILY, TABLE_STATE_QUALIFIER,
      state.convert().toByteArray());
    return put;
  }

  /**
   * Generates and returns a Put containing the region into for the catalog table
   */
  public static Put makePutFromRegionInfo(RegionInfo region, long ts) throws IOException {
    Put put = new Put(region.getRegionName(), ts);
    //copied from MetaTableAccessor
    put.add(CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY)
      .setRow(put.getRow())
      .setFamily(HConstants.CATALOG_FAMILY)
      .setQualifier(HConstants.REGIONINFO_QUALIFIER)
      .setTimestamp(put.getTimestamp())
      .setType(Cell.Type.Put)
      // Serialize the Default Replica HRI otherwise scan of hbase:meta
      // shows an info:regioninfo value with encoded name and region
      // name that differs from that of the hbase;meta row.
      .setValue(RegionInfo.toByteArray(RegionReplicaUtil.getRegionInfoForDefaultReplica(region)))
      .build());
    return put;
  }

  private static void addRegionStateToPut(Put put, RegionState.State state) throws IOException {
    put.add(CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY)
      .setRow(put.getRow())
      .setFamily(HConstants.CATALOG_FAMILY)
      .setQualifier(HConstants.STATE_QUALIFIER)
      .setTimestamp(put.getTimestamp())
      .setType(Cell.Type.Put)
      .setValue(Bytes.toBytes(state.name()))
      .build());
  }

  /**
   * Remove state for table from meta
   * (Copied from MetaTableAccessor)
   * @param connection to use for deletion
   * @param table to delete state for
   */
  public static void deleteTableState(Connection connection, TableName table)
    throws IOException {
    long time = EnvironmentEdgeManager.currentTime();
    Delete delete = new Delete(table.getName());
    delete.addColumns(TABLE_FAMILY, HConstants.TABLE_STATE_QUALIFIER, time);
    deleteFromMetaTable(connection, delete);
    LOG.info("Deleted table " + table + " state from META");
  }

  /**
   * Updates state in META
   * @param conn connection to use
   * @param tableName table to look for
   */
  public static void updateTableState(Connection conn, TableName tableName,
    TableState.State actual) throws IOException {
    Put put = makePutFromTableState(new TableState(tableName, actual),
      EnvironmentEdgeManager.currentTime());
    conn.getTable(TableName.META_TABLE_NAME).put(put);
  }

  /**
   * Returns the RegionInfo object from the column {@link HConstants#CATALOG_FAMILY} and
   * <code>qualifier</code> of the catalog table result.
   * (Copied from MetaTableAccessor)
   * @param r a Result object from the catalog table scan
   * @param qualifier Column family qualifier
   * @return An RegionInfo instance or null.
   */
  public static RegionInfo getRegionInfo(final Result r, byte [] qualifier) {
    Cell cell = r.getColumnLatestCell(CATALOG_FAMILY, qualifier);
    if (cell == null) {
      return null;
    }
    return RegionInfo.parseFromOrNull(cell.getValueArray(),
      cell.getValueOffset(), cell.getValueLength());
  }

  /**
   * Returns the HRegionLocation parsed from the given meta row Result
   * for the given regionInfo and replicaId. The regionInfo can be the default region info
   * for the replica.
   * (Copied from MetaTableAccessor)
   * @param r the meta row result
   * @param regionInfo RegionInfo for default replica
   * @param replicaId the replicaId for the HRegionLocation
   * @return HRegionLocation parsed from the given meta row Result for the given replicaId
   */
  private static HRegionLocation getRegionLocation(final Result r, final RegionInfo regionInfo,
    final int replicaId) {
    ServerName serverName = getServerName(r, replicaId);
    long seqNum = getSeqNumDuringOpen(r, replicaId);
    RegionInfo replicaInfo = RegionReplicaUtil.getRegionInfoForReplica(regionInfo, replicaId);
    return new HRegionLocation(replicaInfo, serverName, seqNum);
  }

  /**
   * The latest seqnum that the server writing to meta observed when opening the region.
   * E.g. the seqNum when the result of {@link #getServerName(Result, int)} was written.
   * (Copied from MetaTableAccessor)
   * @param r Result to pull the seqNum from
   * @return SeqNum, or HConstants.NO_SEQNUM if there's no value written.
   */
  private static long getSeqNumDuringOpen(final Result r, final int replicaId) {
    Cell cell = r.getColumnLatestCell(CATALOG_FAMILY, getSeqNumColumn(replicaId));
    if (cell == null || cell.getValueLength() == 0) {
      return HConstants.NO_SEQNUM;
    }
    return Bytes.toLong(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
  }

  /**
   * Returns a {@link ServerName} from catalog table {@link Result}.
   * (Copied from MetaTableAccessor)
   * @param r Result to pull from
   * @return A ServerName instance or null if necessary fields not found or empty.
   */
  @InterfaceAudience.Private // for use by HMaster#getTableRegionRow which is used for testing only
  public static ServerName getServerName(final Result r, final int replicaId) {
    byte[] serverColumn = getServerColumn(replicaId);
    Cell cell = r.getColumnLatestCell(CATALOG_FAMILY, serverColumn);
    if (cell == null || cell.getValueLength() == 0) {
      return null;
    }
    String hostAndPort = Bytes.toString(
      cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
    byte[] startcodeColumn = getStartCodeColumn(replicaId);
    cell = r.getColumnLatestCell(CATALOG_FAMILY, startcodeColumn);
    if (cell == null || cell.getValueLength() == 0) {
      return null;
    }
    try {
      return ServerName.valueOf(hostAndPort,
        Bytes.toLong(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength()));
    } catch (IllegalArgumentException e) {
      LOG.error("Ignoring invalid region for server " + hostAndPort + "; cell=" + cell, e);
      return null;
    }
  }

  /**
   * Returns an HRegionLocationList extracted from the result.
   * (Copied from MetaTableAccessor)
   * @return an HRegionLocationList containing all locations for the region range or null if
   *   we can't deserialize the result.
   */
  public static RegionLocations getRegionLocations(final Result r) {
    if (r == null) {
      return null;
    }
    RegionInfo regionInfo = getRegionInfo(r, REGIONINFO_QUALIFIER);
    if (regionInfo == null) {
      return null;
    }

    List<HRegionLocation> locations = new ArrayList<>(1);
    NavigableMap<byte[], NavigableMap<byte[],byte[]>> familyMap = r.getNoVersionMap();

    locations.add(getRegionLocation(r, regionInfo, 0));

    NavigableMap<byte[], byte[]> infoMap = familyMap.get(CATALOG_FAMILY);
    if (infoMap == null) {
      return new RegionLocations(locations);
    }

    // iterate until all serverName columns are seen
    int replicaId = 0;
    byte[] serverColumn = getServerColumn(replicaId);
    SortedMap<byte[], byte[]> serverMap;
    serverMap = infoMap.tailMap(serverColumn, false);

    if (serverMap.isEmpty()) {
      return new RegionLocations(locations);
    }

    for (Map.Entry<byte[], byte[]> entry : serverMap.entrySet()) {
      replicaId = parseReplicaIdFromServerColumn(entry.getKey());
      if (replicaId < 0) {
        break;
      }
      HRegionLocation location = getRegionLocation(r, regionInfo, replicaId);
      // In case the region replica is newly created, it's location might be null. We usually do not
      // have HRL's in RegionLocations object with null ServerName. They are handled as null HRLs.
      if (location.getServerName() == null) {
        locations.add(null);
      } else {
        locations.add(location);
      }
    }

    return new RegionLocations(locations);
  }

  /**
   * Returns the daughter regions by reading the corresponding columns of the catalog table
   * Result.
   * (Copied from MetaTableAccessor)
   * @param data a Result object from the catalog table scan
   * @return pair of RegionInfo or PairOfSameType(null, null) if region is not a split parent
   */
  public static PairOfSameType<RegionInfo> getDaughterRegions(Result data) {
    RegionInfo splitA = getRegionInfo(data, HConstants.SPLITA_QUALIFIER);
    RegionInfo splitB = getRegionInfo(data, HConstants.SPLITB_QUALIFIER);
    return new PairOfSameType<>(splitA, splitB);
  }

  /**
   * Returns the column qualifier for serialized region state
   * (Copied from MetaTableAccessor)
   * @param replicaId the replicaId of the region
   * @return a byte[] for sn column qualifier
   */
  @VisibleForTesting
  static byte[] getServerNameColumn(int replicaId) {
    return replicaId == 0 ? HConstants.SERVERNAME_QUALIFIER
      : Bytes.toBytes(HConstants.SERVERNAME_QUALIFIER_STR + META_REPLICA_ID_DELIMITER
      + String.format(RegionInfo.REPLICA_ID_FORMAT, replicaId));
  }

  /**
   * Returns the column qualifier for server column for replicaId
   * (Copied from MetaTableAccessor)
   * @param replicaId the replicaId of the region
   * @return a byte[] for server column qualifier
   */
  @VisibleForTesting
  public static byte[] getServerColumn(int replicaId) {
    return replicaId == 0
      ? HConstants.SERVER_QUALIFIER
      : Bytes.toBytes(HConstants.SERVER_QUALIFIER_STR + META_REPLICA_ID_DELIMITER
      + String.format(RegionInfo.REPLICA_ID_FORMAT, replicaId));
  }

  /**
   * Returns the column qualifier for server start code column for replicaId
   * (Copied from MetaTableAccessor)
   * @param replicaId the replicaId of the region
   * @return a byte[] for server start code column qualifier
   */
  @VisibleForTesting
  public static byte[] getStartCodeColumn(int replicaId) {
    return replicaId == 0
      ? HConstants.STARTCODE_QUALIFIER
      : Bytes.toBytes(HConstants.STARTCODE_QUALIFIER_STR + META_REPLICA_ID_DELIMITER
      + String.format(RegionInfo.REPLICA_ID_FORMAT, replicaId));
  }

  /**
   * Returns the column qualifier for seqNum column for replicaId
   * (Copied from MetaTableAccessor)
   * @param replicaId the replicaId of the region
   * @return a byte[] for seqNum column qualifier
   */
  @VisibleForTesting
  public static byte[] getSeqNumColumn(int replicaId) {
    return replicaId == 0
      ? HConstants.SEQNUM_QUALIFIER
      : Bytes.toBytes(HConstants.SEQNUM_QUALIFIER_STR + META_REPLICA_ID_DELIMITER
      + String.format(RegionInfo.REPLICA_ID_FORMAT, replicaId));
  }

  /**
   * Parses the replicaId from the server column qualifier. See top of the class javadoc
   * for the actual meta layout
   * (Copied from MetaTableAccessor)
   * @param serverColumn the column qualifier
   * @return an int for the replicaId
   */
  @VisibleForTesting
  static int parseReplicaIdFromServerColumn(byte[] serverColumn) {
    String serverStr = Bytes.toString(serverColumn);

    Matcher matcher = SERVER_COLUMN_PATTERN.matcher(serverStr);
    if (matcher.matches() && matcher.groupCount() > 0) {
      String group = matcher.group(1);
      if (group != null && group.length() > 0) {
        return Integer.parseInt(group.substring(1), 16);
      } else {
        return 0;
      }
    }
    return -1;
  }

  public static class MetaScanner<R> {

    public List<R> scanMeta(final Connection conn, Consumer<Scan> scanDecorator,
        Function<Result, R> scanProcessor) throws IOException {
      final Scan metaScan = new Scan();
      scanDecorator.accept(metaScan);
      final Table metaTable = conn.getTable(TableName.META_TABLE_NAME);
      final ResultScanner rs = metaTable.getScanner(metaScan);
      final List<R> results = new ArrayList<>();
      Result result;
      while((result = rs.next())!=null){
        R processedResult = scanProcessor.apply(result);
        if(processedResult != null) {
          results.add(processedResult);
        }
      }
      return results;
    }
  }
}