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