>>> I'm afraid the name is not much good, however it does get to the point.
The project is a user-friendly infrastructure for collecting/storing/analysing data, through a simple
GET/POST request
>>> It is written in Java, using Spring Boot and Hibernate, and is currently around 80% done.
It is designed to be as user-friendly as possible while maintaining security and functionality.
>>> There are two main components of the project: Collection Tables and Scheduled Events. The first provides the user the ability to create a custom database table, with any number of columns of various types. The second is a tool that allows the users to schedule events using CRON expressions, and to have those events perform a task from the provided list. The intended use of these tools is analytics and small-scale automation.
>>> The user may then view the created events and tables in their dashboard, where they can view the totality of their instances or download them in .CSV format
>>> From there the user can easily acces more data on the table, or generate code snippets/GET requests for inserting new values into the table.
>>> Now, to the actual code and solutions to various problems. Firstly, I want the tables to be accessible via URL, meaning the tables actually have to have unique identifiers. This is why the table class looks like this:
@Entity
public class CollectionTable {
@Id
private String id;
private String name;
private Long ownerId;
private String url;
public CollectionTable(String name, Long ownerId, String datatype) {
this.id = UUID.randomUUID().toString();
this.name = name;
this.ownerId = ownerId;
};
>>> The Hibernate instances of the tables are independent from the MySQL instances of the tables as there is
no
concrete template for it - a user creates them dynamically.
In the actual SQL database these tables are stored by their id value, and while this does create a bit of a
mess in the DB, it does also create an additonal layer between the users and the SQL table. If I ever
get to Hibernate encryption, it would also create a potential layer of security for the users' data, as
the Java class effectively acts as a proxy/metadata for the actual data, and there is no way to relate the
data in the SQL tables to a user without the Hibernate-managed entity associated with the table.
>>> The template for the table isn't stored either, as it only takes a simple query to get it, and to
minimize
the usage of this already small query, I have implemented a method to generate the table URL on
initialization:
public void setUrl(Map columns) {
StringBuilder u = new StringBuilder("http://datagath.ljzcvs.dev/tables/input/" + id + "?");
columns.forEach((name, type) -> {
switch (type) {
case "TEXT":
u.append(name).append("='YOURTEXT'&");
break;
case "DATETIME":
u.append(name).append("=YOURDATETIME&")
break;
default:
u.append(name).append("=YOURNUMBER&")
break;
});
if (u.charAt(u.length() - 1) == '&') {
u.deleteCharAt(u.length() - 1);
}
this.url = u.toString();
}
>>> So now, with a simple Controller method, the table can have values added to it whenever the client calls it - methods in different langauges are also provided, as you may see in Fig. 3. I also wanted to have a chart to monitor the activity of the table: you may see that in Fig.2 and Fig.3. To do that, I wrote a simple bucketing SQL query. The method returns an int[], as it operates with the assumption that the client already set it the time interval it wishes to bucket:
public int[] getActivity(CollectionTable table, String timeframe) {
String sql;
int[] buckets;
switch (timeframe) {
case "hour":
sql = """
SELECT FLOOR(TIMESTAMPDIFF(MINUTE, tmstp, NOW()) / 5) AS bucket, COUNT(*) AS cnt
FROM `%s`
WHERE tmstp > NOW() - INTERVAL 1 HOUR
GROUP BY bucket;
""".formatted(table.getId());
buckets = new int[12];
break;
case "week":
sql = """
SELECT FLOOR(TIMESTAMPDIFF(DAY, tmstp, NOW()) / 1) AS bucket, COUNT(*) AS cnt
FROM `%s`
WHERE tmstp > NOW() - INTERVAL 7 DAY
GROUP BY bucket;
""".formatted(table.getId());
buckets = new int[7];
break;
case "month":
sql = """
SELECT FLOOR(TIMESTAMPDIFF(DAY, tmstp, NOW()) / 1) AS bucket, COUNT(*) AS cnt
FROM `%s`
WHERE tmstp > NOW() - INTERVAL 30 DAY
GROUP BY bucket;
""".formatted(table.getId());
buckets = new int[30]
break;
case "day":
default:
sql = """
SELECT FLOOR(TIMESTAMPDIFF(HOUR, tmstp, NOW()) / 1) AS bucket, COUNT(*) AS cnt
FROM `%s`
WHERE tmstp > NOW() - INTERVAL 1 DAY
GROUP BY bucket;
""".formatted(table.getId());
buckets = new int[24]
break;
}
Query query = entityManager.createNativeQuery(sql)
@SuppressWarnings("unchecked")
List
>>> Moving on to the events, they are only stored as Hibernate entities:
public ScheduledEvent(String name, String cronString, User owner, String action) {
this.name = name;
this.cronString = cronString;
this.owner = owner;
this.action = action;
}
public void generateActionBody(Map actionBodyMap) {
String action = actionBodyMap.containsKey("action") ? actionBodyMap.get("action") : null;
if (action == null) {
return;
}
Map> requiredKeys = new HashMap>() {
{
put("PING", Arrays.asList("sendAddress", "pingAddress"));
put("AI/LLM", Arrays.asList("sendAddress", "prompt", "apikey", "model"));
put("REPORT", Arrays.asList("sendAddress", "dataset"));
put("VISUALISATION", Arrays.asList("sendAddress", "dataset", "visualisationType"));
}
};
StringBuilder bodyBuilder = new StringBuilder();
String[] specificKeyRequirements = requiredKeys.containsKey(action)
? requiredKeys.get(action).toArray(new String[0])
: new String[0];
if (specificKeyRequirements.length > 0) {
Boolean containsKeys = Arrays.stream(specificKeyRequirements).allMatch(actionBodyMap::containsKey);
if (containsKeys) {
actionBodyMap.forEach((key, value) -> {
bodyBuilder.append("%s:%s".formatted(key, value)).append(";");
});
}
}else{
throw new IllegalArgumentException("some of the arguments are missing");
}
this.actionBody = bodyBuilder.toString();
}
>>> And then, a Java library Jobrunr takes over, using the user-provided CRON time to execute the events on according time. The execution of the events is not yet fully implemented, only the file emailing part is complete:
public void sendMessageWithAttachment(
String to, String subject, String text, byte[] document)
throws MessagingException, DocumentException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom("[email protected]");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text, false);
helper.addAttachment("Report.pdf", new ByteArrayResource(document));
emailSender.send(message);
}
>>> All in all, to see the full extent of the experience that working on this project has provided me with,
you may visit its' GitHub page. I hope this shows you how versatile my programming experience is.
>>> Thank you for reading,
>>> Ilja Zaicevs.