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