diff --git a/src/main/java/net/ucanaccess/util/Sql.java b/src/main/java/net/ucanaccess/util/Sql.java new file mode 100644 index 00000000..d6a86227 --- /dev/null +++ b/src/main/java/net/ucanaccess/util/Sql.java @@ -0,0 +1,115 @@ +package net.ucanaccess.util; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + *

+ * An immutable sql statement string created from multiple tokens + * in order to write inline sql statements in an easy-to-read fashion + * spread out over multiple lines of code. + *

+ * + *

+ * The class implements {@link CharSequence} and thus can be used as a drop-in + * alternative wherever API supports {@code CharSequence} rather than {@code String}. + *

+ * + * Please note that the validity of the statement is never checked, + * and that {@code null} or empty inputs are permitted (no run-time exceptions).
+ * + * The input of multiple tokens is formatted into a single String by + * removing leading and trailing whitespace and concatenating + * non-empty tokens by a single space character. + * Further, any trailing semicolons are removed from the resulting sql string. + * + *

Example:

+ * + *
+ *     String tblName = "table";
+ *     Sql.of("SELECT COUNT(*)",
+ *            "FROM", tblName,
+ *            " WHERE cond1 = :cond1",
+ *            "   AND cond2 = :cond2");
+ * 
+ * + * @author Markus Spann + * @since v5.1.0 + */ +public final class Sql implements CharSequence { + + private static final Sql EMPTY_SQL = new Sql(""); + + /** The internal sql string. Cannot be null. */ + private final String str; + + private Sql(String sql) { + str = sql; + } + + public static Sql of(CharSequence... _tokens) { + return _tokens == null ? EMPTY_SQL : of(Arrays.asList(_tokens)); + } + + public static Sql of(Iterable _tokens) { + return _tokens == null ? EMPTY_SQL : new Sql(format(_tokens)); + } + + /** + * Formats an sql statement from multiple tokens.
+ * Leading and trailing whitespace is removed from each token and empty tokens ignored.
+ * The tokens are joined using a single blank character to create the sql string.
+ * Finally, any trailing semicolons are removed from the resulting sql. + * + * @param _tokens collection of tokens + * @return formatted sql string + */ + static String format(Iterable _tokens) { + String sql = StreamSupport.stream(Objects.requireNonNull(_tokens).spliterator(), false) + .filter(Objects::nonNull) + .map(CharSequence::toString) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(" ")); + while (sql.endsWith(";")) { + sql = sql.substring(0, sql.length() - 1); + } + return sql; + } + + @Override + public int length() { + return str.length(); + } + + @Override + public char charAt(int _index) { + return str.charAt(_index); + } + + @Override + public CharSequence subSequence(int _start, int _end) { + return str.subSequence(_start, _end); + } + + @Override + public int hashCode() { + return str.hashCode(); + } + + @Override + public boolean equals(Object _obj) { + if (_obj == null || getClass() != _obj.getClass()) { + return false; + } + return str.equals(((Sql) _obj).str); + } + + @Override + public String toString() { + return str; + } + +} diff --git a/src/test/java/net/ucanaccess/util/SqlTest.java b/src/test/java/net/ucanaccess/util/SqlTest.java new file mode 100644 index 00000000..47169dbb --- /dev/null +++ b/src/test/java/net/ucanaccess/util/SqlTest.java @@ -0,0 +1,69 @@ +package net.ucanaccess.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.ucanaccess.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class SqlTest extends AbstractBaseTest { + + @Test + void testEmptyInput() { + assertThat(Sql.of((CharSequence[]) null).toString()).isBlank(); + assertThat(Sql.of((CharSequence) null).toString()).isBlank(); + assertThat(Sql.of((Collection) null).toString()).isBlank(); + assertThat(Sql.of(new CharSequence[0]).toString()).isBlank(); + assertThat(Sql.of(new ArrayList<>()).toString()).isBlank(); + assertThat(Sql.of().toString()).isBlank(); + } + + @Test + void testMultipleStrings() { + assertThat(Sql.of(List.of("A", "B", "C"))) + .hasToString("A B C"); + assertThat(Sql.of("A", "B", "C")) + .hasToString("A B C"); + } + + @Test + void testMultipleTokens() { + assertThat(Sql.of("SELECT COUNT(*) FROM table", + "WHERE cond1 = :cond1; ")) + .hasToString("SELECT COUNT(*) FROM table WHERE cond1 = :cond1"); + } + + @ParameterizedTest + @ValueSource(strings = {" ", "\n", "\t", ";"}) + @NullAndEmptySource + void testStrip(String str) { + Sql sql = Sql.of(str); + assertThat(sql).hasToString(""); + } + + @Test + void testCharSequenceMethods() { + Sql sql = Sql.of("SELECT * FROM table"); + assertThat(sql).hasSize(19); + assertThat(sql.charAt(0)).isEqualTo('S'); + assertThat(sql.subSequence(0, 3)).isEqualTo("SEL"); + } + + @Test + void testEqualsAndHashCode() { + Sql sql1 = Sql.of("DROP TABLE table;"); + Sql sql2 = Sql.of(sql1); + assertThat(sql1).isNotNull() + .isEqualTo(sql2) + .hasSameHashCodeAs(sql2) + .isNotEqualTo(Sql.of()); + assertThat(sql1.toString()).isNotBlank(); + } + +}