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