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