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.client;
019
020import edu.umd.cs.findbugs.annotations.CheckForNull;
021import java.io.DataInputStream;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Comparator;
026import java.util.List;
027import java.util.stream.Collectors;
028import org.apache.hadoop.hbase.HConstants;
029import org.apache.hadoop.hbase.TableName;
030import org.apache.hadoop.hbase.exceptions.DeserializationException;
031import org.apache.hadoop.hbase.util.ByteArrayHashKey;
032import org.apache.hadoop.hbase.util.Bytes;
033import org.apache.hadoop.hbase.util.HashKey;
034import org.apache.hadoop.hbase.util.JenkinsHash;
035import org.apache.hadoop.hbase.util.MD5Hash;
036import org.apache.hadoop.io.DataInputBuffer;
037import org.apache.hadoop.io.IOUtils;
038import org.apache.yetus.audience.InterfaceAudience;
039
040import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
041import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
042
043/**
044 * Information about a region. A region is a range of keys in the whole keyspace of a table, an
045 * identifier (a timestamp) for differentiating between subset ranges (after region split) and a
046 * replicaId for differentiating the instance for the same range and some status information about
047 * the region. The region has a unique name which consists of the following fields:
048 * <ul>
049 * <li>tableName : The name of the table</li>
050 * <li>startKey : The startKey for the region.</li>
051 * <li>regionId : A timestamp when the region is created.</li>
052 * <li>replicaId : An id starting from 0 to differentiate replicas of the same region range but
053 * hosted in separated servers. The same region range can be hosted in multiple locations.</li>
054 * <li>encodedName : An MD5 encoded string for the region name.</li>
055 * </ul>
056 * <br>
057 * Other than the fields in the region name, region info contains:
058 * <ul>
059 * <li>endKey : the endKey for the region (exclusive)</li>
060 * <li>split : Whether the region is split</li>
061 * <li>offline : Whether the region is offline</li>
062 * </ul>
063 */
064@InterfaceAudience.Public
065public interface RegionInfo extends Comparable<RegionInfo> {
066
067  /**
068   * Separator used to demarcate the encodedName in a region name in the new format. See description
069   * on new format above.
070   */
071  @InterfaceAudience.Private
072  int ENC_SEPARATOR = '.';
073
074  @InterfaceAudience.Private
075  int MD5_HEX_LENGTH = 32;
076
077  @InterfaceAudience.Private
078  int DEFAULT_REPLICA_ID = 0;
079
080  /**
081   * to keep appended int's sorted in string format. Only allows 2 bytes to be sorted for replicaId.
082   */
083  @InterfaceAudience.Private
084  String REPLICA_ID_FORMAT = "%04X";
085
086  @InterfaceAudience.Private
087  byte REPLICA_ID_DELIMITER = (byte) '_';
088
089  @InterfaceAudience.Private
090  String INVALID_REGION_NAME_FORMAT_MESSAGE = "Invalid regionName format";
091
092  @InterfaceAudience.Private
093  Comparator<RegionInfo> COMPARATOR = (RegionInfo lhs, RegionInfo rhs) -> {
094    if (rhs == null) {
095      return 1;
096    }
097
098    // Are regions of same table?
099    int result = lhs.getTable().compareTo(rhs.getTable());
100    if (result != 0) {
101      return result;
102    }
103
104    // Compare start keys.
105    result = Bytes.compareTo(lhs.getStartKey(), rhs.getStartKey());
106    if (result != 0) {
107      return result;
108    }
109
110    // Compare end keys.
111    result = Bytes.compareTo(lhs.getEndKey(), rhs.getEndKey());
112
113    if (result != 0) {
114      if (lhs.getStartKey().length != 0 && lhs.getEndKey().length == 0) {
115        return 1; // this is last region
116      }
117      if (rhs.getStartKey().length != 0 && rhs.getEndKey().length == 0) {
118        return -1; // o is the last region
119      }
120      return result;
121    }
122
123    // regionId is usually milli timestamp -- this defines older stamps
124    // to be "smaller" than newer stamps in sort order.
125    if (lhs.getRegionId() > rhs.getRegionId()) {
126      return 1;
127    } else if (lhs.getRegionId() < rhs.getRegionId()) {
128      return -1;
129    }
130
131    int replicaDiff = lhs.getReplicaId() - rhs.getReplicaId();
132    if (replicaDiff != 0) {
133      return replicaDiff;
134    }
135
136    if (lhs.isOffline() == rhs.isOffline()) {
137      return 0;
138    }
139    if (lhs.isOffline()) {
140      return -1;
141    }
142
143    return 1;
144  };
145
146  /**
147   * Returns Return a short, printable name for this region (usually encoded name) for us logging.
148   */
149  String getShortNameToLog();
150
151  /** Returns the regionId. */
152  long getRegionId();
153
154  /**
155   * @return the regionName as an array of bytes.
156   * @see #getRegionNameAsString()
157   */
158  byte[] getRegionName();
159
160  /** Returns Region name as a String for use in logging, etc. */
161  String getRegionNameAsString();
162
163  /** Returns the encoded region name. */
164  String getEncodedName();
165
166  /** Returns the encoded region name as an array of bytes. */
167  byte[] getEncodedNameAsBytes();
168
169  /** Returns the startKey. */
170  byte[] getStartKey();
171
172  /** Returns the endKey. */
173  byte[] getEndKey();
174
175  /** Returns current table name of the region */
176  TableName getTable();
177
178  /** Returns returns region replica id */
179  int getReplicaId();
180
181  /** Returns True if has been split and has daughters. */
182  boolean isSplit();
183
184  /**
185   * @return True if this region is offline.
186   * @deprecated since 3.0.0 and will be removed in 4.0.0
187   * @see <a href="https://issues.apache.org/jira/browse/HBASE-25210">HBASE-25210</a>
188   */
189  @Deprecated
190  boolean isOffline();
191
192  /**
193   * @return True if this is a split parent region.
194   * @deprecated since 3.0.0 and will be removed in 4.0.0, Use {@link #isSplit()} instead.
195   * @see <a href="https://issues.apache.org/jira/browse/HBASE-25210">HBASE-25210</a>
196   */
197  @Deprecated
198  boolean isSplitParent();
199
200  /** Returns true if this region is a meta region. */
201  boolean isMetaRegion();
202
203  /**
204   * @return true if the given inclusive range of rows is fully contained by this region. For
205   *         example, if the region is foo,a,g and this is passed ["b","c"] or ["a","c"] it will
206   *         return true, but if this is passed ["b","z"] it will return false.
207   * @throws IllegalArgumentException if the range passed is invalid (ie. end &lt; start)
208   */
209  boolean containsRange(byte[] rangeStartKey, byte[] rangeEndKey);
210
211  /** Returns true if the given row falls in this region. */
212  boolean containsRow(byte[] row);
213
214  /**
215   * Does region name contain its encoded name?
216   * @param regionName region name
217   * @return boolean indicating if this a new format region name which contains its encoded name.
218   */
219  @InterfaceAudience.Private
220  static boolean hasEncodedName(final byte[] regionName) {
221    // check if region name ends in ENC_SEPARATOR
222    return (regionName.length >= 1)
223      && (regionName[regionName.length - 1] == RegionInfo.ENC_SEPARATOR);
224  }
225
226  /** Returns the encodedName */
227  @InterfaceAudience.Private
228  static String encodeRegionName(final byte[] regionName) {
229    String encodedName;
230    if (hasEncodedName(regionName)) {
231      // region is in new format:
232      // <tableName>,<startKey>,<regionIdTimeStamp>/encodedName/
233      encodedName =
234        Bytes.toString(regionName, regionName.length - MD5_HEX_LENGTH - 1, MD5_HEX_LENGTH);
235    } else {
236      // old format region name. First hbase:meta region also
237      // use this format.EncodedName is the JenkinsHash value.
238      HashKey<byte[]> key = new ByteArrayHashKey(regionName, 0, regionName.length);
239      int hashVal = Math.abs(JenkinsHash.getInstance().hash(key, 0));
240      encodedName = String.valueOf(hashVal);
241    }
242    return encodedName;
243  }
244
245  @InterfaceAudience.Private
246  static String getRegionNameAsString(byte[] regionName) {
247    return getRegionNameAsString(null, regionName);
248  }
249
250  @InterfaceAudience.Private
251  static String getRegionNameAsString(@CheckForNull RegionInfo ri, byte[] regionName) {
252    if (RegionInfo.hasEncodedName(regionName)) {
253      // new format region names already have their encoded name.
254      return Bytes.toStringBinary(regionName);
255    }
256
257    // old format. regionNameStr doesn't have the region name.
258    if (ri == null) {
259      return Bytes.toStringBinary(regionName) + "." + RegionInfo.encodeRegionName(regionName);
260    } else {
261      return Bytes.toStringBinary(regionName) + "." + ri.getEncodedName();
262    }
263  }
264
265  /**
266   * @return Return a String of short, printable names for <code>hris</code> (usually encoded name)
267   *         for us logging.
268   */
269  static String getShortNameToLog(RegionInfo... hris) {
270    return getShortNameToLog(Arrays.asList(hris));
271  }
272
273  /**
274   * @return Return a String of short, printable names for <code>hris</code> (usually encoded name)
275   *         for us logging.
276   */
277  static String getShortNameToLog(final List<RegionInfo> ris) {
278    return ris.stream().map(RegionInfo::getEncodedName).collect(Collectors.toList()).toString();
279  }
280
281  /**
282   * Gets the table name from the specified region name.
283   * @param regionName to extract the table name from
284   * @return Table name
285   */
286  @InterfaceAudience.Private
287  // This method should never be used. Its awful doing parse from bytes.
288  // It is fallback in case we can't get the tablename any other way. Could try removing it.
289  // Keeping it Audience Private so can remove at later date.
290  static TableName getTable(final byte[] regionName) {
291    int offset = -1;
292    for (int i = 0; i < regionName.length; i++) {
293      if (regionName[i] == HConstants.DELIMITER) {
294        offset = i;
295        break;
296      }
297    }
298    if (offset <= 0) {
299      throw new IllegalArgumentException("offset=" + offset);
300    }
301    byte[] buff = new byte[offset];
302    System.arraycopy(regionName, 0, buff, 0, offset);
303    return TableName.valueOf(buff);
304  }
305
306  /**
307   * Gets the start key from the specified region name.
308   * @return Start key.
309   */
310  static byte[] getStartKey(final byte[] regionName) throws IOException {
311    return parseRegionName(regionName)[1];
312  }
313
314  /**
315   * Figure if the passed bytes represent an encoded region name or not.
316   * @param regionName A Region name either encoded or not.
317   * @return True if <code>regionName</code> represents an encoded name.
318   */
319  @InterfaceAudience.Private // For use by internals only.
320  public static boolean isEncodedRegionName(byte[] regionName) {
321    // If not parseable as region name, presume encoded. TODO: add stringency; e.g. if hex.
322    return parseRegionNameOrReturnNull(regionName) == null && regionName.length <= MD5_HEX_LENGTH;
323  }
324
325  /**
326   * Returns A deserialized {@link RegionInfo} or null if we failed deserialize or passed bytes null
327   */
328  @InterfaceAudience.Private
329  static RegionInfo parseFromOrNull(final byte[] bytes) {
330    if (bytes == null) return null;
331    return parseFromOrNull(bytes, 0, bytes.length);
332  }
333
334  /**
335   * Returns A deserialized {@link RegionInfo} or null if we failed deserialize or passed bytes null
336   */
337  @InterfaceAudience.Private
338  static RegionInfo parseFromOrNull(final byte[] bytes, int offset, int len) {
339    if (bytes == null || len <= 0) return null;
340    try {
341      return parseFrom(bytes, offset, len);
342    } catch (DeserializationException e) {
343      return null;
344    }
345  }
346
347  /**
348   * @param bytes A pb RegionInfo serialized with a pb magic prefix.
349   * @return A deserialized {@link RegionInfo}
350   */
351  @InterfaceAudience.Private
352  static RegionInfo parseFrom(final byte[] bytes) throws DeserializationException {
353    if (bytes == null) return null;
354    return parseFrom(bytes, 0, bytes.length);
355  }
356
357  /**
358   * @param bytes  A pb RegionInfo serialized with a pb magic prefix.
359   * @param offset starting point in the byte array
360   * @param len    length to read on the byte array
361   * @return A deserialized {@link RegionInfo}
362   */
363  @InterfaceAudience.Private
364  static RegionInfo parseFrom(final byte[] bytes, int offset, int len)
365    throws DeserializationException {
366    if (ProtobufUtil.isPBMagicPrefix(bytes, offset, len)) {
367      int pblen = ProtobufUtil.lengthOfPBMagic();
368      try {
369        HBaseProtos.RegionInfo.Builder builder = HBaseProtos.RegionInfo.newBuilder();
370        ProtobufUtil.mergeFrom(builder, bytes, pblen + offset, len - pblen);
371        HBaseProtos.RegionInfo ri = builder.build();
372        return ProtobufUtil.toRegionInfo(ri);
373      } catch (IOException e) {
374        throw new DeserializationException(e);
375      }
376    } else {
377      throw new DeserializationException("PB encoded RegionInfo expected");
378    }
379  }
380
381  static boolean isMD5Hash(String encodedRegionName) {
382    if (encodedRegionName.length() != MD5_HEX_LENGTH) {
383      return false;
384    }
385
386    for (int i = 0; i < encodedRegionName.length(); i++) {
387      char c = encodedRegionName.charAt(i);
388      if (!((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'))) {
389        return false;
390      }
391    }
392    return true;
393  }
394
395  /**
396   * Check whether two regions are adjacent; i.e. lies just before or just after in a table.
397   * @return true if two regions are adjacent
398   */
399  static boolean areAdjacent(RegionInfo regionA, RegionInfo regionB) {
400    if (regionA == null || regionB == null) {
401      throw new IllegalArgumentException("Can't check whether adjacent for null region");
402    }
403    if (!regionA.getTable().equals(regionB.getTable())) {
404      return false;
405    }
406    RegionInfo a = regionA;
407    RegionInfo b = regionB;
408    if (Bytes.compareTo(a.getStartKey(), b.getStartKey()) > 0) {
409      a = regionB;
410      b = regionA;
411    }
412    return Bytes.equals(a.getEndKey(), b.getStartKey());
413  }
414
415  /**
416   * @return This instance serialized as protobuf w/ a magic pb prefix.
417   * @see #parseFrom(byte[])
418   */
419  static byte[] toByteArray(RegionInfo ri) {
420    byte[] bytes = ProtobufUtil.toRegionInfo(ri).toByteArray();
421    return ProtobufUtil.prependPBMagic(bytes);
422  }
423
424  /**
425   * Use logging.
426   * @param encodedRegionName The encoded regionname.
427   * @return <code>hbase:meta</code> if passed <code>1028785192</code> else returns
428   *         <code>encodedRegionName</code>
429   */
430  static String prettyPrint(final String encodedRegionName) {
431    if (encodedRegionName.equals("1028785192")) {
432      return encodedRegionName + "/hbase:meta";
433    }
434    return encodedRegionName;
435  }
436
437  /**
438   * Make a region name of passed parameters.
439   * @param startKey  Can be null
440   * @param regionid  Region id (Usually timestamp from when region was created).
441   * @param newFormat should we create the region name in the new format (such that it contains its
442   *                  encoded name?).
443   * @return Region name made of passed tableName, startKey and id
444   */
445  static byte[] createRegionName(final TableName tableName, final byte[] startKey,
446    final long regionid, boolean newFormat) {
447    return createRegionName(tableName, startKey, Long.toString(regionid), newFormat);
448  }
449
450  /**
451   * Make a region name of passed parameters.
452   * @param startKey  Can be null
453   * @param id        Region id (Usually timestamp from when region was created).
454   * @param newFormat should we create the region name in the new format (such that it contains its
455   *                  encoded name?).
456   * @return Region name made of passed tableName, startKey and id
457   */
458  static byte[] createRegionName(final TableName tableName, final byte[] startKey, final String id,
459    boolean newFormat) {
460    return createRegionName(tableName, startKey, Bytes.toBytes(id), newFormat);
461  }
462
463  /**
464   * Make a region name of passed parameters.
465   * @param startKey  Can be null
466   * @param regionid  Region id (Usually timestamp from when region was created).
467   * @param newFormat should we create the region name in the new format (such that it contains its
468   *                  encoded name?).
469   * @return Region name made of passed tableName, startKey, id and replicaId
470   */
471  static byte[] createRegionName(final TableName tableName, final byte[] startKey,
472    final long regionid, int replicaId, boolean newFormat) {
473    return createRegionName(tableName, startKey, Bytes.toBytes(Long.toString(regionid)), replicaId,
474      newFormat);
475  }
476
477  /**
478   * Make a region name of passed parameters.
479   * @param startKey  Can be null
480   * @param id        Region id (Usually timestamp from when region was created).
481   * @param newFormat should we create the region name in the new format (such that it contains its
482   *                  encoded name?).
483   * @return Region name made of passed tableName, startKey and id
484   */
485  static byte[] createRegionName(final TableName tableName, final byte[] startKey, final byte[] id,
486    boolean newFormat) {
487    return createRegionName(tableName, startKey, id, DEFAULT_REPLICA_ID, newFormat);
488  }
489
490  /**
491   * Make a region name of passed parameters.
492   * @param startKey  Can be null
493   * @param id        Region id (Usually timestamp from when region was created).
494   * @param newFormat should we create the region name in the new format
495   * @return Region name made of passed tableName, startKey, id and replicaId
496   */
497  static byte[] createRegionName(final TableName tableName, final byte[] startKey, final byte[] id,
498    final int replicaId, boolean newFormat) {
499    int len = tableName.getName().length + 2 + id.length + (startKey == null ? 0 : startKey.length);
500    if (newFormat) {
501      len += MD5_HEX_LENGTH + 2;
502    }
503    byte[] replicaIdBytes = null;
504    // Special casing: replicaId is only appended if replicaId is greater than
505    // 0. This is because all regions in meta would have to be migrated to the new
506    // name otherwise
507    if (replicaId > 0) {
508      // use string representation for replica id
509      replicaIdBytes = Bytes.toBytes(String.format(REPLICA_ID_FORMAT, replicaId));
510      len += 1 + replicaIdBytes.length;
511    }
512
513    byte[] b = new byte[len];
514
515    int offset = tableName.getName().length;
516    System.arraycopy(tableName.getName(), 0, b, 0, offset);
517    b[offset++] = HConstants.DELIMITER;
518    if (startKey != null && startKey.length > 0) {
519      System.arraycopy(startKey, 0, b, offset, startKey.length);
520      offset += startKey.length;
521    }
522    b[offset++] = HConstants.DELIMITER;
523    System.arraycopy(id, 0, b, offset, id.length);
524    offset += id.length;
525
526    if (replicaIdBytes != null) {
527      b[offset++] = REPLICA_ID_DELIMITER;
528      System.arraycopy(replicaIdBytes, 0, b, offset, replicaIdBytes.length);
529      offset += replicaIdBytes.length;
530    }
531
532    if (newFormat) {
533      //
534      // Encoded name should be built into the region name.
535      //
536      // Use the region name thus far (namely, <tablename>,<startKey>,<id>_<replicaId>)
537      // to compute a MD5 hash to be used as the encoded name, and append
538      // it to the byte buffer.
539      //
540      String md5Hash = MD5Hash.getMD5AsHex(b, 0, offset);
541      byte[] md5HashBytes = Bytes.toBytes(md5Hash);
542
543      if (md5HashBytes.length != MD5_HEX_LENGTH) {
544        System.out.println(
545          "MD5-hash length mismatch: Expected=" + MD5_HEX_LENGTH + "; Got=" + md5HashBytes.length);
546      }
547
548      // now append the bytes '.<encodedName>.' to the end
549      b[offset++] = ENC_SEPARATOR;
550      System.arraycopy(md5HashBytes, 0, b, offset, MD5_HEX_LENGTH);
551      offset += MD5_HEX_LENGTH;
552      b[offset] = ENC_SEPARATOR;
553    }
554
555    return b;
556  }
557
558  /**
559   * Creates a RegionInfo object for MOB data.
560   * @param tableName the name of the table
561   * @return the MOB {@link RegionInfo}.
562   */
563  static RegionInfo createMobRegionInfo(TableName tableName) {
564    // Skipping reference to RegionInfoBuilder in this class.
565    return new MutableRegionInfo(tableName, Bytes.toBytes(".mob"), HConstants.EMPTY_END_ROW, false,
566      0, DEFAULT_REPLICA_ID, false);
567  }
568
569  /**
570   * Separate elements of a regionName.
571   * @return Array of byte[] containing tableName, startKey and id OR null if not parseable as a
572   *         region name.
573   * @throws IOException if not parseable as regionName.
574   */
575  static byte[][] parseRegionName(final byte[] regionName) throws IOException {
576    byte[][] result = parseRegionNameOrReturnNull(regionName);
577    if (result == null) {
578      throw new IOException(
579        INVALID_REGION_NAME_FORMAT_MESSAGE + ": " + Bytes.toStringBinary(regionName));
580    }
581    return result;
582  }
583
584  /**
585   * Separate elements of a regionName. Region name is of the format:
586   * <code>tablename,startkey,regionIdTimestamp[_replicaId][.encodedName.]</code>. Startkey can
587   * contain the delimiter (',') so we parse from the start and then parse from the end.
588   * @return Array of byte[] containing tableName, startKey and id OR null if not parseable as a
589   *         region name.
590   */
591  static byte[][] parseRegionNameOrReturnNull(final byte[] regionName) {
592    int offset = -1;
593    for (int i = 0; i < regionName.length; i++) {
594      if (regionName[i] == HConstants.DELIMITER) {
595        offset = i;
596        break;
597      }
598    }
599    if (offset == -1) {
600      return null;
601    }
602    byte[] tableName = new byte[offset];
603    System.arraycopy(regionName, 0, tableName, 0, offset);
604    offset = -1;
605
606    int endOffset = regionName.length;
607    // check whether regionName contains encodedName
608    if (
609      regionName.length > MD5_HEX_LENGTH + 2 && regionName[regionName.length - 1] == ENC_SEPARATOR
610        && regionName[regionName.length - MD5_HEX_LENGTH - 2] == ENC_SEPARATOR
611    ) {
612      endOffset = endOffset - MD5_HEX_LENGTH - 2;
613    }
614
615    // parse from end
616    byte[] replicaId = null;
617    int idEndOffset = endOffset;
618    for (int i = endOffset - 1; i > 0; i--) {
619      if (regionName[i] == REPLICA_ID_DELIMITER) { // replicaId may or may not be present
620        replicaId = new byte[endOffset - i - 1];
621        System.arraycopy(regionName, i + 1, replicaId, 0, endOffset - i - 1);
622        idEndOffset = i;
623        // do not break, continue to search for id
624      }
625      if (regionName[i] == HConstants.DELIMITER) {
626        offset = i;
627        break;
628      }
629    }
630    if (offset == -1) {
631      return null;
632    }
633    byte[] startKey = HConstants.EMPTY_BYTE_ARRAY;
634    if (offset != tableName.length + 1) {
635      startKey = new byte[offset - tableName.length - 1];
636      System.arraycopy(regionName, tableName.length + 1, startKey, 0,
637        offset - tableName.length - 1);
638    }
639    byte[] id = new byte[idEndOffset - offset - 1];
640    System.arraycopy(regionName, offset + 1, id, 0, idEndOffset - offset - 1);
641    byte[][] elements = new byte[replicaId == null ? 3 : 4][];
642    elements[0] = tableName;
643    elements[1] = startKey;
644    elements[2] = id;
645    if (replicaId != null) {
646      elements[3] = replicaId;
647    }
648    return elements;
649  }
650
651  /**
652   * Serializes given RegionInfo's as a byte array. Use this instead of
653   * {@link RegionInfo#toByteArray(RegionInfo)} when writing to a stream and you want to use the pb
654   * mergeDelimitedFrom (w/o the delimiter, pb reads to EOF which may not be what you want).
655   * {@link #parseDelimitedFrom(byte[], int, int)} can be used to read back the instances.
656   * @param infos RegionInfo objects to serialize
657   * @return This instance serialized as a delimited protobuf w/ a magic pb prefix.
658   */
659  static byte[] toDelimitedByteArray(RegionInfo... infos) throws IOException {
660    byte[][] bytes = new byte[infos.length][];
661    int size = 0;
662    for (int i = 0; i < infos.length; i++) {
663      bytes[i] = toDelimitedByteArray(infos[i]);
664      size += bytes[i].length;
665    }
666
667    byte[] result = new byte[size];
668    int offset = 0;
669    for (byte[] b : bytes) {
670      System.arraycopy(b, 0, result, offset, b.length);
671      offset += b.length;
672    }
673    return result;
674  }
675
676  /**
677   * Use this instead of {@link RegionInfo#toByteArray(RegionInfo)} when writing to a stream and you
678   * want to use the pb mergeDelimitedFrom (w/o the delimiter, pb reads to EOF which may not be what
679   * you want).
680   * @return This instance serialized as a delimied protobuf w/ a magic pb prefix.
681   */
682  static byte[] toDelimitedByteArray(RegionInfo ri) throws IOException {
683    return ProtobufUtil.toDelimitedByteArray(ProtobufUtil.toRegionInfo(ri));
684  }
685
686  /**
687   * Parses an RegionInfo instance from the passed in stream. Presumes the RegionInfo was serialized
688   * to the stream with {@link #toDelimitedByteArray(RegionInfo)}.
689   * @return An instance of RegionInfo.
690   */
691  static RegionInfo parseFrom(final DataInputStream in) throws IOException {
692    // I need to be able to move back in the stream if this is not a pb
693    // serialization so I can do the Writable decoding instead.
694    int pblen = ProtobufUtil.lengthOfPBMagic();
695    byte[] pbuf = new byte[pblen];
696    if (in.markSupported()) { // read it with mark()
697      in.mark(pblen);
698    }
699
700    // assumption: if Writable serialization, it should be longer than pblen.
701    IOUtils.readFully(in, pbuf, 0, pblen);
702    if (ProtobufUtil.isPBMagicPrefix(pbuf)) {
703      return ProtobufUtil.toRegionInfo(HBaseProtos.RegionInfo.parseDelimitedFrom(in));
704    } else {
705      throw new IOException("PB encoded RegionInfo expected");
706    }
707  }
708
709  /**
710   * Parses all the RegionInfo instances from the passed in stream until EOF. Presumes the
711   * RegionInfo's were serialized to the stream with oDelimitedByteArray()
712   * @param bytes  serialized bytes
713   * @param offset the start offset into the byte[] buffer
714   * @param length how far we should read into the byte[] buffer
715   * @return All the RegionInfos that are in the byte array. Keeps reading till we hit the end.
716   */
717  static List<RegionInfo> parseDelimitedFrom(final byte[] bytes, final int offset, final int length)
718    throws IOException {
719    if (bytes == null) {
720      throw new IllegalArgumentException("Can't build an object with empty bytes array");
721    }
722    List<RegionInfo> ris = new ArrayList<>();
723    try (DataInputBuffer in = new DataInputBuffer()) {
724      in.reset(bytes, offset, length);
725      while (in.available() > 0) {
726        RegionInfo ri = parseFrom(in);
727        ris.add(ri);
728      }
729    }
730    return ris;
731  }
732
733  /** Returns True if this is first Region in Table */
734  default boolean isFirst() {
735    return Bytes.equals(getStartKey(), HConstants.EMPTY_START_ROW);
736  }
737
738  /** Returns True if this is last Region in Table */
739  default boolean isLast() {
740    return Bytes.equals(getEndKey(), HConstants.EMPTY_END_ROW);
741  }
742
743  /**
744   * @return True if region is next, adjacent but 'after' this one.
745   * @see #isAdjacent(RegionInfo)
746   * @see #areAdjacent(RegionInfo, RegionInfo)
747   */
748  default boolean isNext(RegionInfo after) {
749    return getTable().equals(after.getTable()) && Bytes.equals(getEndKey(), after.getStartKey());
750  }
751
752  /**
753   * @return True if region is adjacent, either just before or just after this one.
754   * @see #isNext(RegionInfo)
755   */
756  default boolean isAdjacent(RegionInfo other) {
757    return getTable().equals(other.getTable()) && areAdjacent(this, other);
758  }
759
760  /** Returns True if RegionInfo is degenerate... if startKey > endKey. */
761  default boolean isDegenerate() {
762    return !isLast() && Bytes.compareTo(getStartKey(), getEndKey()) > 0;
763  }
764
765  /**
766   * @return True if an overlap in region range.
767   * @see #isDegenerate()
768   */
769  default boolean isOverlap(RegionInfo other) {
770    if (other == null) {
771      return false;
772    }
773    if (!getTable().equals(other.getTable())) {
774      return false;
775    }
776    int startKeyCompare = Bytes.compareTo(getStartKey(), other.getStartKey());
777    if (startKeyCompare == 0) {
778      return true;
779    }
780    if (startKeyCompare < 0) {
781      if (isLast()) {
782        return true;
783      }
784      return Bytes.compareTo(getEndKey(), other.getStartKey()) > 0;
785    }
786    if (other.isLast()) {
787      return true;
788    }
789    return Bytes.compareTo(getStartKey(), other.getEndKey()) < 0;
790  }
791
792  default int compareTo(RegionInfo other) {
793    return RegionInfo.COMPARATOR.compare(this, other);
794  }
795}