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.constraint;
019
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.DataOutputStream;
023import java.io.IOException;
024import java.lang.reflect.InvocationTargetException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.regex.Pattern;
032
033import org.apache.yetus.audience.InterfaceAudience;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.HTableDescriptor;
038import org.apache.hadoop.hbase.client.TableDescriptor;
039import org.apache.hadoop.hbase.util.Bytes;
040import org.apache.hadoop.hbase.util.Pair;
041
042/**
043 * Utilities for adding/removing constraints from a table.
044 * <p>
045 * Constraints can be added on table load time, via the {@link HTableDescriptor}.
046 * <p>
047 * NOTE: this class is NOT thread safe. Concurrent setting/enabling/disabling of
048 * constraints can cause constraints to be run at incorrect times or not at all.
049 */
050@InterfaceAudience.Private
051public final class Constraints {
052  private static final int DEFAULT_PRIORITY = -1;
053
054  private Constraints() {
055  }
056
057  private static final Logger LOG = LoggerFactory.getLogger(Constraints.class);
058  private static final String CONSTRAINT_HTD_KEY_PREFIX = "constraint $";
059  private static final Pattern CONSTRAINT_HTD_ATTR_KEY_PATTERN = Pattern
060      .compile(CONSTRAINT_HTD_KEY_PREFIX, Pattern.LITERAL);
061
062  // configuration key for if the constraint is enabled
063  private static final String ENABLED_KEY = "_ENABLED";
064  // configuration key for the priority
065  private static final String PRIORITY_KEY = "_PRIORITY";
066
067  // smallest priority a constraiNt can have
068  private static final long MIN_PRIORITY = 0L;
069  // ensure a priority less than the smallest we could intentionally set
070  private static final long UNSET_PRIORITY = MIN_PRIORITY - 1;
071
072  private static String COUNTER_KEY = "hbase.constraint.counter";
073
074  /**
075   * Enable constraints on a table.
076   * <p>
077   * Currently, if you attempt to add a constraint to the table, then
078   * Constraints will automatically be turned on.
079   * 
080   * @param desc
081   *          table description to add the processor
082   * @throws IOException
083   *           If the {@link ConstraintProcessor} CP couldn't be added to the
084   *           table.
085   */
086  public static void enable(HTableDescriptor desc) throws IOException {
087    // if the CP has already been loaded, do nothing
088    String clazz = ConstraintProcessor.class.getName();
089    if (desc.hasCoprocessor(clazz)) {
090      return;
091    }
092
093    // add the constrain processor CP to the table
094    desc.addCoprocessor(clazz);
095  }
096
097  /**
098   * Turn off processing constraints for a given table, even if constraints have
099   * been turned on or added.
100   * 
101   * @param desc
102   *          {@link HTableDescriptor} where to disable {@link Constraint
103   *          Constraints}.
104   */
105  public static void disable(HTableDescriptor desc) {
106    desc.removeCoprocessor(ConstraintProcessor.class.getName());
107  }
108
109  /**
110   * Remove all {@link Constraint Constraints} that have been added to the table
111   * and turn off the constraint processing.
112   * <p>
113   * All {@link Configuration Configurations} and their associated
114   * {@link Constraint} are removed.
115   * 
116   * @param desc
117   *          {@link HTableDescriptor} to remove {@link Constraint Constraints}
118   *          from.
119   */
120  public static void remove(HTableDescriptor desc) {
121    // disable constraints
122    disable(desc);
123
124    // remove all the constraint settings
125    List<Bytes> keys = new ArrayList<>();
126    // loop through all the key, values looking for constraints
127    for (Map.Entry<Bytes, Bytes> e : desc
128        .getValues().entrySet()) {
129      String key = Bytes.toString((e.getKey().get()));
130      String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
131      if (className.length == 2) {
132        keys.add(e.getKey());
133      }
134    }
135    // now remove all the keys we found
136    for (Bytes key : keys) {
137      desc.remove(key);
138    }
139  }
140
141  /**
142   * Check to see if the Constraint is currently set.
143   * 
144   * @param desc
145   *          {@link HTableDescriptor} to check
146   * @param clazz
147   *          {@link Constraint} class to check for.
148   * @return <tt>true</tt> if the {@link Constraint} is present, even if it is
149   *         disabled. <tt>false</tt> otherwise.
150   */
151  public static boolean has(HTableDescriptor desc,
152      Class<? extends Constraint> clazz) {
153    return getKeyValueForClass(desc, clazz) != null;
154  }
155
156  /**
157   * Get the kv {@link Entry} in the descriptor for the specified class
158   * 
159   * @param desc {@link HTableDescriptor} to read
160   * @param clazz To search for
161   * @return The {@link Pair} of {@literal <key, value>} in the table, if that class is
162   *         present. {@code NULL} otherwise.
163   */
164  private static Pair<String, String> getKeyValueForClass(
165      HTableDescriptor desc, Class<? extends Constraint> clazz) {
166    // get the serialized version of the constraint
167    String key = serializeConstraintClass(clazz);
168    String value = desc.getValue(key);
169
170    return value == null ? null : new Pair<>(key, value);
171  }
172
173  /**
174   * Add configuration-less constraints to the table.
175   * <p>
176   * This will overwrite any configuration associated with the previous
177   * constraint of the same class.
178   * <p>
179   * Each constraint, when added to the table, will have a specific priority,
180   * dictating the order in which the {@link Constraint} will be run. A
181   * {@link Constraint} earlier in the list will be run before those later in
182   * the list. The same logic applies between two Constraints over time (earlier
183   * added is run first on the regionserver).
184   * 
185   * @param desc
186   *          {@link HTableDescriptor} to add {@link Constraint Constraints}
187   * @param constraints
188   *          {@link Constraint Constraints} to add. All constraints are
189   *          considered automatically enabled on add
190   * @throws IOException
191   *           If constraint could not be serialized/added to table
192   */
193  public static void add(HTableDescriptor desc,
194      Class<? extends Constraint>... constraints) throws IOException {
195    // make sure constraints are enabled
196    enable(desc);
197    long priority = getNextPriority(desc);
198
199    // store each constraint
200    for (Class<? extends Constraint> clazz : constraints) {
201      addConstraint(desc, clazz, null, priority++);
202    }
203    updateLatestPriority(desc, priority);
204  }
205
206  /**
207   * Add constraints and their associated configurations to the table.
208   * <p>
209   * Adding the same constraint class twice will overwrite the first
210   * constraint's configuration
211   * <p>
212   * Each constraint, when added to the table, will have a specific priority,
213   * dictating the order in which the {@link Constraint} will be run. A
214   * {@link Constraint} earlier in the list will be run before those later in
215   * the list. The same logic applies between two Constraints over time (earlier
216   * added is run first on the regionserver).
217   * 
218   * @param desc
219   *          {@link HTableDescriptor} to add a {@link Constraint}
220   * @param constraints
221   *          {@link Pair} of a {@link Constraint} and its associated
222   *          {@link Configuration}. The Constraint will be configured on load
223   *          with the specified configuration.All constraints are considered
224   *          automatically enabled on add
225   * @throws IOException
226   *           if any constraint could not be deserialized. Assumes if 1
227   *           constraint is not loaded properly, something has gone terribly
228   *           wrong and that all constraints need to be enforced.
229   */
230  public static void add(HTableDescriptor desc,
231      Pair<Class<? extends Constraint>, Configuration>... constraints)
232      throws IOException {
233    enable(desc);
234    long priority = getNextPriority(desc);
235    for (Pair<Class<? extends Constraint>, Configuration> pair : constraints) {
236      addConstraint(desc, pair.getFirst(), pair.getSecond(), priority++);
237    }
238    updateLatestPriority(desc, priority);
239  }
240
241  /**
242   * Add a {@link Constraint} to the table with the given configuration
243   * <p>
244   * Each constraint, when added to the table, will have a specific priority,
245   * dictating the order in which the {@link Constraint} will be run. A
246   * {@link Constraint} added will run on the regionserver before those added to
247   * the {@link HTableDescriptor} later.
248   * 
249   * @param desc
250   *          table descriptor to the constraint to
251   * @param constraint
252   *          to be added
253   * @param conf
254   *          configuration associated with the constraint
255   * @throws IOException
256   *           if any constraint could not be deserialized. Assumes if 1
257   *           constraint is not loaded properly, something has gone terribly
258   *           wrong and that all constraints need to be enforced.
259   */
260  public static void add(HTableDescriptor desc,
261      Class<? extends Constraint> constraint, Configuration conf)
262      throws IOException {
263    enable(desc);
264    long priority = getNextPriority(desc);
265    addConstraint(desc, constraint, conf, priority++);
266
267    updateLatestPriority(desc, priority);
268  }
269
270  /**
271   * Write the raw constraint and configuration to the descriptor.
272   * <p>
273   * This method takes care of creating a new configuration based on the passed
274   * in configuration and then updating that with enabled and priority of the
275   * constraint.
276   * <p>
277   * When a constraint is added, it is automatically enabled.
278   */
279  private static void addConstraint(HTableDescriptor desc,
280      Class<? extends Constraint> clazz, Configuration conf, long priority)
281      throws IOException {
282    writeConstraint(desc, serializeConstraintClass(clazz),
283        configure(conf, true, priority));
284  }
285
286  /**
287   * Setup the configuration for a constraint as to whether it is enabled and
288   * its priority
289   * 
290   * @param conf
291   *          on which to base the new configuration
292   * @param enabled
293   *          <tt>true</tt> if it should be run
294   * @param priority
295   *          relative to other constraints
296   * @return a new configuration, storable in the {@link HTableDescriptor}
297   */
298  private static Configuration configure(Configuration conf, boolean enabled,
299      long priority) {
300    // create the configuration to actually be stored
301    // clone if possible, but otherwise just create an empty configuration
302    Configuration toWrite = conf == null ? new Configuration()
303        : new Configuration(conf);
304
305    // update internal properties
306    toWrite.setBooleanIfUnset(ENABLED_KEY, enabled);
307
308    // set if unset long
309    if (toWrite.getLong(PRIORITY_KEY, UNSET_PRIORITY) == UNSET_PRIORITY) {
310      toWrite.setLong(PRIORITY_KEY, priority);
311    }
312
313    return toWrite;
314  }
315
316  /**
317   * Just write the class to a String representation of the class as a key for
318   * the {@link HTableDescriptor}
319   * 
320   * @param clazz
321   *          Constraint class to convert to a {@link HTableDescriptor} key
322   * @return key to store in the {@link HTableDescriptor}
323   */
324  private static String serializeConstraintClass(
325      Class<? extends Constraint> clazz) {
326    String constraintClazz = clazz.getName();
327    return CONSTRAINT_HTD_KEY_PREFIX + constraintClazz;
328  }
329
330  /**
331   * Write the given key and associated configuration to the
332   * {@link HTableDescriptor}
333   */
334  private static void writeConstraint(HTableDescriptor desc, String key,
335      Configuration conf) throws IOException {
336    // store the key and conf in the descriptor
337    desc.setValue(key, serializeConfiguration(conf));
338  }
339
340  /**
341   * Write the configuration to a String
342   * 
343   * @param conf
344   *          to write
345   * @return String representation of that configuration
346   * @throws IOException
347   */
348  private static String serializeConfiguration(Configuration conf)
349      throws IOException {
350    // write the configuration out to the data stream
351    ByteArrayOutputStream bos = new ByteArrayOutputStream();
352    DataOutputStream dos = new DataOutputStream(bos);
353    conf.writeXml(dos);
354    dos.flush();
355    byte[] data = bos.toByteArray();
356    return Bytes.toString(data);
357  }
358
359  /**
360   * Read the {@link Configuration} stored in the byte stream.
361   * 
362   * @param bytes
363   *          to read from
364   * @return A valid configuration
365   */
366  private static Configuration readConfiguration(byte[] bytes)
367      throws IOException {
368    ByteArrayInputStream is = new ByteArrayInputStream(bytes);
369    Configuration conf = new Configuration(false);
370    conf.addResource(is);
371    return conf;
372  }
373
374  /**
375   * Read in the configuration from the String encoded configuration
376   * 
377   * @param bytes
378   *          to read from
379   * @return A valid configuration
380   * @throws IOException
381   *           if the configuration could not be read
382   */
383  private static Configuration readConfiguration(String bytes)
384      throws IOException {
385    return readConfiguration(Bytes.toBytes(bytes));
386  }
387
388  private static long getNextPriority(HTableDescriptor desc) {
389    String value = desc.getValue(COUNTER_KEY);
390
391    long priority;
392    // get the current priority
393    if (value == null) {
394      priority = MIN_PRIORITY;
395    } else {
396      priority = Long.parseLong(value) + 1;
397    }
398
399    return priority;
400  }
401
402  private static void updateLatestPriority(HTableDescriptor desc, long priority) {
403    // update the max priority
404    desc.setValue(COUNTER_KEY, Long.toString(priority));
405  }
406
407  /**
408   * Update the configuration for the {@link Constraint}; does not change the
409   * order in which the constraint is run.
410   * 
411   * @param desc
412   *          {@link HTableDescriptor} to update
413   * @param clazz
414   *          {@link Constraint} to update
415   * @param configuration
416   *          to update the {@link Constraint} with.
417   * @throws IOException
418   *           if the Constraint was not stored correctly
419   * @throws IllegalArgumentException
420   *           if the Constraint was not present on this table.
421   */
422  public static void setConfiguration(HTableDescriptor desc,
423      Class<? extends Constraint> clazz, Configuration configuration)
424      throws IOException, IllegalArgumentException {
425    // get the entry for this class
426    Pair<String, String> e = getKeyValueForClass(desc, clazz);
427
428    if (e == null) {
429      throw new IllegalArgumentException("Constraint: " + clazz.getName()
430          + " is not associated with this table.");
431    }
432
433    // clone over the configuration elements
434    Configuration conf = new Configuration(configuration);
435
436    // read in the previous info about the constraint
437    Configuration internal = readConfiguration(e.getSecond());
438
439    // update the fields based on the previous settings
440    conf.setIfUnset(ENABLED_KEY, internal.get(ENABLED_KEY));
441    conf.setIfUnset(PRIORITY_KEY, internal.get(PRIORITY_KEY));
442
443    // update the current value
444    writeConstraint(desc, e.getFirst(), conf);
445  }
446
447  /**
448   * Remove the constraint (and associated information) for the table
449   * descriptor.
450   * 
451   * @param desc
452   *          {@link HTableDescriptor} to modify
453   * @param clazz
454   *          {@link Constraint} class to remove
455   */
456  public static void remove(HTableDescriptor desc,
457      Class<? extends Constraint> clazz) {
458    String key = serializeConstraintClass(clazz);
459    desc.remove(key);
460  }
461
462  /**
463   * Enable the given {@link Constraint}. Retains all the information (e.g.
464   * Configuration) for the {@link Constraint}, but makes sure that it gets
465   * loaded on the table.
466   * 
467   * @param desc
468   *          {@link HTableDescriptor} to modify
469   * @param clazz
470   *          {@link Constraint} to enable
471   * @throws IOException
472   *           If the constraint cannot be properly deserialized
473   */
474  public static void enableConstraint(HTableDescriptor desc,
475      Class<? extends Constraint> clazz) throws IOException {
476    changeConstraintEnabled(desc, clazz, true);
477  }
478
479  /**
480   * Disable the given {@link Constraint}. Retains all the information (e.g.
481   * Configuration) for the {@link Constraint}, but it just doesn't load the
482   * {@link Constraint} on the table.
483   * 
484   * @param desc
485   *          {@link HTableDescriptor} to modify
486   * @param clazz
487   *          {@link Constraint} to disable.
488   * @throws IOException
489   *           if the constraint cannot be found
490   */
491  public static void disableConstraint(HTableDescriptor desc,
492      Class<? extends Constraint> clazz) throws IOException {
493    changeConstraintEnabled(desc, clazz, false);
494  }
495
496  /**
497   * Change the whether the constraint (if it is already present) is enabled or
498   * disabled.
499   */
500  private static void changeConstraintEnabled(HTableDescriptor desc,
501      Class<? extends Constraint> clazz, boolean enabled) throws IOException {
502    // get the original constraint
503    Pair<String, String> entry = getKeyValueForClass(desc, clazz);
504    if (entry == null) {
505      throw new IllegalArgumentException("Constraint: " + clazz.getName()
506          + " is not associated with this table. You can't enable it!");
507    }
508
509    // create a new configuration from that conf
510    Configuration conf = readConfiguration(entry.getSecond());
511
512    // set that it is enabled
513    conf.setBoolean(ENABLED_KEY, enabled);
514
515    // write it back out
516    writeConstraint(desc, entry.getFirst(), conf);
517  }
518
519  /**
520   * Check to see if the given constraint is enabled.
521   * 
522   * @param desc
523   *          {@link HTableDescriptor} to check.
524   * @param clazz
525   *          {@link Constraint} to check for
526   * @return <tt>true</tt> if the {@link Constraint} is present and enabled.
527   *         <tt>false</tt> otherwise.
528   * @throws IOException
529   *           If the constraint has improperly stored in the table
530   */
531  public static boolean enabled(HTableDescriptor desc,
532      Class<? extends Constraint> clazz) throws IOException {
533    // get the kv
534    Pair<String, String> entry = getKeyValueForClass(desc, clazz);
535    // its not enabled so just return false. In fact, its not even present!
536    if (entry == null) {
537      return false;
538    }
539
540    // get the info about the constraint
541    Configuration conf = readConfiguration(entry.getSecond());
542
543    return conf.getBoolean(ENABLED_KEY, false);
544  }
545
546  /**
547   * Get the constraints stored in the table descriptor
548   * 
549   * @param desc
550   *          To read from
551   * @param classloader
552   *          To use when loading classes. If a special classloader is used on a
553   *          region, for instance, then that should be the classloader used to
554   *          load the constraints. This could also apply to unit-testing
555   *          situation, where want to ensure that class is reloaded or not.
556   * @return List of configured {@link Constraint Constraints}
557   * @throws IOException
558   *           if any part of reading/arguments fails
559   */
560  static List<? extends Constraint> getConstraints(TableDescriptor desc,
561      ClassLoader classloader) throws IOException {
562    List<Constraint> constraints = new ArrayList<>();
563    // loop through all the key, values looking for constraints
564    for (Map.Entry<Bytes, Bytes> e : desc
565        .getValues().entrySet()) {
566      // read out the constraint
567      String key = Bytes.toString(e.getKey().get()).trim();
568      String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
569      if (className.length == 2) {
570        key = className[1];
571        if (LOG.isDebugEnabled()) {
572          LOG.debug("Loading constraint:" + key);
573        }
574
575        // read in the rest of the constraint
576        Configuration conf;
577        try {
578          conf = readConfiguration(e.getValue().get());
579        } catch (IOException e1) {
580          // long that we don't have a valid configuration stored, and move on.
581          LOG.warn("Corrupted configuration found for key:" + key
582              + ",  skipping it.");
583          continue;
584        }
585        // if it is not enabled, skip it
586        if (!conf.getBoolean(ENABLED_KEY, false)) {
587          if (LOG.isDebugEnabled())
588            LOG.debug("Constraint: " + key + " is DISABLED - skipping it");
589          // go to the next constraint
590          continue;
591        }
592
593        try {
594          // add the constraint, now that we expect it to be valid.
595          Class<? extends Constraint> clazz = classloader.loadClass(key)
596              .asSubclass(Constraint.class);
597          Constraint constraint = clazz.getDeclaredConstructor().newInstance();
598          constraint.setConf(conf);
599          constraints.add(constraint);
600        } catch (InvocationTargetException | NoSuchMethodException | ClassNotFoundException |
601            InstantiationException | IllegalAccessException e1) {
602          throw new IOException(e1);
603        }
604      }
605    }
606    // sort them, based on the priorities
607    Collections.sort(constraints, constraintComparator);
608    return constraints;
609  }
610
611  private static final Comparator<Constraint> constraintComparator = new Comparator<Constraint>() {
612    @Override
613    public int compare(Constraint c1, Constraint c2) {
614      // compare the priorities of the constraints stored in their configuration
615      return Long.compare(c1.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY),
616          c2.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY));
617    }
618  };
619
620}