added TableOption enums, MapBuilder

This commit is contained in:
Matthew Adams
2013-11-18 17:48:30 -06:00
parent 8045e5893b
commit 4729f94071
9 changed files with 642 additions and 63 deletions

View File

@@ -25,12 +25,12 @@ import com.datastax.driver.core.DataType;
public class ColumnBuilder {
/**
* Default ordering of primary key fields is {@link Ordering#ASCENDING}.
* Default ordering of primary key fields; value is {@link Ordering#ASCENDING}.
*/
public static final Ordering DFAULT_ORDERING = ASCENDING;
private String name;
private DataType type;
private DataType type; // TODO: determining if we should be coupling this to Datastax Java Driver type?
private KeyType keyType;
private Ordering ordering;

View File

@@ -8,4 +8,4 @@ public class CqlBuilder {
public static CreateTableBuilder createTable() {
return new CreateTableBuilder();
}
}
}

View File

@@ -9,7 +9,7 @@ import static org.springframework.data.cassandra.mapping.KeyType.PRIMARY;
import static org.springframework.data.cassandra.mapping.Ordering.ASCENDING;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -29,7 +29,7 @@ public class CreateTableBuilder {
private boolean ifNotExists = false;
private String name;
private List<ColumnBuilder> columns = new ArrayList<ColumnBuilder>();
private Map<String, Object> options = new HashMap<String, Object>();
private Map<String, Object> options = new LinkedHashMap<String, Object>();
/**
* Causes the inclusion of an <code>IF NOT EXISTS</code> clause.
@@ -65,40 +65,27 @@ public class CreateTableBuilder {
}
/**
* Adds the given single-string option with no value to this table's options. Convenient overload of
* <code>with(string, null, false, false)</code>.
* Convenience method that calls <code>with(option, null)</code>.
*
* @param singleStringOption
* @return
* @return this
*/
public CreateTableBuilder with(String string) {
return with(string, null, false, false);
public CreateTableBuilder with(TableOption option) {
return with(option, null);
}
/**
* Adds the given single-quote-escaped then single-quoted option value by name to this table's options. Convenient
* overload of <code>with(name, value, true, true)</code>
* Sets the given table option. This is a convenience method that calls
* {@link #with(String, Object, boolean, boolean)} appropriately from the given {@link TableOption} and value for that
* option.
*
* @see #with(String, Object, boolean, boolean)
* @param option The option to set.
* @param value The value of the option. Must be type-compatible with the {@link TableOption}.
* @return this
*/
public CreateTableBuilder withQuoted(String name, Object value) {
return with(name, value, true, true);
}
/**
* Adds the given option value by name with no quoting or escaping to this table's options. Convenient overload of
* <code>with(name, value, false, false)</code>
*
* @see #with(String, Object, boolean, boolean)
* @return this
*/
public CreateTableBuilder withUnquoted(String name, Object value) {
return with(name, value, false, false);
}
public CreateTableBuilder with(String name, Map<String, Object> valueMap) {
return with(name, valueMap, false, false);
public CreateTableBuilder with(TableOption option, Object value) {
option.checkValue(value);
return with(option.getName(), value, option.escapesValue(), option.quotesValue());
}
/**
@@ -152,22 +139,12 @@ public class CreateTableBuilder {
return this;
}
/**
* Convenient method that calls <code>with("COMPACT STORAGE", null)</code>.
*
* @see #with(String, Object)
* @return this
*/
public CreateTableBuilder withCompactStorage() {
return with("COMPACT STORAGE");
}
protected List<ColumnBuilder> columns() {
return columns == null ? columns = new ArrayList<ColumnBuilder>() : columns;
}
protected Map<String, Object> options() {
return options == null ? options = new HashMap<String, Object>() : options;
return options == null ? options = new LinkedHashMap<String, Object>() : options;
}
public String toCql() {
@@ -269,10 +246,11 @@ public class CreateTableBuilder {
// begin options
// begin option clause
if (clustering != null || !options.isEmpty()) {
// option preamble
first = true;
cql.append(" WITH ");
// end option preamble
if (clustering != null) {
cql.append(clustering);
@@ -297,24 +275,32 @@ public class CreateTableBuilder {
cql.append(" = ");
Map<String, Object> valueMap = null;
if ((value instanceof Map) && !(valueMap = (Map<String, Object>) value).isEmpty()) {
Map<Option, Object> valueMap = null;
if ((value instanceof Map) && !(valueMap = (Map<Option, Object>) value).isEmpty()) {
// then option value is a non-empty map
// append { 'name' : 'value', ... }
cql.append("{ ");
boolean mapFirst = true;
for (Map.Entry<String, Object> entry : valueMap.entrySet()) {
for (Map.Entry<Option, Object> entry : valueMap.entrySet()) {
if (mapFirst) {
mapFirst = false;
} else {
cql.append(", ");
}
cql.append(singleQuote(entry.getKey())); // 'name'
Option option = entry.getKey();
cql.append(singleQuote(option.getName())); // entries in map keys are always quoted
cql.append(" : ");
Object entryValue = entry.getValue();
cql.append(singleQuote(entryValue == null ? "" : entryValue.toString())); // 'value'
entryValue = entryValue == null ? "" : entryValue.toString();
if (option.escapesValue()) {
entryValue = escapeSingle(value);
}
if (option.quotesValue()) {
entryValue = singleQuote(value);
}
cql.append(entryValue);
}
cql.append(" }");

View File

@@ -0,0 +1,142 @@
package org.springframework.data.cassandra.cql.builder;
import static org.springframework.data.cassandra.cql.CqlStringUtils.escapeSingle;
import static org.springframework.data.cassandra.cql.CqlStringUtils.singleQuote;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
/**
* A default implementation of {@link Option} to which {@link Enum} types can delegate, since they can't extend
* anything.
*
* @author Matthew T. Adams
*/
public class DefaultOption implements Option {
private String name;
private Class<?> type;
private boolean requiresValue;
private boolean escapesValue;
private boolean quotesValue;
private HashSet<Object> enumConstants; // HACK for enums only
public DefaultOption(String name, Class<?> type, boolean requiresValue, boolean escapesValue, boolean quotesValue) {
this.name = name;
if (type != null) {
if (type.isInterface() && !(Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type))) {
throw new IllegalArgumentException("given type [" + type.getName() + "] must be a class, Map or Collection");
}
// HACK for enums only
if (type.isEnum()) {
enumConstants = new HashSet<Object>(Arrays.asList(type.getEnumConstants()));
}
}
this.type = type;
this.requiresValue = requiresValue;
this.escapesValue = escapesValue;
this.quotesValue = quotesValue;
}
public boolean isCoerceable(Object value) {
if (value == null) {
return true;
}
// check collections
if (Map.class.isAssignableFrom(type)) {
return Map.class.isAssignableFrom(value.getClass());
}
// check map
if (Collection.class.isAssignableFrom(type)) {
return Collection.class.isAssignableFrom(value.getClass());
}
// check enum
if (type.isEnum()) {
// HACK -- prefer to use Enum.valueOf(type, stringValue), but can't
return enumConstants.contains(value.toString());
}
// check class via String constructor
try {
Constructor<?> ctor = type.getConstructor(String.class);
if (!ctor.isAccessible()) {
ctor.setAccessible(true);
}
ctor.newInstance(value.toString());
return true;
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
} catch (IllegalArgumentException e) {
} catch (InvocationTargetException e) {
} catch (NoSuchMethodException e) {
} catch (SecurityException e) {
}
return false;
}
public Class<?> getType() {
return type;
}
public String getName() {
return name;
}
public boolean takesValue() {
return type != null;
}
public boolean requiresValue() {
return this.requiresValue;
}
public boolean escapesValue() {
return this.escapesValue;
}
public boolean quotesValue() {
return this.quotesValue;
}
public void checkValue(Object value) {
if (takesValue()) {
if (value == null) {
if (requiresValue) {
throw new IllegalArgumentException("Option [" + getName() + "] requires a value");
}
return; // doesn't require a value, so null is ok
}
// else value is not null
if (isCoerceable(value)) {
return;
}
// else value is not coerceable into the expected type
throw new IllegalArgumentException("Option [" + getName() + "] takes value coerceable to type ["
+ getType().getName() + "]");
}
// else this option doesn't take a value
if (value != null) {
throw new IllegalArgumentException("Option [" + getName() + "] takes no value");
}
}
public String toString(Object value) {
if (value == null) {
return null;
}
String string = value.toString();
string = escapesValue ? escapeSingle(string) : string;
string = quotesValue ? singleQuote(string) : string;
return string;
}
@Override
public String toString() {
return getName();
}
}

View File

@@ -0,0 +1,126 @@
package org.springframework.data.cassandra.cql.builder;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Builder for maps, which also conveniently implements {@link Map} via delegation for convenience so you don't have to
* actually {@link #build()} it if you don't want to (or forget).
*
* @author Matthew T. Adams
* @param <K> The key type of the map.
* @param <V> The value type of the map.
*/
public class MapBuilder<K, V> implements Map<K, V> {
/**
* Factory method to construct a new <code>MapBuilder&lt;Object,Object&gt;</code>. Convenient if imported statically.
*/
public static MapBuilder<Object, Object> map() {
return map(Object.class, Object.class);
}
/**
* Factory method to construct a new builder with the given key &amp; value types. Convenient if imported statically.
*/
public static <K, V> MapBuilder<K, V> map(Class<K> keyType, Class<V> valueType) {
return new MapBuilder<K, V>();
}
/**
* Factory method to construct a new builder with a shallow copy of the given map. Convenient if imported statically.
*/
public static <K, V> MapBuilder<K, V> map(Map<K, V> source) {
return new MapBuilder<K, V>(source);
}
private Map<K, V> map;
public MapBuilder() {
this(new LinkedHashMap<K, V>());
}
/**
* Constructs a new instance with a copy of the given map.
*/
public MapBuilder(Map<K, V> source) {
this.map = new LinkedHashMap<K, V>(source);
}
/**
* Adds an entry to this map, then returns <code>this</code>.
*
* @return this
*/
public MapBuilder<K, V> entry(K key, V value) {
map.put(key, value);
return this;
}
/**
* Returns a new map based on the current state of this builder's map.
*
* @return A new Map<K, V> with this builder's map's current content.
*/
public Map<K, V> build() {
return new LinkedHashMap<K, V>(map);
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean containsKey(Object key) {
return map.containsKey(key);
}
public boolean containsValue(Object value) {
return map.containsValue(value);
}
public V get(Object key) {
return map.get(key);
}
public V put(K key, V value) {
return map.put(key, value);
}
public V remove(Object key) {
return map.remove(key);
}
public void putAll(Map<? extends K, ? extends V> m) {
map.putAll(m);
}
public void clear() {
map.clear();
}
public Set<K> keySet() {
return map.keySet();
}
public Collection<V> values() {
return map.values();
}
public Set<java.util.Map.Entry<K, V>> entrySet() {
return map.entrySet();
}
public boolean equals(Object o) {
return map.equals(o);
}
public int hashCode() {
return map.hashCode();
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.data.cassandra.cql.builder;
/**
* Interface to represent option types.
*
* @author Matthew T. Adams
*/
public interface Option {
/**
* The type that values must be able to be coerced into for this option.
*/
Class<?> getType();
/**
* The (usually lower-cased, underscore-separated) name of this table option.
*/
String getName();
/**
* Whether this option takes a value.
*/
boolean takesValue();
/**
* Whether this option should escape single quotes in its value.
*/
boolean escapesValue();
/**
* Whether this option's value should be single-quoted.
*/
boolean quotesValue();
/**
* Whether this option requires a value.
*/
boolean requiresValue();
/**
* Checks that the given value can be coerced into the type given by {@link #getType()}.
*/
void checkValue(Object value);
/**
* Tests whether the given value can be coerced into the type given by {@link #getType()}.
*/
boolean isCoerceable(Object value);
/**
* Renders the given value to a string according to this option's settings. Given <code>null</code>, returns
* <code>null</code>.
*
* @see #escapesValue()
* @see #quotesValue()
*/
String toString(Object value);
}

View File

@@ -0,0 +1,270 @@
package org.springframework.data.cassandra.cql.builder;
import java.util.Map;
/**
* Enumeration that represents all known table options. If a table option is not listed here, but is supported by
* Cassandra, use the method {@link CreateTableBuilder#with(String, Object, boolean, boolean)} to write the raw value.
*
* @author Matthew T. Adams
* @see CompactionOption
* @see CompressionOption
* @see CachingOption
*/
public enum TableOption implements Option {
/**
* <code>comment</code>
*/
COMMENT("comment", String.class, false, true, true),
/**
* <code>COMPACT STORAGE</code>
*/
COMPACT_STORAGE("COMPACT STORAGE", null, false, false, false),
/**
* <code>compaction</code>. Value is a <code>Map&lt;CompactionOption,Object&gt;</code>.
*
* @see CompactionOption
*/
COMPACTION("compaction", Map.class, false, false, false),
/**
* <code>compression</code>. Value is a <code>Map&lt;CompressionOption,Object&gt;</code>.
*
* @see {@link CompressionOption}
*/
COMPRESSION("compression", Map.class, false, false, false),
/**
* <code>replicate_on_write</code>
*/
REPLICATE_ON_WRITE("replicate_on_write", Boolean.class, false, false, false),
/**
* <code>caching</code>
*
* @see CachingOption
*/
CACHING("caching", CachingOption.class, false, false, false),
/**
* <code>bloom_filter_fp_chance</code>
*/
BLOOM_FILTER_FP_CHANCE("bloom_filter_fp_chance", Double.class, false, false, false),
/**
* <code>read_repair_chance</code>
*/
READ_REPAIR_CHANCE("read_repair_chance", Double.class, false, false, false),
/**
* <code>dclocal_read_repair_chance</code>
*/
DCLOCAL_READ_REPAIR_CHANCE("dclocal_read_repair_chance", Double.class, false, false, false),
/**
* <code>gc_grace_seconds</code>
*/
GC_GRACE_SECONDS("gc_grace_seconds", Long.class, false, false, false);
private Option delegate;
private TableOption(String name, Class<?> type, boolean requiresValue, boolean escapesValue, boolean quotesValue) {
this.delegate = new DefaultOption(name, type, requiresValue, escapesValue, quotesValue);
}
public Class<?> getType() {
return delegate.getType();
}
public boolean takesValue() {
return delegate.takesValue();
}
public String getName() {
return delegate.getName();
}
public boolean escapesValue() {
return delegate.escapesValue();
}
public boolean quotesValue() {
return delegate.quotesValue();
}
public boolean requiresValue() {
return delegate.requiresValue();
}
public void checkValue(Object value) {
delegate.checkValue(value);
}
public boolean isCoerceable(Object value) {
return delegate.isCoerceable(value);
}
public String toString() {
return delegate.toString();
}
public String toString(Object value) {
return delegate.toString(value);
}
/**
* Known caching options.
*
* @author Matthew T. Adams
*/
public enum CachingOption {
ALL, KEYS_ONLY, ROWS_ONLY, NONE;
}
/**
* Known compaction options.
*
* @author Matthew T. Adams
*/
public enum CompactionOption implements Option {
/**
* <code>tombstone_threshold</code>
*/
TOMBSTONE_THRESHOLD("tombstone_threshold", Double.class, false, false, false),
/**
* <code>tombstone_compaction_interval</code>
*/
TOMBSTONE_COMPACTION_INTERVAL("tombstone_compaction_interval", Double.class, false, false, false),
/**
* <code>min_sstable_size</code>
*/
MIN_SSTABLE_SIZE("min_sstable_size", Long.class, false, false, false),
/**
* <code>min_threshold</code>
*/
MIN_THRESHOLD("min_threshold", Long.class, false, false, false),
/**
* <code>max_threshold</code>
*/
MAX_THRESHOLD("max_threshold", Long.class, false, false, false),
/**
* <code>bucket_low</code>
*/
BUCKET_LOW("bucket_low", Double.class, false, false, false),
/**
* <code>bucket_high</code>
*/
BUCKET_HIGH("bucket_high", Double.class, false, false, false),
/**
* <code>sstable_size_in_mb</code>
*/
SSTABLE_SIZE_IN_MB("sstable_size_in_mb", Long.class, false, false, false);
private Option delegate;
private CompactionOption(String name, Class<?> type, boolean requiresValue, boolean escapesValue,
boolean quotesValue) {
this.delegate = new DefaultOption(name, type, requiresValue, escapesValue, quotesValue);
}
public Class<?> getType() {
return delegate.getType();
}
public boolean takesValue() {
return delegate.takesValue();
}
public String getName() {
return delegate.getName();
}
public boolean escapesValue() {
return delegate.escapesValue();
}
public boolean quotesValue() {
return delegate.quotesValue();
}
public boolean requiresValue() {
return delegate.requiresValue();
}
public void checkValue(Object value) {
delegate.checkValue(value);
}
public boolean isCoerceable(Object value) {
return delegate.isCoerceable(value);
}
public String toString() {
return delegate.toString();
}
public String toString(Object value) {
return delegate.toString(value);
}
}
/**
* Known compression options.
*
* @author Matthew T. Adams
*/
public enum CompressionOption implements Option {
/**
* <code>sstable_compression</code>
*/
STABLE_COMPRESSION("sstable_compression", String.class, false, false, false),
/**
* <code>chunk_length_kb</code>
*/
CHUNK_LENGTH_KB("chunk_length_kb", Long.class, false, false, false),
/**
* <code>crc_check_chance</code>
*/
CRC_CHECK_CHANCE("crc_check_chance", Double.class, false, false, false);
private Option delegate;
private CompressionOption(String name, Class<?> type, boolean requiresValue, boolean escapesValue,
boolean quotesValue) {
this.delegate = new DefaultOption(name, type, requiresValue, escapesValue, quotesValue);
}
public Class<?> getType() {
return delegate.getType();
}
public boolean takesValue() {
return delegate.takesValue();
}
public String getName() {
return delegate.getName();
}
public boolean escapesValue() {
return delegate.escapesValue();
}
public boolean quotesValue() {
return delegate.quotesValue();
}
public boolean requiresValue() {
return delegate.requiresValue();
}
public void checkValue(Object value) {
delegate.checkValue(value);
}
public boolean isCoerceable(Object value) {
return delegate.isCoerceable(value);
}
public String toString() {
return delegate.toString();
}
public String toString(Object value) {
return delegate.toString(value);
}
}
}

View File

@@ -8,12 +8,12 @@ package org.springframework.data.cassandra.mapping;
public enum KeyType {
/**
* Used for a column that is a primary key that also is or is part of the partition key.
* Used for a column that is a primary key and that also is or is part of the partition key.
*/
PARTITION,
/**
* Use for a primary key column that is not part of the partition key and, therefore, may also be ordered.
* Used for a primary key column that is not part of the partition key.
*/
PRIMARY
}

View File

@@ -1,9 +1,11 @@
package org.springframework.data.cassandra.cql;
import static org.springframework.data.cassandra.cql.builder.CqlBuilder.createTable;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.data.cassandra.cql.builder.MapBuilder.map;
import static org.springframework.data.cassandra.cql.builder.TableOption.BLOOM_FILTER_FP_CHANCE;
import static org.springframework.data.cassandra.cql.builder.TableOption.COMMENT;
import static org.springframework.data.cassandra.cql.builder.TableOption.COMPACTION;
import static org.springframework.data.cassandra.cql.builder.TableOption.CompactionOption.TOMBSTONE_THRESHOLD;
import org.junit.Test;
import org.springframework.data.cassandra.cql.builder.CreateTableBuilder;
@@ -16,25 +18,20 @@ public class CreateTableBuilderTest {
public void createTableTest() {
String name = "mytable";
DataType type0 = DataType.text();
String partition0 = "partitionKey0";
String partKey0 = "partitionKey0";
String partition1 = "partitionKey1";
String primary0 = "primary0";
DataType type1 = DataType.text();
String column1 = "column1";
DataType type2 = DataType.bigint();
String column2 = "column2";
Object value2 = "this is a comment";
String option2 = "comment";
Object value3 = "0.00075";
String option3 = "bloom_filter_fp_chance";
Map<String, Object> value4 = new HashMap<String, Object>();
value4.put("class", "LeveledCompactionStrategy");
String option4 = "compaction";
Object comment = "this is a comment";
Object bloom = "0.00075";
CreateTableBuilder builder = createTable().ifNotExists().name(name).partitionKeyColumn(partition0, type0)
CreateTableBuilder builder = createTable().ifNotExists().name(name).partitionKeyColumn(partKey0, type0)
.partitionKeyColumn(partition1, type0).primaryKeyColumn(primary0, type0).column(column1, type1)
.column(column2, type2).withQuoted(option2, value2).withUnquoted(option3, value3).with(option4, value4)
.withCompactStorage();
.column(column2, type2).with(COMMENT, comment).with(BLOOM_FILTER_FP_CHANCE, bloom)
.with(COMPACTION, map().entry(TOMBSTONE_THRESHOLD, "0.15"));
String cql = builder.toCql();
System.out.println(cql);