diff --git a/src/main/java/org/springframework/data/cassandra/cql/CqlStringUtils.java b/src/main/java/org/springframework/data/cassandra/cql/CqlStringUtils.java
index 6d8ee4a4b..eebaa12d1 100644
--- a/src/main/java/org/springframework/data/cassandra/cql/CqlStringUtils.java
+++ b/src/main/java/org/springframework/data/cassandra/cql/CqlStringUtils.java
@@ -9,29 +9,28 @@ public class CqlStringUtils {
protected static final String DOUBLE_QUOTE = "\"";
protected static final String DOUBLE_DOUBLE_QUOTE = "\"\"";
- /**
- * Helper {@link StringBuilder} factory method. If given a non-null argument, returns that, else returns
- * a new {@link StringBuilder}. Intended to be imported statically by other classes in the builder's fluent API
- * implementation.
- *
- * @param sb
- * @return The given {@link StringBuilder} if not null, else a new one.
- *
- * @author Matthew T. Adams
- */
- public static StringBuilder ensureNotNull(StringBuilder sb) {
+ public static StringBuilder noNull(StringBuilder sb) {
return sb == null ? new StringBuilder() : sb;
}
- public static final String IDENTIFIER_REGEX = "[a-zA-Z0-9_]*";
- public static final Pattern IDENTIFIER_PATTERN = Pattern.compile(IDENTIFIER_REGEX);
+ public static final String UNESCAPED_DOUBLE_QUOTE_REGEX = "TODO";
+ public static final Pattern UNESCAPED_DOUBLE_QUOTE_PATTERN = Pattern.compile(UNESCAPED_DOUBLE_QUOTE_REGEX);
- public static boolean isIdentifier(CharSequence chars) {
- return IDENTIFIER_PATTERN.matcher(chars).matches();
+ public static final String UNQUOTED_IDENTIFIER_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*";
+ public static final Pattern UNQUOTED_IDENTIFIER_PATTERN = Pattern.compile(UNQUOTED_IDENTIFIER_REGEX);
+
+ public static boolean isUnquotedIdentifier(CharSequence chars) {
+ return UNQUOTED_IDENTIFIER_PATTERN.matcher(chars).matches();
}
- public static final String QUOTED_IDENTIFIER_REGEX = "([a-zA-Z0-9_]|'{2}+|\"{2}+)*";
- public static final Pattern QUOTED_IDENTIFIER_PATTERN = Pattern.compile(IDENTIFIER_REGEX);
+ public static void checkUnquotedIdentifier(CharSequence chars) {
+ if (!CqlStringUtils.isUnquotedIdentifier(chars)) {
+ throw new IllegalArgumentException("[" + chars + "] is not a valid CQL identifier");
+ }
+ }
+
+ public static final String QUOTED_IDENTIFIER_REGEX = "[a-zA-Z_]([a-zA-Z0-9_]|\"{2}+)*";
+ public static final Pattern QUOTED_IDENTIFIER_PATTERN = Pattern.compile(QUOTED_IDENTIFIER_REGEX);
public static boolean isQuotedIdentifier(CharSequence chars) {
return QUOTED_IDENTIFIER_PATTERN.matcher(chars).matches();
@@ -43,19 +42,45 @@ public class CqlStringUtils {
}
}
- /**
- * Trims then escapes the given {@link CharSequence}. Given null, returns null.
- */
- public static String scrub(Object thing) {
- return thing == null ? (String) null : escape(thing.toString().trim());
+ public static boolean isIdentifier(CharSequence chars) {
+ return isUnquotedIdentifier(chars) || isQuotedIdentifier(chars);
+ }
+
+ public static void checkIdentifier(CharSequence chars) {
+ if (!CqlStringUtils.isIdentifier(chars)) {
+ throw new IllegalArgumentException("[" + chars + "] is not a valid CQL quoted or unquoted identifier");
+ }
}
/**
- * Doubles single quote characters and doubles double quote characters (' -> '' and " -> ""). Given
- * null, returns null.
+ * Renders the given string as a legal Cassandra identifier.
+ *
+ *
If the given identifier is a legal unquoted identifier, it is returned unchanged.
+ *
If the given identifier is a legal quoted identifier, it is returned encased in double quotes.
+ *
If the given identifier is illegal, an {@link IllegalArgumentException} is thrown.
+ *
*/
- public static String escape(Object thing) {
- return escapeDouble(escapeSingle(thing));
+ public static String identifize(String candidate) {
+
+ checkIdentifier(candidate);
+
+ if (isUnquotedIdentifier(candidate)) {
+ return candidate;
+ }
+ // else it must be quoted
+ return doubleQuote(candidate);
+ }
+
+ /**
+ * Renders the given string as a legal Cassandra string column or table option value, by escaping single quotes and
+ * encasing the result in single quotes. Given null, returns null.
+ */
+ public static String valuize(String candidate) {
+
+ if (candidate == null) {
+ return null;
+ }
+ return singleQuote(escapeSingle(candidate));
}
/**
@@ -69,7 +94,7 @@ public class CqlStringUtils {
* Doubles double quote characters (" -> ""). Given null, returns null.
*/
public static String escapeDouble(Object things) {
- return things == null ? (String) null : things.toString().replace(DOUBLE_QUOTE, DOUBLE_SINGLE_QUOTE);
+ return things == null ? (String) null : things.toString().replace(DOUBLE_QUOTE, DOUBLE_DOUBLE_QUOTE);
}
/**
diff --git a/src/main/java/org/springframework/data/cassandra/cql/builder/AbstractTableBuilder.java b/src/main/java/org/springframework/data/cassandra/cql/builder/AbstractTableBuilder.java
new file mode 100644
index 000000000..3412ee6f6
--- /dev/null
+++ b/src/main/java/org/springframework/data/cassandra/cql/builder/AbstractTableBuilder.java
@@ -0,0 +1,149 @@
+package org.springframework.data.cassandra.cql.builder;
+
+import static org.springframework.data.cassandra.cql.CqlStringUtils.noNull;
+import static org.springframework.data.cassandra.cql.CqlStringUtils.escapeSingle;
+import static org.springframework.data.cassandra.cql.CqlStringUtils.identifize;
+import static org.springframework.data.cassandra.cql.CqlStringUtils.singleQuote;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.data.cassandra.cql.CqlStringUtils;
+
+/**
+ * Base class that contains behavior common to table operations.
+ *
+ * @author Matthew T. Adams
+ * @param T The subtype of AbstractTableBuilder.
+ */
+public abstract class AbstractTableBuilder> {
+
+ protected abstract StringBuilder toCql(StringBuilder cql);
+
+ private String name;
+
+ protected Map options = new LinkedHashMap();
+
+ /**
+ * Sets the table name.
+ *
+ * @return this
+ */
+ @SuppressWarnings("unchecked")
+ public T name(String name) {
+ CqlStringUtils.checkIdentifier(name);
+ this.name = name;
+ return (T) this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getNameAsIdentifier() {
+ return identifize(name);
+ }
+
+ /**
+ * Convenience method that calls with(option, null).
+ *
+ * @return this
+ */
+ public T with(TableOption option) {
+ return with(option, null);
+ }
+
+ /**
+ * 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.
+ *
+ * @param option The option to set.
+ * @param value The value of the option. Must be type-compatible with the {@link TableOption}.
+ * @return this
+ * @see #with(String, Object, boolean, boolean)
+ */
+ public T with(TableOption option, Object value) {
+ option.checkValue(value);
+ return (T) with(option.getName(), value, option.escapesValue(), option.quotesValue());
+ }
+
+ /**
+ * Adds the given option by name to this table's options.
+ *
+ * Options that have null values are considered single string options where the name of the option is the
+ * string to be used. Otherwise, the result of {@link Object#toString()} is considered to be the value of the option
+ * with the given name. The value, after conversion to string, may have embedded single quotes escaped according to
+ * parameter escape and may be single-quoted according to parameter quote.
+ *
+ * @param name The name of the option
+ * @param value The value of the option. If null, the value is ignored and the option is considered to be
+ * composed of only the name, otherwise the value's {@link Object#toString()} value is used.
+ * @param escape Whether to escape the value via {@link CqlStringUtils#escapeSingle(Object)}. Ignored if given value
+ * is an instance of a {@link Map}.
+ * @param quote Whether to quote the value via {@link CqlStringUtils#singleQuote(Object)}. Ignored if given value is
+ * an instance of a {@link Map}.
+ * @return this
+ */
+ @SuppressWarnings("unchecked")
+ public T with(String name, Object value, boolean escape, boolean quote) {
+ if (!(value instanceof Map)) {
+ if (escape) {
+ value = escapeSingle(value);
+ }
+ if (quote) {
+ value = singleQuote(value);
+ }
+ }
+ options().put(name, value);
+ return (T) this;
+ }
+
+ protected Map options() {
+ return options == null ? options = new LinkedHashMap() : options;
+ }
+
+ protected StringBuilder optionValueMap(Map