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