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