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.rest;
019
020import java.io.UnsupportedEncodingException;
021import java.net.URLDecoder;
022import java.util.ArrayList;
023import java.util.Base64;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.TreeSet;
028import org.apache.hadoop.hbase.HConstants;
029import org.apache.hadoop.hbase.util.Bytes;
030import org.apache.yetus.audience.InterfaceAudience;
031
032/**
033 * Parses a path based row/column/timestamp specification into its component elements.
034 * <p>
035 */
036@InterfaceAudience.Private
037public class RowSpec {
038  public static final long DEFAULT_START_TIMESTAMP = 0;
039  public static final long DEFAULT_END_TIMESTAMP = Long.MAX_VALUE;
040
041  private byte[] row = HConstants.EMPTY_START_ROW;
042  private byte[] endRow = null;
043  private TreeSet<byte[]> columns = new TreeSet<>(Bytes.BYTES_COMPARATOR);
044  private List<String> labels = new ArrayList<>();
045  private long startTime = DEFAULT_START_TIMESTAMP;
046  private long endTime = DEFAULT_END_TIMESTAMP;
047  private int maxVersions = 1;
048  private int maxValues = Integer.MAX_VALUE;
049  private boolean partialTimeRange;
050
051  public RowSpec(String path) throws IllegalArgumentException {
052    this(path, null);
053  }
054
055  public RowSpec(String path, String keyEncoding) throws IllegalArgumentException {
056    int i = 0;
057    while (path.charAt(i) == '/') {
058      i++;
059    }
060    i = parseRowKeys(path, i);
061    i = parseColumns(path, i);
062    i = parseTimestamp(path, i);
063    i = parseQueryParams(path, i);
064
065    if (keyEncoding != null) {
066      // See https://en.wikipedia.org/wiki/Base64#Variants_summary_table
067      Base64.Decoder decoder;
068      switch (keyEncoding) {
069        case "b64":
070        case "base64":
071        case "b64url":
072        case "base64url":
073          decoder = Base64.getUrlDecoder();
074          break;
075        case "b64basic":
076        case "base64basic":
077          decoder = Base64.getDecoder();
078          break;
079        default:
080          throw new IllegalArgumentException("unknown key encoding '" + keyEncoding + "'");
081      }
082      this.row = decoder.decode(this.row);
083      if (this.endRow != null) {
084        this.endRow = decoder.decode(this.endRow);
085      }
086      TreeSet<byte[]> decodedColumns = new TreeSet<>(Bytes.BYTES_COMPARATOR);
087      for (byte[] encodedColumn : this.columns) {
088        decodedColumns.add(decoder.decode(encodedColumn));
089      }
090      this.columns = decodedColumns;
091    }
092  }
093
094  private int parseRowKeys(final String path, int i) throws IllegalArgumentException {
095    String startRow = null, endRow = null;
096    try {
097      StringBuilder sb = new StringBuilder();
098      char c;
099      while (i < path.length() && (c = path.charAt(i)) != '/') {
100        sb.append(c);
101        i++;
102      }
103      i++;
104      String row = startRow = sb.toString();
105      int idx = startRow.indexOf(',');
106      if (idx != -1) {
107        startRow = URLDecoder.decode(row.substring(0, idx), HConstants.UTF8_ENCODING);
108        endRow = URLDecoder.decode(row.substring(idx + 1), HConstants.UTF8_ENCODING);
109      } else {
110        startRow = URLDecoder.decode(row, HConstants.UTF8_ENCODING);
111      }
112    } catch (IndexOutOfBoundsException e) {
113      throw new IllegalArgumentException(e);
114    } catch (UnsupportedEncodingException e) {
115      throw new RuntimeException(e);
116    }
117    // HBase does not support wildcards on row keys so we will emulate a
118    // suffix glob by synthesizing appropriate start and end row keys for
119    // table scanning
120    if (startRow.charAt(startRow.length() - 1) == '*') {
121      if (endRow != null)
122        throw new IllegalArgumentException("invalid path: start row " + "specified with wildcard");
123      this.row = Bytes.toBytes(startRow.substring(0, startRow.lastIndexOf("*")));
124      this.endRow = new byte[this.row.length + 1];
125      System.arraycopy(this.row, 0, this.endRow, 0, this.row.length);
126      this.endRow[this.row.length] = (byte) 255;
127    } else {
128      this.row = Bytes.toBytes(startRow.toString());
129      if (endRow != null) {
130        this.endRow = Bytes.toBytes(endRow.toString());
131      }
132    }
133    return i;
134  }
135
136  private int parseColumns(final String path, int i) throws IllegalArgumentException {
137    if (i >= path.length()) {
138      return i;
139    }
140    try {
141      char c;
142      StringBuilder column = new StringBuilder();
143      while (i < path.length() && (c = path.charAt(i)) != '/') {
144        if (c == ',') {
145          if (column.length() < 1) {
146            throw new IllegalArgumentException("invalid path");
147          }
148          String s = URLDecoder.decode(column.toString(), HConstants.UTF8_ENCODING);
149          this.columns.add(Bytes.toBytes(s));
150          column.setLength(0);
151          i++;
152          continue;
153        }
154        column.append(c);
155        i++;
156      }
157      i++;
158      // trailing list entry
159      if (column.length() > 0) {
160        String s = URLDecoder.decode(column.toString(), HConstants.UTF8_ENCODING);
161        this.columns.add(Bytes.toBytes(s));
162      }
163    } catch (IndexOutOfBoundsException e) {
164      throw new IllegalArgumentException(e);
165    } catch (UnsupportedEncodingException e) {
166      // shouldn't happen
167      throw new RuntimeException(e);
168    }
169    return i;
170  }
171
172  private int parseTimestamp(final String path, int i) throws IllegalArgumentException {
173    if (i >= path.length()) {
174      return i;
175    }
176    long time0 = 0, time1 = 0;
177    try {
178      char c = 0;
179      StringBuilder stamp = new StringBuilder();
180      while (i < path.length()) {
181        c = path.charAt(i);
182        if (c == '/' || c == ',') {
183          break;
184        }
185        stamp.append(c);
186        i++;
187      }
188      try {
189        time0 = Long.parseLong(URLDecoder.decode(stamp.toString(), HConstants.UTF8_ENCODING));
190      } catch (NumberFormatException e) {
191        throw new IllegalArgumentException(e);
192      }
193      if (c == ',') {
194        stamp = new StringBuilder();
195        i++;
196        while (i < path.length() && ((c = path.charAt(i)) != '/')) {
197          stamp.append(c);
198          i++;
199        }
200        try {
201          time1 = Long.parseLong(URLDecoder.decode(stamp.toString(), HConstants.UTF8_ENCODING));
202        } catch (NumberFormatException e) {
203          throw new IllegalArgumentException(e);
204        }
205      }
206      if (c == '/') {
207        i++;
208      }
209    } catch (IndexOutOfBoundsException e) {
210      throw new IllegalArgumentException(e);
211    } catch (UnsupportedEncodingException e) {
212      // shouldn't happen
213      throw new RuntimeException(e);
214    }
215    if (time1 != 0) {
216      startTime = time0;
217      endTime = time1;
218    } else {
219      endTime = time0;
220      partialTimeRange = true;
221    }
222    return i;
223  }
224
225  private int parseQueryParams(final String path, int i) {
226    if (i >= path.length()) {
227      return i;
228    }
229    StringBuilder query = new StringBuilder();
230    try {
231      query.append(URLDecoder.decode(path.substring(i), HConstants.UTF8_ENCODING));
232    } catch (UnsupportedEncodingException e) {
233      // should not happen
234      throw new RuntimeException(e);
235    }
236    i += query.length();
237    int j = 0;
238    while (j < query.length()) {
239      char c = query.charAt(j);
240      if (c != '?' && c != '&') {
241        break;
242      }
243      if (++j > query.length()) {
244        throw new IllegalArgumentException("malformed query parameter");
245      }
246      char what = query.charAt(j);
247      if (++j > query.length()) {
248        break;
249      }
250      c = query.charAt(j);
251      if (c != '=') {
252        throw new IllegalArgumentException("malformed query parameter");
253      }
254      if (++j > query.length()) {
255        break;
256      }
257      switch (what) {
258        case 'm': {
259          StringBuilder sb = new StringBuilder();
260          while (j <= query.length()) {
261            c = query.charAt(j);
262            if (c < '0' || c > '9') {
263              j--;
264              break;
265            }
266            sb.append(c);
267          }
268          maxVersions = Integer.parseInt(sb.toString());
269        }
270          break;
271        case 'n': {
272          StringBuilder sb = new StringBuilder();
273          while (j <= query.length()) {
274            c = query.charAt(j);
275            if (c < '0' || c > '9') {
276              j--;
277              break;
278            }
279            sb.append(c);
280          }
281          maxValues = Integer.parseInt(sb.toString());
282        }
283          break;
284        default:
285          throw new IllegalArgumentException("unknown parameter '" + c + "'");
286      }
287    }
288    return i;
289  }
290
291  public RowSpec(byte[] startRow, byte[] endRow, byte[][] columns, long startTime, long endTime,
292    int maxVersions) {
293    this.row = startRow;
294    this.endRow = endRow;
295    if (columns != null) {
296      Collections.addAll(this.columns, columns);
297    }
298    this.startTime = startTime;
299    this.endTime = endTime;
300    this.maxVersions = maxVersions;
301  }
302
303  public RowSpec(byte[] startRow, byte[] endRow, Collection<byte[]> columns, long startTime,
304    long endTime, int maxVersions, Collection<String> labels) {
305    this(startRow, endRow, columns, startTime, endTime, maxVersions);
306    if (labels != null) {
307      this.labels.addAll(labels);
308    }
309  }
310
311  public RowSpec(byte[] startRow, byte[] endRow, Collection<byte[]> columns, long startTime,
312    long endTime, int maxVersions) {
313    this.row = startRow;
314    this.endRow = endRow;
315    if (columns != null) {
316      this.columns.addAll(columns);
317    }
318    this.startTime = startTime;
319    this.endTime = endTime;
320    this.maxVersions = maxVersions;
321  }
322
323  public boolean isSingleRow() {
324    return endRow == null;
325  }
326
327  public int getMaxVersions() {
328    return maxVersions;
329  }
330
331  public void setMaxVersions(final int maxVersions) {
332    this.maxVersions = maxVersions;
333  }
334
335  public int getMaxValues() {
336    return maxValues;
337  }
338
339  public void setMaxValues(final int maxValues) {
340    this.maxValues = maxValues;
341  }
342
343  public boolean hasColumns() {
344    return !columns.isEmpty();
345  }
346
347  public boolean hasLabels() {
348    return !labels.isEmpty();
349  }
350
351  public byte[] getRow() {
352    return row;
353  }
354
355  public byte[] getStartRow() {
356    return row;
357  }
358
359  public boolean hasEndRow() {
360    return endRow != null;
361  }
362
363  public byte[] getEndRow() {
364    return endRow;
365  }
366
367  public void addColumn(final byte[] column) {
368    columns.add(column);
369  }
370
371  public byte[][] getColumns() {
372    return columns.toArray(new byte[columns.size()][]);
373  }
374
375  public List<String> getLabels() {
376    return labels;
377  }
378
379  public boolean hasTimestamp() {
380    return (startTime == 0) && (endTime != Long.MAX_VALUE);
381  }
382
383  public long getTimestamp() {
384    return endTime;
385  }
386
387  public long getStartTime() {
388    return startTime;
389  }
390
391  public void setStartTime(final long startTime) {
392    this.startTime = startTime;
393  }
394
395  public long getEndTime() {
396    return endTime;
397  }
398
399  public void setEndTime(long endTime) {
400    this.endTime = endTime;
401  }
402
403  @Override
404  public String toString() {
405    StringBuilder result = new StringBuilder();
406    result.append("{startRow => '");
407    if (row != null) {
408      result.append(Bytes.toString(row));
409    }
410    result.append("', endRow => '");
411    if (endRow != null) {
412      result.append(Bytes.toString(endRow));
413    }
414    result.append("', columns => [");
415    for (byte[] col : columns) {
416      result.append(" '");
417      result.append(Bytes.toString(col));
418      result.append("'");
419    }
420    result.append(" ], startTime => ");
421    result.append(Long.toString(startTime));
422    result.append(", endTime => ");
423    result.append(Long.toString(endTime));
424    result.append(", maxVersions => ");
425    result.append(Integer.toString(maxVersions));
426    result.append(", maxValues => ");
427    result.append(Integer.toString(maxValues));
428    result.append("}");
429    return result.toString();
430  }
431
432  public boolean isPartialTimeRange() {
433    return partialTimeRange;
434  }
435
436}