View Javadoc

1   /*
2    * Copyright 2010 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  
21  package org.apache.hadoop.hbase.rest;
22  
23  import java.io.IOException;
24  import java.util.Map;
25  import java.util.concurrent.ConcurrentHashMap;
26  import java.util.concurrent.ConcurrentSkipListMap;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import javax.ws.rs.Encoded;
31  import javax.ws.rs.Path;
32  import javax.ws.rs.PathParam;
33  import javax.ws.rs.QueryParam;
34  
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.hadoop.hbase.HColumnDescriptor;
38  import org.apache.hadoop.hbase.HConstants;
39  import org.apache.hadoop.hbase.HTableDescriptor;
40  import org.apache.hadoop.hbase.TableNotFoundException;
41  import org.apache.hadoop.hbase.client.HBaseAdmin;
42  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
43  import org.apache.hadoop.hbase.rest.transform.NullTransform;
44  import org.apache.hadoop.hbase.rest.transform.Transform;
45  import org.apache.hadoop.hbase.util.Bytes;
46  import org.apache.hadoop.util.StringUtils;
47  
48  public class TableResource extends ResourceBase {
49    private static final Log LOG = LogFactory.getLog(TableResource.class);
50  
51    /**
52     * HCD attributes starting with this string are considered transform
53     * directives
54     */
55    private static final String DIRECTIVE_KEY = "Transform$";
56  
57    /**
58     * Transform directives are of the form <tt>&lt;qualifier&gt;:&lt;class&gt;</tt>
59     * where <tt>qualifier</tt> is a string for exact matching or '*' as a wildcard
60     * that will match anything; and <tt>class</tt> is either the fully qualified
61     * class name of a transform implementation or can be the short name of a
62     * transform in the <tt>org.apache.hadoop.hbase.rest.transform package</tt>.
63     */
64    private static final Pattern DIRECTIVE_PATTERN =
65      Pattern.compile("([^\\:]+)\\:([^\\,]+)\\,?");
66    private static final Transform defaultTransform = new NullTransform();
67    private static final
68      Map<String,Map<byte[],Map<byte[],Transform>>> transformMap =
69        new ConcurrentHashMap<String,Map<byte[],Map<byte[],Transform>>>();
70    private static final Map<String,Long> lastCheckedMap =
71      new ConcurrentHashMap<String,Long>();
72  
73    /**
74     * @param table the table
75     * @param family the column family
76     * @param qualifier the column qualifier, or null
77     * @return the transformation specified for the given family or qualifier, if
78     * any, otherwise the default
79     */
80    static Transform getTransform(String table, byte[] family, byte[] qualifier) {
81      if (qualifier == null) {
82        qualifier = HConstants.EMPTY_BYTE_ARRAY;
83      }
84      Map<byte[],Map<byte[],Transform>> familyMap = transformMap.get(table);
85      if (familyMap != null) {
86        Map<byte[],Transform> columnMap = familyMap.get(family);
87        if (columnMap != null) {
88          Transform t = columnMap.get(qualifier);
89          // check as necessary if there is a wildcard entry
90          if (t == null) {
91            t = columnMap.get(HConstants.EMPTY_BYTE_ARRAY);
92          }
93          // if we found something, return it, otherwise we will return the
94          // default by falling through
95          if (t != null) {
96            return t;
97          }
98        }
99      }
100     return defaultTransform;
101   }
102 
103   synchronized static void setTransform(String table, byte[] family,
104       byte[] qualifier, Transform transform) {
105     Map<byte[],Map<byte[],Transform>> familyMap = transformMap.get(table);
106     if (familyMap == null) {
107       familyMap =  new ConcurrentSkipListMap<byte[],Map<byte[],Transform>>(
108           Bytes.BYTES_COMPARATOR);
109       transformMap.put(table, familyMap);
110     }
111     Map<byte[],Transform> columnMap = familyMap.get(family);
112     if (columnMap == null) {
113       columnMap = new ConcurrentSkipListMap<byte[],Transform>(
114           Bytes.BYTES_COMPARATOR);
115       familyMap.put(family, columnMap);
116     }
117     // if transform is null, remove any existing entry
118     if (transform != null) {
119       columnMap.put(qualifier, transform);
120     } else {
121       columnMap.remove(qualifier);
122     }
123   }
124 
125   String table;
126 
127   /**
128    * Scan the table schema for transform directives. These are column family
129    * attributes containing a comma-separated list of elements of the form
130    * <tt>&lt;qualifier&gt;:&lt;transform-class&gt;</tt>, where qualifier
131    * can be a string for exact matching or '*' as a wildcard to match anything.
132    * The attribute key must begin with the string "Transform$".
133    */
134   void scanTransformAttrs() throws IOException {
135     try {
136       HBaseAdmin admin = new HBaseAdmin(servlet.getConfiguration());
137       HTableDescriptor htd = admin.getTableDescriptor(Bytes.toBytes(table));
138       for (HColumnDescriptor hcd: htd.getFamilies()) {
139         for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e:
140             hcd.getValues().entrySet()) {
141           // does the key start with the transform directive tag?
142           String key = Bytes.toString(e.getKey().get());
143           if (!key.startsWith(DIRECTIVE_KEY)) {
144             // no, skip
145             continue;
146           }
147           // match a comma separated list of one or more directives
148           byte[] value = e.getValue().get();
149           Matcher m = DIRECTIVE_PATTERN.matcher(Bytes.toString(value));
150           while (m.find()) {
151             byte[] qualifier = HConstants.EMPTY_BYTE_ARRAY;
152             String s = m.group(1);
153             if (s.length() > 0 && !s.equals("*")) {
154               qualifier = Bytes.toBytes(s);
155             }
156             boolean retry = false;
157             String className = m.group(2);
158             while (true) {
159               try {
160                 // if a transform was previously configured for the qualifier,
161                 // this will simply replace it
162                 setTransform(table, hcd.getName(), qualifier,
163                   (Transform)Class.forName(className).newInstance());
164                 break;
165               } catch (InstantiationException ex) {
166                 LOG.error(StringUtils.stringifyException(ex));
167                 if (retry) {
168                   break;
169                 }
170                 retry = true;
171               } catch (IllegalAccessException ex) {
172                 LOG.error(StringUtils.stringifyException(ex));
173                 if (retry) {
174                   break;
175                 }
176                 retry = true;
177               } catch (ClassNotFoundException ex) {
178                 if (retry) {
179                   LOG.error(StringUtils.stringifyException(ex));
180                   break;
181                 }
182                 className = "org.apache.hadoop.hbase.rest.transform." + className;
183                 retry = true;
184               }
185             }
186           }
187         }
188       }
189     } catch (TableNotFoundException e) {
190       // ignore
191     }
192   }
193 
194   /**
195    * Constructor
196    * @param table
197    * @throws IOException
198    */
199   public TableResource(String table) throws IOException {
200     super();
201     this.table = table;
202     // Scanning the table schema is too expensive to do for every operation.
203     // Do it once per minute by default.
204     // Setting hbase.rest.transform.check.interval to <= 0 disables rescanning.
205     long now = System.currentTimeMillis();
206     Long lastChecked = lastCheckedMap.get(table);
207     if (lastChecked != null) {
208       long interval = servlet.getConfiguration()
209         .getLong("hbase.rest.transform.check.interval", 60000);
210       if (interval > 0 && (now - lastChecked.longValue()) > interval) {
211         scanTransformAttrs();
212         lastCheckedMap.put(table, now);
213       }
214     } else {
215       scanTransformAttrs();
216       lastCheckedMap.put(table, now);
217     }
218   }
219 
220   /** @return the table name */
221   String getName() {
222     return table;
223   }
224 
225   /**
226    * @return true if the table exists
227    * @throws IOException
228    */
229   boolean exists() throws IOException {
230     HBaseAdmin admin = new HBaseAdmin(servlet.getConfiguration());
231     return admin.tableExists(table);
232   }
233 
234   /**
235    * Apply any configured transformations to the value
236    * @param family
237    * @param qualifier
238    * @param value
239    * @param direction
240    * @return
241    * @throws IOException
242    */
243   byte[] transform(byte[] family, byte[] qualifier, byte[] value,
244       Transform.Direction direction) throws IOException {
245     Transform t = getTransform(table, family, qualifier);
246     if (t != null) {
247       return t.transform(value, direction);
248     }
249     return value;
250   }
251 
252   @Path("exists")
253   public ExistsResource getExistsResource() throws IOException {
254     return new ExistsResource(this);
255   }
256 
257   @Path("regions")
258   public RegionsResource getRegionsResource() throws IOException {
259     return new RegionsResource(this);
260   }
261 
262   @Path("scanner")
263   public ScannerResource getScannerResource() throws IOException {
264     return new ScannerResource(this);
265   }
266 
267   @Path("schema")
268   public SchemaResource getSchemaResource() throws IOException {
269     return new SchemaResource(this);
270   }
271 
272   @Path("{rowspec: .+}")
273   public RowResource getRowResource(
274       // We need the @Encoded decorator so Jersey won't urldecode before
275       // the RowSpec constructor has a chance to parse
276       final @PathParam("rowspec") @Encoded String rowspec,
277       final @QueryParam("v") String versions) throws IOException {
278     return new RowResource(this, rowspec, versions);
279   }
280 }