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