View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.constraint;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.DataOutputStream;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Comparator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Map.Entry;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.hbase.HTableDescriptor;
36  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
37  import org.apache.hadoop.hbase.util.Bytes;
38  import org.apache.hadoop.hbase.util.Pair;
39  
40  /**
41   * Utilities for adding/removing constraints from a table.
42   * <p>
43   * Constraints can be added on table load time, via the {@link HTableDescriptor}.
44   * <p>
45   * NOTE: this class is NOT thread safe. Concurrent setting/enabling/disabling of
46   * constraints can cause constraints to be run at incorrect times or not at all.
47   */
48  public final class Constraints {
49    private static final int DEFAULT_PRIORITY = -1;
50  
51    private Constraints() {
52    }
53  
54    private static final Log LOG = LogFactory.getLog(Constraints.class);
55    private static final String CONSTRAINT_HTD_KEY_PREFIX = "constraint $";
56    private static final Pattern CONSTRAINT_HTD_ATTR_KEY_PATTERN = Pattern
57        .compile(CONSTRAINT_HTD_KEY_PREFIX, Pattern.LITERAL);
58  
59    // configuration key for if the constraint is enabled
60    private static final String ENABLED_KEY = "_ENABLED";
61    // configuration key for the priority
62    private static final String PRIORITY_KEY = "_PRIORITY";
63  
64    // smallest priority a constraiNt can have
65    private static final long MIN_PRIORITY = 0L;
66    // ensure a priority less than the smallest we could intentionally set
67    private static final long UNSET_PRIORITY = MIN_PRIORITY - 1;
68  
69    private static String COUNTER_KEY = "hbase.constraint.counter";
70  
71    /**
72     * Enable constraints on a table.
73     * <p>
74     * Currently, if you attempt to add a constraint to the table, then
75     * Constraints will automatically be turned on.
76     * 
77     * @param desc
78     *          table description to add the processor
79     * @throws IOException
80     *           If the {@link ConstraintProcessor} CP couldn't be added to the
81     *           table.
82     */
83    public static void enable(HTableDescriptor desc) throws IOException {
84      // if the CP has already been loaded, do nothing
85      String clazz = ConstraintProcessor.class.getName();
86      if (desc.hasCoprocessor(clazz)) {
87        return;
88      }
89  
90      // add the constrain processor CP to the table
91      desc.addCoprocessor(clazz);
92    }
93  
94    /**
95     * Turn off processing constraints for a given table, even if constraints have
96     * been turned on or added.
97     * 
98     * @param desc
99     *          {@link HTableDescriptor} where to disable {@link Constraint
100    *          Constraints}.
101    */
102   public static void disable(HTableDescriptor desc) {
103     desc.removeCoprocessor(ConstraintProcessor.class.getName());
104   }
105 
106   /**
107    * Remove all {@link Constraint Constraints} that have been added to the table
108    * and turn off the constraint processing.
109    * <p>
110    * All {@link Configuration Configurations} and their associated
111    * {@link Constraint} are removed.
112    * 
113    * @param desc
114    *          {@link HTableDescriptor} to remove {@link Constraint Constraints}
115    *          from.
116    */
117   public static void remove(HTableDescriptor desc) {
118     // disable constraints
119     disable(desc);
120 
121     // remove all the constraint settings
122     List<ImmutableBytesWritable> keys = new ArrayList<ImmutableBytesWritable>();
123     // loop through all the key, values looking for constraints
124     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e : desc
125         .getValues().entrySet()) {
126       String key = Bytes.toString((e.getKey().get()));
127       String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
128       if (className.length == 2) {
129         keys.add(e.getKey());
130       }
131     }
132     // now remove all the keys we found
133     for (ImmutableBytesWritable key : keys) {
134       desc.remove(key.get());
135     }
136   }
137 
138   /**
139    * Check to see if the Constraint is currently set.
140    * 
141    * @param desc
142    *          {@link HTableDescriptor} to check
143    * @param clazz
144    *          {@link Constraint} class to check for.
145    * @return <tt>true</tt> if the {@link Constraint} is present, even if it is
146    *         disabled. <tt>false</tt> otherwise.
147    */
148   public static boolean has(HTableDescriptor desc,
149       Class<? extends Constraint> clazz) {
150     return getKeyValueForClass(desc, clazz) != null;
151   }
152 
153   /**
154    * Get the kv {@link Entry} in the descriptor for the specified class
155    * 
156    * @param desc
157    *          {@link HTableDescriptor} to read
158    * @param clazz
159    *          to search for
160    * @return the {@link Pair} of <key, value> in the table, if that class is
161    *         present. <tt>null</tt> otherwise.
162    */
163   private static Pair<String, String> getKeyValueForClass(
164       HTableDescriptor desc, Class<? extends Constraint> clazz) {
165     // get the serialized version of the constraint
166     String key = serializeConstraintClass(clazz);
167     String value = desc.getValue(key);
168 
169     return value == null ? null : new Pair<String, String>(key, value);
170   }
171 
172   /**
173    * Add configuration-less constraints to the table.
174    * <p>
175    * This will overwrite any configuration associated with the previous
176    * constraint of the same class.
177    * <p>
178    * Each constraint, when added to the table, will have a specific priority,
179    * dictating the order in which the {@link Constraint} will be run. A
180    * {@link Constraint} earlier in the list will be run before those later in
181    * the list. The same logic applies between two Constraints over time (earlier
182    * added is run first on the regionserver).
183    * 
184    * @param desc
185    *          {@link HTableDescriptor} to add {@link Constraint Constraints}
186    * @param constraints
187    *          {@link Constraint Constraints} to add. All constraints are
188    *          considered automatically enabled on add
189    * @throws IOException
190    *           If constraint could not be serialized/added to table
191    */
192   public static void add(HTableDescriptor desc,
193       Class<? extends Constraint>... constraints) throws IOException {
194     // make sure constraints are enabled
195     enable(desc);
196     long priority = getNextPriority(desc);
197 
198     // store each constraint
199     for (Class<? extends Constraint> clazz : constraints) {
200       addConstraint(desc, clazz, null, priority++);
201     }
202     updateLatestPriority(desc, priority);
203   }
204 
205   /**
206    * Add constraints and their associated configurations to the table.
207    * <p>
208    * Adding the same constraint class twice will overwrite the first
209    * constraint's configuration
210    * <p>
211    * Each constraint, when added to the table, will have a specific priority,
212    * dictating the order in which the {@link Constraint} will be run. A
213    * {@link Constraint} earlier in the list will be run before those later in
214    * the list. The same logic applies between two Constraints over time (earlier
215    * added is run first on the regionserver).
216    * 
217    * @param desc
218    *          {@link HTableDescriptor} to add a {@link Constraint}
219    * @param constraints
220    *          {@link Pair} of a {@link Constraint} and its associated
221    *          {@link Configuration}. The Constraint will be configured on load
222    *          with the specified configuration.All constraints are considered
223    *          automatically enabled on add
224    * @throws IOException
225    *           if any constraint could not be deserialized. Assumes if 1
226    *           constraint is not loaded properly, something has gone terribly
227    *           wrong and that all constraints need to be enforced.
228    */
229   public static void add(HTableDescriptor desc,
230       Pair<Class<? extends Constraint>, Configuration>... constraints)
231       throws IOException {
232     enable(desc);
233     long priority = getNextPriority(desc);
234     for (Pair<Class<? extends Constraint>, Configuration> pair : constraints) {
235       addConstraint(desc, pair.getFirst(), pair.getSecond(), priority++);
236     }
237     updateLatestPriority(desc, priority);
238   }
239 
240   /**
241    * Add a {@link Constraint} to the table with the given configuration
242    * <p>
243    * Each constraint, when added to the table, will have a specific priority,
244    * dictating the order in which the {@link Constraint} will be run. A
245    * {@link Constraint} added will run on the regionserver before those added to
246    * the {@link HTableDescriptor} later.
247    * 
248    * @param desc
249    *          table descriptor to the constraint to
250    * @param constraint
251    *          to be added
252    * @param conf
253    *          configuration associated with the constraint
254    * @throws IOException
255    *           if any constraint could not be deserialized. Assumes if 1
256    *           constraint is not loaded properly, something has gone terribly
257    *           wrong and that all constraints need to be enforced.
258    */
259   public static void add(HTableDescriptor desc,
260       Class<? extends Constraint> constraint, Configuration conf)
261       throws IOException {
262     enable(desc);
263     long priority = getNextPriority(desc);
264     addConstraint(desc, constraint, conf, priority++);
265 
266     updateLatestPriority(desc, priority);
267   }
268 
269   /**
270    * Write the raw constraint and configuration to the descriptor.
271    * <p>
272    * This method takes care of creating a new configuration based on the passed
273    * in configuration and then updating that with enabled and priority of the
274    * constraint.
275    * <p>
276    * When a constraint is added, it is automatically enabled.
277    */
278   private static void addConstraint(HTableDescriptor desc,
279       Class<? extends Constraint> clazz, Configuration conf, long priority)
280       throws IOException {
281     writeConstraint(desc, serializeConstraintClass(clazz),
282         configure(conf, true, priority));
283   }
284 
285   /**
286    * Setup the configuration for a constraint as to whether it is enabled and
287    * its priority
288    * 
289    * @param conf
290    *          on which to base the new configuration
291    * @param enabled
292    *          <tt>true</tt> if it should be run
293    * @param priority
294    *          relative to other constraints
295    * @returns a new configuration, storable in the {@link HTableDescriptor}
296    */
297   private static Configuration configure(Configuration conf, boolean enabled,
298       long priority) {
299     // create the configuration to actually be stored
300     // clone if possible, but otherwise just create an empty configuration
301     Configuration toWrite = conf == null ? new Configuration()
302         : new Configuration(conf);
303 
304     // update internal properties
305     toWrite.setBooleanIfUnset(ENABLED_KEY, enabled);
306 
307     // set if unset long
308     if (toWrite.getLong(PRIORITY_KEY, UNSET_PRIORITY) == UNSET_PRIORITY) {
309       toWrite.setLong(PRIORITY_KEY, priority);
310     }
311 
312     return toWrite;
313   }
314 
315   /**
316    * Just write the class to a String representation of the class as a key for
317    * the {@link HTableDescriptor}
318    * 
319    * @param clazz
320    *          Constraint class to convert to a {@link HTableDescriptor} key
321    * @return key to store in the {@link HTableDescriptor}
322    */
323   private static String serializeConstraintClass(
324       Class<? extends Constraint> clazz) {
325     String constraintClazz = clazz.getName();
326     return CONSTRAINT_HTD_KEY_PREFIX + constraintClazz;
327   }
328 
329   /**
330    * Write the given key and associated configuration to the
331    * {@link HTableDescriptor}
332    */
333   private static void writeConstraint(HTableDescriptor desc, String key,
334       Configuration conf) throws IOException {
335     // store the key and conf in the descriptor
336     desc.setValue(key, serializeConfiguration(conf));
337   }
338 
339   /**
340    * Write the configuration to a String
341    * 
342    * @param conf
343    *          to write
344    * @return String representation of that configuration
345    * @throws IOException
346    */
347   private static String serializeConfiguration(Configuration conf)
348       throws IOException {
349     // write the configuration out to the data stream
350     ByteArrayOutputStream bos = new ByteArrayOutputStream();
351     DataOutputStream dos = new DataOutputStream(bos);
352     conf.writeXml(dos);
353     dos.flush();
354     byte[] data = bos.toByteArray();
355     return Bytes.toString(data);
356   }
357 
358   /**
359    * Read the {@link Configuration} stored in the byte stream.
360    * 
361    * @param bytes
362    *          to read from
363    * @return A valid configuration
364    */
365   private static Configuration readConfiguration(byte[] bytes)
366       throws IOException {
367     ByteArrayInputStream is = new ByteArrayInputStream(bytes);
368     Configuration conf = new Configuration(false);
369     conf.addResource(is);
370     return conf;
371   }
372 
373   /**
374    * Read in the configuration from the String encoded configuration
375    * 
376    * @param bytes
377    *          to read from
378    * @return A valid configuration
379    * @throws IOException
380    *           if the configuration could not be read
381    */
382   private static Configuration readConfiguration(String bytes)
383       throws IOException {
384     return readConfiguration(Bytes.toBytes(bytes));
385   }
386 
387   private static long getNextPriority(HTableDescriptor desc) {
388     String value = desc.getValue(COUNTER_KEY);
389 
390     long priority;
391     // get the current priority
392     if (value == null) {
393       priority = MIN_PRIORITY;
394     } else {
395       priority = Long.parseLong(value) + 1;
396     }
397 
398     return priority;
399   }
400 
401   private static void updateLatestPriority(HTableDescriptor desc, long priority) {
402     // update the max priority
403     desc.setValue(COUNTER_KEY, Long.toString(priority));
404   }
405 
406   /**
407    * Update the configuration for the {@link Constraint}; does not change the
408    * order in which the constraint is run.
409    * 
410    * @param desc
411    *          {@link HTableDescriptor} to update
412    * @param clazz
413    *          {@link Constraint} to update
414    * @param configuration
415    *          to update the {@link Constraint} with.
416    * @throws IOException
417    *           if the Constraint was not stored correctly
418    * @throws IllegalArgumentException
419    *           if the Constraint was not present on this table.
420    */
421   public static void setConfiguration(HTableDescriptor desc,
422       Class<? extends Constraint> clazz, Configuration configuration)
423       throws IOException, IllegalArgumentException {
424     // get the entry for this class
425     Pair<String, String> e = getKeyValueForClass(desc, clazz);
426 
427     if (e == null) {
428       throw new IllegalArgumentException("Constraint: " + clazz.getName()
429           + " is not associated with this table.");
430     }
431 
432     // clone over the configuration elements
433     Configuration conf = new Configuration(configuration);
434 
435     // read in the previous info about the constraint
436     Configuration internal = readConfiguration(e.getSecond());
437 
438     // update the fields based on the previous settings
439     conf.setIfUnset(ENABLED_KEY, internal.get(ENABLED_KEY));
440     conf.setIfUnset(PRIORITY_KEY, internal.get(PRIORITY_KEY));
441 
442     // update the current value
443     writeConstraint(desc, e.getFirst(), conf);
444   }
445 
446   /**
447    * Remove the constraint (and associated information) for the table
448    * descriptor.
449    * 
450    * @param desc
451    *          {@link HTableDescriptor} to modify
452    * @param clazz
453    *          {@link Constraint} class to remove
454    */
455   public static void remove(HTableDescriptor desc,
456       Class<? extends Constraint> clazz) {
457     String key = serializeConstraintClass(clazz);
458     desc.remove(key);
459   }
460 
461   /**
462    * Enable the given {@link Constraint}. Retains all the information (e.g.
463    * Configuration) for the {@link Constraint}, but makes sure that it gets
464    * loaded on the table.
465    * 
466    * @param desc
467    *          {@link HTableDescriptor} to modify
468    * @param clazz
469    *          {@link Constraint} to enable
470    * @throws IOException
471    *           If the constraint cannot be properly deserialized
472    */
473   public static void enableConstraint(HTableDescriptor desc,
474       Class<? extends Constraint> clazz) throws IOException {
475     changeConstraintEnabled(desc, clazz, true);
476   }
477 
478   /**
479    * Disable the given {@link Constraint}. Retains all the information (e.g.
480    * Configuration) for the {@link Constraint}, but it just doesn't load the
481    * {@link Constraint} on the table.
482    * 
483    * @param desc
484    *          {@link HTableDescriptor} to modify
485    * @param clazz
486    *          {@link Constraint} to disable.
487    * @throws IOException
488    *           if the constraint cannot be found
489    */
490   public static void disableConstraint(HTableDescriptor desc,
491       Class<? extends Constraint> clazz) throws IOException {
492     changeConstraintEnabled(desc, clazz, false);
493   }
494 
495   /**
496    * Change the whether the constraint (if it is already present) is enabled or
497    * disabled.
498    */
499   private static void changeConstraintEnabled(HTableDescriptor desc,
500       Class<? extends Constraint> clazz, boolean enabled) throws IOException {
501     // get the original constraint
502     Pair<String, String> entry = getKeyValueForClass(desc, clazz);
503     if (entry == null) {
504       throw new IllegalArgumentException("Constraint: " + clazz.getName()
505           + " is not associated with this table. You can't enable it!");
506     }
507 
508     // create a new configuration from that conf
509     Configuration conf = readConfiguration(entry.getSecond());
510 
511     // set that it is enabled
512     conf.setBoolean(ENABLED_KEY, enabled);
513 
514     // write it back out
515     writeConstraint(desc, entry.getFirst(), conf);
516   }
517 
518   /**
519    * Check to see if the given constraint is enabled.
520    * 
521    * @param desc
522    *          {@link HTableDescriptor} to check.
523    * @param clazz
524    *          {@link Constraint} to check for
525    * @return <tt>true</tt> if the {@link Constraint} is present and enabled.
526    *         <tt>false</tt> otherwise.
527    * @throws IOException
528    *           If the constraint has improperly stored in the table
529    */
530   public static boolean enabled(HTableDescriptor desc,
531       Class<? extends Constraint> clazz) throws IOException {
532     // get the kv
533     Pair<String, String> entry = getKeyValueForClass(desc, clazz);
534     // its not enabled so just return false. In fact, its not even present!
535     if (entry == null) {
536       return false;
537     }
538 
539     // get the info about the constraint
540     Configuration conf = readConfiguration(entry.getSecond());
541 
542     return conf.getBoolean(ENABLED_KEY, false);
543   }
544 
545   /**
546    * Get the constraints stored in the table descriptor
547    * 
548    * @param desc
549    *          To read from
550    * @param classloader
551    *          To use when loading classes. If a special classloader is used on a
552    *          region, for instance, then that should be the classloader used to
553    *          load the constraints. This could also apply to unit-testing
554    *          situation, where want to ensure that class is reloaded or not.
555    * @return List of configured {@link Constraint Constraints}
556    * @throws IOException
557    *           if any part of reading/arguments fails
558    */
559   static List<? extends Constraint> getConstraints(HTableDescriptor desc,
560       ClassLoader classloader) throws IOException {
561     List<Constraint> constraints = new ArrayList<Constraint>();
562     // loop through all the key, values looking for constraints
563     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e : desc
564         .getValues().entrySet()) {
565       // read out the constraint
566       String key = Bytes.toString(e.getKey().get()).trim();
567       String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
568       if (className.length == 2) {
569         key = className[1];
570         if (LOG.isDebugEnabled()) {
571           LOG.debug("Loading constraint:" + key);
572         }
573 
574         // read in the rest of the constraint
575         Configuration conf;
576         try {
577           conf = readConfiguration(e.getValue().get());
578         } catch (IOException e1) {
579           // long that we don't have a valid configuration stored, and move on.
580           LOG.warn("Corrupted configuration found for key:" + key
581               + ",  skipping it.");
582           continue;
583         }
584         // if it is not enabled, skip it
585         if (!conf.getBoolean(ENABLED_KEY, false)) {
586           if (LOG.isDebugEnabled())
587             LOG.debug("Constraint: " + key + " is DISABLED - skipping it");
588           // go to the next constraint
589           continue;
590         }
591 
592         try {
593           // add the constraint, now that we expect it to be valid.
594           Class<? extends Constraint> clazz = classloader.loadClass(key)
595               .asSubclass(Constraint.class);
596           Constraint constraint = clazz.newInstance();
597           constraint.setConf(conf);
598           constraints.add(constraint);
599         } catch (ClassNotFoundException e1) {
600           throw new IOException(e1);
601         } catch (InstantiationException e1) {
602           throw new IOException(e1);
603         } catch (IllegalAccessException e1) {
604           throw new IOException(e1);
605         }
606       }
607     }
608     // sort them, based on the priorities
609     Collections.sort(constraints, constraintComparator);
610     return constraints;
611   }
612 
613   private static final Comparator<Constraint> constraintComparator = new Comparator<Constraint>() {
614     @Override
615     public int compare(Constraint c1, Constraint c2) {
616       // compare the priorities of the constraints stored in their configuration
617       return Long.valueOf(c1.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY))
618           .compareTo(c2.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY));
619     }
620   };
621 
622 }