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}