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}