/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.cassandra.io.compress; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.AbstractSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.IVersionedSerializer; public class CompressionParameters { public final static int DEFAULT_CHUNK_LENGTH = 65536; public final static double DEFAULT_CRC_CHECK_CHANCE = 1.0; public final static IVersionedSerializer<CompressionParameters> serializer = new Serializer(); public static final String SSTABLE_COMPRESSION = "sstable_compression"; public static final String CHUNK_LENGTH_KB = "chunk_length_kb"; public static final String CRC_CHECK_CHANCE = "crc_check_chance"; public static final Set<String> GLOBAL_OPTIONS = ImmutableSet.of(CRC_CHECK_CHANCE); public final ICompressor sstableCompressor; private final Integer chunkLength; private volatile double crcCheckChance; public final Map<String, String> otherOptions; // Unrecognized options, can be use by the compressor public static CompressionParameters create(Map<? extends CharSequence, ? extends CharSequence> opts) throws ConfigurationException { Map<String, String> options = copyOptions(opts); String sstableCompressionClass = options.get(SSTABLE_COMPRESSION); String chunkLength = options.get(CHUNK_LENGTH_KB); options.remove(SSTABLE_COMPRESSION); options.remove(CHUNK_LENGTH_KB); CompressionParameters cp = new CompressionParameters(sstableCompressionClass, parseChunkLength(chunkLength), options); cp.validate(); return cp; } public CompressionParameters(String sstableCompressorClass, Integer chunkLength, Map<String, String> otherOptions) throws ConfigurationException { this(createCompressor(parseCompressorClass(sstableCompressorClass), otherOptions), chunkLength, otherOptions); } public CompressionParameters(ICompressor sstableCompressor) { // can't try/catch as first statement in the constructor, thus repeating constructor code here. this.sstableCompressor = sstableCompressor; chunkLength = null; otherOptions = Collections.emptyMap(); crcCheckChance = DEFAULT_CRC_CHECK_CHANCE; } public CompressionParameters(ICompressor sstableCompressor, Integer chunkLength, Map<String, String> otherOptions) throws ConfigurationException { this.sstableCompressor = sstableCompressor; this.chunkLength = chunkLength; this.otherOptions = otherOptions; String chance = otherOptions.get(CRC_CHECK_CHANCE); this.crcCheckChance = (chance == null) ? DEFAULT_CRC_CHECK_CHANCE : parseCrcCheckChance(chance); } public CompressionParameters copy() { try { return new CompressionParameters(sstableCompressor, chunkLength, new HashMap<>(otherOptions)); } catch (ConfigurationException e) { throw new AssertionError(e); // can't happen at this point. } } public void setCrcCheckChance(double crcCheckChance) throws ConfigurationException { validateCrcCheckChance(crcCheckChance); this.crcCheckChance = crcCheckChance; } public double getCrcCheckChance() { return this.crcCheckChance; } private static double parseCrcCheckChance(String crcCheckChance) throws ConfigurationException { try { double chance = Double.parseDouble(crcCheckChance); validateCrcCheckChance(chance); return chance; } catch (NumberFormatException e) { throw new ConfigurationException("crc_check_chance should be a double"); } } private static void validateCrcCheckChance(double crcCheckChance) throws ConfigurationException { if (crcCheckChance < 0.0d || crcCheckChance > 1.0d) throw new ConfigurationException("crc_check_chance should be between 0.0 and 1.0"); } public int chunkLength() { return chunkLength == null ? DEFAULT_CHUNK_LENGTH : chunkLength; } private static Class<? extends ICompressor> parseCompressorClass(String className) throws ConfigurationException { if (className == null || className.isEmpty()) return null; className = className.contains(".") ? className : "org.apache.cassandra.io.compress." + className; try { return (Class<? extends ICompressor>)Class.forName(className); } catch (Exception e) { throw new ConfigurationException("Could not create Compression for type " + className, e); } } private static ICompressor createCompressor(Class<? extends ICompressor> compressorClass, Map<String, String> compressionOptions) throws ConfigurationException { if (compressorClass == null) { if (!compressionOptions.isEmpty()) throw new ConfigurationException("Unknown compression options (" + compressionOptions.keySet() + ") since no compression class found"); return null; } try { Method method = compressorClass.getMethod("create", Map.class); ICompressor compressor = (ICompressor)method.invoke(null, compressionOptions); // Check for unknown options AbstractSet<String> supportedOpts = Sets.union(compressor.supportedOptions(), GLOBAL_OPTIONS); for (String provided : compressionOptions.keySet()) if (!supportedOpts.contains(provided)) throw new ConfigurationException("Unknown compression options " + provided); return compressor; } catch (NoSuchMethodException e) { throw new ConfigurationException("create method not found", e); } catch (SecurityException e) { throw new ConfigurationException("Access forbiden", e); } catch (IllegalAccessException e) { throw new ConfigurationException("Cannot access method create in " + compressorClass.getName(), e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); throw new ConfigurationException(String.format("%s.create() threw an error: %s", compressorClass.getSimpleName(), cause == null ? e.getClass().getName() + " " + e.getMessage() : cause.getClass().getName() + " " + cause.getMessage()), e); } catch (ExceptionInInitializerError e) { throw new ConfigurationException("Cannot initialize class " + compressorClass.getName()); } } private static Map<String, String> copyOptions(Map<? extends CharSequence, ? extends CharSequence> co) { if (co == null || co.isEmpty()) return Collections.<String, String>emptyMap(); Map<String, String> compressionOptions = new HashMap<String, String>(); for (Map.Entry<? extends CharSequence, ? extends CharSequence> entry : co.entrySet()) { compressionOptions.put(entry.getKey().toString(), entry.getValue().toString()); } return compressionOptions; } /** * Parse the chunk length (in KB) and returns it as bytes. */ public static Integer parseChunkLength(String chLengthKB) throws ConfigurationException { if (chLengthKB == null) return null; try { int parsed = Integer.parseInt(chLengthKB); if (parsed > Integer.MAX_VALUE / 1024) throw new ConfigurationException("Value of " + CHUNK_LENGTH_KB + " is too large (" + parsed + ")"); return 1024 * parsed; } catch (NumberFormatException e) { throw new ConfigurationException("Invalid value for " + CHUNK_LENGTH_KB, e); } } // chunkLength must be a power of 2 because we assume so when // computing the chunk number from an uncompressed file offset (see // CompressedRandomAccessReader.decompresseChunk()) public void validate() throws ConfigurationException { // if chunk length was not set (chunkLength == null), this is fine, default will be used if (chunkLength != null) { if (chunkLength <= 0) throw new ConfigurationException("Invalid negative or null " + CHUNK_LENGTH_KB); int c = chunkLength; boolean found = false; while (c != 0) { if ((c & 0x01) != 0) { if (found) throw new ConfigurationException(CHUNK_LENGTH_KB + " must be a power of 2"); else found = true; } c >>= 1; } } validateCrcCheckChance(crcCheckChance); } public Map<String, String> asThriftOptions() { Map<String, String> options = new HashMap<String, String>(otherOptions); if (sstableCompressor == null) return options; options.put(SSTABLE_COMPRESSION, sstableCompressor.getClass().getName()); if (chunkLength != null) options.put(CHUNK_LENGTH_KB, chunkLengthInKB()); return options; } private String chunkLengthInKB() { return String.valueOf(chunkLength() / 1024); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (obj == null || obj.getClass() != getClass()) { return false; } CompressionParameters cp = (CompressionParameters) obj; return new EqualsBuilder() .append(sstableCompressor, cp.sstableCompressor) .append(chunkLength, cp.chunkLength) .append(otherOptions, cp.otherOptions) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(29, 1597) .append(sstableCompressor) .append(chunkLength) .append(otherOptions) .toHashCode(); } static class Serializer implements IVersionedSerializer<CompressionParameters> { public void serialize(CompressionParameters parameters, DataOutput out, int version) throws IOException { out.writeUTF(parameters.sstableCompressor.getClass().getSimpleName()); out.writeInt(parameters.otherOptions.size()); for (Map.Entry<String, String> entry : parameters.otherOptions.entrySet()) { out.writeUTF(entry.getKey()); out.writeUTF(entry.getValue()); } out.writeInt(parameters.chunkLength()); } public CompressionParameters deserialize(DataInput in, int version) throws IOException { String compressorName = in.readUTF(); int optionCount = in.readInt(); Map<String, String> options = new HashMap<String, String>(); for (int i = 0; i < optionCount; ++i) { String key = in.readUTF(); String value = in.readUTF(); options.put(key, value); } int chunkLength = in.readInt(); CompressionParameters parameters; try { parameters = new CompressionParameters(compressorName, chunkLength, options); } catch (ConfigurationException e) { throw new RuntimeException("Cannot create CompressionParameters for parameters", e); } return parameters; } public long serializedSize(CompressionParameters parameters, int version) { long size = TypeSizes.NATIVE.sizeof(parameters.sstableCompressor.getClass().getSimpleName()); size += TypeSizes.NATIVE.sizeof(parameters.otherOptions.size()); for (Map.Entry<String, String> entry : parameters.otherOptions.entrySet()) { size += TypeSizes.NATIVE.sizeof(entry.getKey()); size += TypeSizes.NATIVE.sizeof(entry.getValue()); } size += TypeSizes.NATIVE.sizeof(parameters.chunkLength()); return size; } } }