aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/xenoworld/ormpaloompa/Table.java346
-rw-r--r--src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java52
-rw-r--r--src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java16
-rw-r--r--src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java12
-rw-r--r--src/main/java/overview.adoc18
5 files changed, 444 insertions, 0 deletions
diff --git a/src/main/java/de/xenoworld/ormpaloompa/Table.java b/src/main/java/de/xenoworld/ormpaloompa/Table.java
new file mode 100644
index 0000000..96497b6
--- /dev/null
+++ b/src/main/java/de/xenoworld/ormpaloompa/Table.java
@@ -0,0 +1,346 @@
+package de.xenoworld.ormpaloompa;
+
+import de.xenoworld.ormpaloompa.annotations.Field;
+import de.xenoworld.ormpaloompa.annotations.TableInfo;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+// TODO: Filter out fields where the value is null
+// TODO: Filter out the identity fields when updating
+// (insert identities only when they are not null → auto IDs, but things like the term-database do
+// have a string as key)
+// TODO: Use more Optionals and get rid of exception throwing where possible
+// Name: Ormpaloompa
+/**
+ * A SQLite table of a POJO `T` as a proxy.
+ *
+ * This makes writing and retrieving simple things from the SQLite database
+ * easier.
+ *
+ * It takes a simple class with some annotations such as:
+ *
+ * [source,java]
+ * ----
+ * class Bird {
+ * @Field(identity = true)
+ * public Integer id;
+ *
+ * @Field
+ * public String name;
+ * }
+ * ----
+ *
+ * and makes it possible to insert and query for objects:
+ *
+ * [source,java]
+ * ----
+ * Table<Bird> t = new Table<>(Bird.class, someDatabaseConnection);
+ * Bird newBird = new Bird();
+ * newBird.name = "A new bird";
+ * Object id = table.insert(newBird);
+ * Bird storedBird = t.getById(id).get();
+ * ----
+ *
+ * @param <T>
+ */
+public class Table<T> {
+ private final Class<T> tableClass;
+ private final Connection connection;
+ private final String tableName;
+
+ private final TableField identity;
+ private ArrayList<TableField> tableFields;
+
+ private enum Query {
+ COUNT("SELECT COUNT(*) FROM %s"),
+ GET_BY_ID("SELECT * FROM %s WHERE %s = ?"),
+ INSERT("INSERT INTO %s (%s) VALUES (%s)"),
+ WHERE("SELECT * FROM %s WHERE %s"),
+ UPDATE_BY_ID("UPDATE %s SET %s = ? WHERE %s = ?"),;
+
+ final String query;
+
+ Query(String query) {
+ this.query = query;
+ }
+
+ @Override
+ public String toString() {
+ return this.query;
+ }
+ }
+
+
+ private class TableField {
+ final String name;
+ final String dflt;
+ final boolean identity;
+
+ private TableField(String name, String dflt, boolean identity) {
+ this.name = name;
+ this.dflt = dflt;
+ this.identity = identity;
+ }
+
+ @Override
+ public String toString() {
+ return "TableField{" +
+ "name='" + name + '\'' +
+ ", dflt='" + dflt + '\'' +
+ ", identity=" + identity +
+ '}';
+ }
+ }
+
+ public Table(Class<T> tableClass, Connection connection) {
+ this.tableClass = tableClass;
+ this.connection = connection;
+
+ tableFields = new ArrayList<>();
+
+ Stream.of(tableClass.getFields())
+ .map(field -> Pair.of(field.getName(), field.getAnnotation(Field.class)))
+ .forEach(p -> tableFields.add(
+ new TableField(
+ p.getRight().name().isEmpty() ? p.getLeft() : p.getRight().name(),
+ p.getRight().dflt(),
+ p.getRight().identity())));
+
+ this.tableName = discoverTableName(tableClass);
+ this.identity = discoverIdentity(tableClass);
+ }
+
+ // TODO: This is kind of an idiotic contraption
+ private TableField discoverIdentity(Class<T> tableClass) {
+ return tableFields.stream()
+ .filter(f -> f.identity)
+ .findFirst()
+ .orElseGet(() -> tableFields.stream()
+ .filter(f -> f.name.toLowerCase().equals("id"))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException(
+ String.format("%s: No identity given, and no field 'id' present.",
+ tableClass.getName()
+ )
+ )
+ ));
+ }
+
+ /**
+ * Get name for table from TableInfo annotation or append "s" to the lower
+ * case table name.
+ *
+ * @param tableClass table to inspect
+ * @return name of table in database
+ */
+ private String discoverTableName(Class<T> tableClass) {
+ TableInfo tableInfo = tableClass.getAnnotation(TableInfo.class);
+ if (tableInfo != null && !tableInfo.tableName().isEmpty()) {
+ return tableInfo.tableName();
+ }
+
+ return tableClass.getSimpleName().toLowerCase() + "s";
+ }
+
+ /**
+ * Return the number of rows in the table, uses `COUNT(*)`.
+ *
+ * @return total number of rows
+ * @throws SQLException
+ */
+ public Integer count() throws SQLException {
+ String query = String.format(Query.COUNT.toString(), tableName);
+ PreparedStatement stmt = connection.prepareStatement(query);
+ ResultSet result = stmt.executeQuery();
+ result.next();
+ return result.getInt(1);
+ }
+
+ /**
+ * Get a `T` by Identity.
+ *
+ * @param id identity
+ * @return `Optional<T>`, `empty()` if no object could be found, or if an
+ * error occurred.
+ */
+ public Optional<T> getById(Object id) {
+ String query = String.format(Query.GET_BY_ID.toString(), tableName, identity.name);
+
+ try {
+ PreparedStatement stmt = connection.prepareStatement(query);
+ stmt.setObject(1, id);
+ ResultSet result = stmt.executeQuery();
+
+ if (!result.next()) {
+ return Optional.empty();
+ }
+
+ T t = fromResultSet(result);
+
+ return Optional.of(t);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Return matching rows.
+ *
+ * This will return a `Stream` of `T`.
+ *
+ * The optional `args` parameter an be used in situations where the
+ * WhereQuery needs parameters, for example:
+ *
+ * [source,java]
+ * ----
+ * find(where("foo = ?").limit(1), "bar")
+ * ----
+ *
+ * This will create a query for `… WHERE foo = bar …`.
+ *
+ * @param whereQuery a query
+ * @param args (optional) arguments to WHERE clause
+ * @return a Stream of matching `T`
+ */
+ public Stream<T> find(WhereQuery whereQuery, Object... args) {
+ // TODO: Make try-mess better
+ try {
+ String query = String.format(Query.WHERE.toString(), tableName, whereQuery.toString());
+ PreparedStatement stmt = connection.prepareStatement(query);
+
+ for (int i = 0; i < args.length; i++) {
+ stmt.setObject(i + 1, args[i]);
+ }
+
+ ResultSet resultSet = stmt.executeQuery();
+
+ Stream.Builder<T> tBuilder = Stream.builder();
+
+ while (resultSet.next()) {
+ tBuilder.add(fromResultSet(resultSet));
+ }
+ return tBuilder.build();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return Stream.empty();
+ }
+ }
+
+ private T fromResultSet(ResultSet result) throws InstantiationException, IllegalAccessException {
+ T t = tableClass.newInstance();
+
+ tableFields.forEach(f -> {
+ try {
+ t.getClass().getField(f.name).set(t, result.getObject(f.name));
+ } catch (IllegalAccessException | NoSuchFieldException | SQLException e) {
+ e.printStackTrace();
+ }
+ });
+ return t;
+ }
+
+ public Long insert(T t) throws SQLException {
+ String fields = tableFields
+ .stream()
+ .map(f -> f.name)
+ .collect(Collectors.joining(","));
+
+ StringBuffer placeholder = new StringBuffer("?");
+
+ IntStream.range(0, tableFields.size() - 1).forEach(i -> placeholder.append(", ?"));
+
+ String query = String.format(Query.INSERT.toString(), tableName, fields, placeholder);
+ PreparedStatement stmt = connection.prepareStatement(query);
+
+ fillStatementFromObject(t, stmt);
+
+ stmt.execute();
+
+ return stmt.getGeneratedKeys().getLong(1);
+ }
+
+ public void update(T t) {
+ String fields = tableFields
+ .stream()
+ .map(f -> f.name)
+ .collect(Collectors.joining(" = ?, "));
+
+ String query = String.format(Query.UPDATE_BY_ID.toString(), tableName, fields, identity.name);
+
+ try {
+ PreparedStatement stmt = connection.prepareStatement(query);
+ int order = fillStatementFromObject(t, stmt);
+ stmt.setObject(order, t.getClass().getField(identity.name).get(t));
+ stmt.execute();
+ } catch (SQLException | NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private int fillStatementFromObject(T t, PreparedStatement stmt) {
+ final int[] order = {1};
+
+ tableFields.stream().forEach(f -> {
+ try {
+ Object o = t.getClass().getField(f.name).get(t);
+ stmt.setObject(order[0]++, o);
+ } catch (IllegalAccessException | NoSuchFieldException | SQLException e) {
+ e.printStackTrace();
+ }
+ });
+
+ return order[0];
+ }
+
+ /**
+ * Take `t` and insert a new entry, or update an existing entry.
+ *
+ * @param t
+ * @return Optional of insert Id (a Long)
+ * @throws NoSuchFieldException
+ * @throws IllegalAccessException
+ * @throws SQLException
+ */
+ public Optional<Object> updateOrInsert(T t) throws NoSuchFieldException, IllegalAccessException, SQLException {
+ Optional<T> existingEntry = Optional.empty();
+ Optional<Object> idValue = retrieveIdValue(t);
+
+ if (idValue.isPresent()) {
+ existingEntry = getById(idValue.get());
+ }
+
+ if (existingEntry.isPresent()) {
+ update(t);
+ return Optional.empty();
+ } else {
+ return Optional.of(insert(t));
+ }
+ }
+
+ /**
+ * Retrieve the current value of the `@Identity` field.
+ *
+ * Will return an empty Optional if the value is null, or if there was
+ * an Exception.
+ *
+ * @param t object to retrieve identity value from
+ * @return value of identity, or empty on null or error
+ */
+ public Optional<Object> retrieveIdValue(T t) {
+ try {
+ return Optional.ofNullable(t.getClass().getField(identity.name).get(t));
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java b/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java
new file mode 100644
index 0000000..fcc42eb
--- /dev/null
+++ b/src/main/java/de/xenoworld/ormpaloompa/WhereQuery.java
@@ -0,0 +1,52 @@
+package de.xenoworld.ormpaloompa;
+
+import java.util.Optional;
+
+public class WhereQuery {
+ private Optional<Integer> limit = Optional.empty();
+ private Optional<String> orderFields = Optional.empty();
+ private Order order;
+ private String where;
+
+ public enum Order {
+ DESC,
+ ASC
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer query = new StringBuffer();
+
+ query.append(where);
+
+ orderFields.ifPresent(
+ o -> query.append(String.format(" ORDER BY %s %s", o, order.toString()))
+ );
+
+ limit.ifPresent(
+ i -> query.append(String.format(" LIMIT %d", i))
+ );
+
+ return query.toString();
+ }
+
+ public WhereQuery(String where) {
+ this.where = where;
+ }
+
+ public WhereQuery limit(Integer limit) {
+ this.limit = Optional.of(limit);
+ return this;
+ }
+
+ public WhereQuery orderBy(String fields, Order order) {
+ orderFields = Optional.of(fields);
+ this.order = order;
+ return this;
+ }
+
+ public static WhereQuery where(String where) {
+ return new WhereQuery(where);
+ }
+
+}
diff --git a/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java b/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java
new file mode 100644
index 0000000..5cd917b
--- /dev/null
+++ b/src/main/java/de/xenoworld/ormpaloompa/annotations/Field.java
@@ -0,0 +1,16 @@
+package de.xenoworld.ormpaloompa.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Annotate a public field.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Field {
+ String name() default "";
+
+ String dflt() default "";
+
+ boolean identity() default false;
+} \ No newline at end of file
diff --git a/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java b/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java
new file mode 100644
index 0000000..5094c5d
--- /dev/null
+++ b/src/main/java/de/xenoworld/ormpaloompa/annotations/TableInfo.java
@@ -0,0 +1,12 @@
+package de.xenoworld.ormpaloompa.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Optional table annotation, can be used to set the table name in the database
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TableInfo {
+ String tableName() default "";
+}
diff --git a/src/main/java/overview.adoc b/src/main/java/overview.adoc
new file mode 100644
index 0000000..90a82f6
--- /dev/null
+++ b/src/main/java/overview.adoc
@@ -0,0 +1,18 @@
+= Eduard XVII — The loyal MUC servant
+
+== Continuous Integration
+
+We currently use the CI of Gitlab.com, but it might be warranted to use a
+dedicated runner which could execute the requests much faster. This is work
+for Future Homer.
+
+Currently everything is configured in the file `.gitlab-ci.yml`:
+
+[source,yaml]
+----
+include::../../../.gitlab-ci.yml[]
+----
+
+NOTE: Replacing `ec.SunEC` is done here because there were problems in the
+ Docker container with OpenJDK. Apparently making a HTTPS connection
+ to the Gradle repositories would not work. \ No newline at end of file