Skip to main content

CRUD Activities - Setup

This section provides the necessary steps to implement CRUD (Create Read Update Delete) operations on Activities.
This tutorial project uses the MongoDB database running in the Data container, however you can also implement a custom DAO (Data Access Object) class to interact with other database types (Data container also includes a PostgreSQL service). This flexibility is made possible by the controller, which accepts any DAO instance through constructor injection.

info

In real-world scenarios, it is highly recommended to use Wanderlust for managing CRUD operations on entities.

Util

1. Create the package com.smeup.kokos.util

2. Create new class FieldsValidator
This class checks the variables annotated with @Required.
The method isRequiredNotNull returns a boolean value:

  • True: all @Required variables are not null

  • False: at least one @Required variable is null

    package com.smeup.kokos.util;

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.reflect.Field;

    public class FieldsValidator {

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Required {
    }

    public static <T> boolean isRequiredNotNull(T object) {

    for (Field field : object.getClass().getDeclaredFields()) {
    field.setAccessible(true);
    try {

    if (field.isAnnotationPresent(Required.class) && field.get(object) == null) {
    return false;
    }
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    return false;
    }
    }
    return true;
    }
    }

3. Create new class TimeUtil
It will be used to handle the LocalDate and LocalTime data for activities.

package com.smeup.kokos.util;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class TimeUtil {

public static LocalTime parseToLocalTime(String timeStr) {
try {
timeStr = timeStr.replace(":", "");
String formattedTime;

if (timeStr.length() < 4 || timeStr.length() == 5) {
throw new IllegalArgumentException("Invalid format");
}

if (timeStr.length() == 4) {
formattedTime = timeStr.substring(0, 2) + ":" + timeStr.substring(2, 4);
return LocalTime.parse(formattedTime);
}

formattedTime = timeStr.substring(0, 2) + ":" + timeStr.substring(2, 4) + ":"
+ timeStr.substring(4, 6);
return LocalTime.parse(formattedTime);
} catch (Exception e) {
System.err.println("Error parsing time: " + timeStr);
e.printStackTrace();
return null;
}
}

public static LocalDate parseToLocalDate(String strDate) {
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

try {
return LocalDate.parse(strDate, dateFormatter);
} catch (DateTimeParseException e) {
System.err.println("Error parsing date: " + strDate + ". Expected format: yyyy-MM-dd");
e.printStackTrace();
return null;
}
}

public static double getTotalHours(LocalTime start, LocalTime end) {
Duration duration = Duration.between(start, end);
if (duration.isZero() || duration.isNegative()) {
return 0;
}

long hours = duration.toHours();
long minutes = duration.toMinutes() % 60;
double totHours = hours + (minutes / 60.0);
return (double) Math.round(totHours * 100) / 100;
}
}

4. Create new class WordUtil

package com.smeup.kokos.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class WordUtil {

private static final Map<String, String> PLURALS = new LinkedHashMap<>(Map.ofEntries(
Map.entry("Activity", "Activities")));

public static String getPlural(String word) {
return PLURALS.getOrDefault(word, word + "s");
}

}

Entity - Activity

1. Create the package com.smeup.kokos.entity.activity

2. Create new class Activity
@Required annotation indicate which fields must be provided when creating a new activity.

package com.smeup.kokos.entity.activity;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;

import com.smeup.kokos.util.FieldsValidator.Required;

public class Activity {
@Required
private LocalDate date;
@Required
private LocalTime start;
@Required
private LocalTime end;
@Required
private String styleCategory;
@Required
private String category;

private String id;
private String description;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public LocalDate getDate() {
return date;
}

public void setDate(LocalDate date) {
this.date = date;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public LocalTime getStart() {
return start;
}

public void setStart(LocalTime start) {
this.start = start;
}

public LocalTime getEnd() {
return end;
}

public void setEnd(LocalTime end) {
this.end = end;
}

public String getStyleCategory() {
return styleCategory;
}

public void setStyleCategory(String styleCategory) {
this.styleCategory = styleCategory;
}

public String getCategory() {
return category;
}

public void setCategory(String category) {
this.category = category;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Activity activity = (Activity) o;
return Objects.equals(id, activity.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {

return "{" +
"id=" + id +
", date=" + date +
", description=" + description +
", start=" + start +
", end=" + end +
", styleCategory=" + styleCategory +
", category=" + category +
'}';
}
}

3. Create new class ActivityBuilder
This class demonstrates how to implement the builder pattern, which will be used to define Data Structures in the Micro Executor services.
The builder pattern provides a flexible and readable way to create complex objects by allowing method chaining to set properties step by step. This ensures that the object is created without requiring a large constructor with many parameters.

package com.smeup.kokos.entity.activity;

import java.time.LocalDate;
import java.time.LocalTime;

public class ActivityBuilder {
private Activity activity;

private ActivityBuilder() {
this.activity = new Activity();
}

public static ActivityBuilder builder() {
return new ActivityBuilder();
}

public ActivityBuilder withId(String id) {
this.activity.setId(id);
return this;
}

public ActivityBuilder withDate(LocalDate date) {
this.activity.setDate(date);
return this;
}

public ActivityBuilder withStart(LocalTime start) {
this.activity.setStart(start);
return this;
}

public ActivityBuilder withEnd(LocalTime end) {
this.activity.setEnd(end);
return this;
}

public ActivityBuilder withStyleCategory(String styleCategory) {
this.activity.setStyleCategory(styleCategory);
return this;
}

public ActivityBuilder withCategory(String category) {
this.activity.setCategory(category);
return this;
}

public ActivityBuilder withDescription(String description) {
this.activity.setDescription(description);
return this;
}

public Activity build() {
return this.activity;
}
}

Database - Mongo

1. Add MongoDb dependecy
In pom.xml

<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>4.9.1</version>
</dependency>

2. Create new package com.smeup.kokos.db.mongo

3. Create new enum MongoDatabaseName
It tracks the database names, in this case tutorial_db

package com.smeup.kokos.db.mongo;

public enum MongoDatabaseName {
TUTORIAL("tutorial_db");

private final String name;

MongoDatabaseName(String name) {
this.name = name;
}

public String get() {
return name;
}
}

4. Create new class MongoConnection
It handles the connection to the MongoDB instance running in the Data container and provides a static method to get the specific database.

package com.smeup.kokos.db.mongo;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;

public class MongoConnection {
private static final String MONGO_DB_USER = "root";
private static final String MONGO_DB_PASSWORD = "smeup";
private static final String MONGO_DB_ADDRESS = String.format(
"mongodb://%s:%s@localhost:27017",
MONGO_DB_USER,
MONGO_DB_PASSWORD);

private static MongoClient client = null;

public static MongoClient getClient() {
if (client == null) {
client = MongoClients.create(MONGO_DB_ADDRESS);
}
return client;
}

public static MongoDatabase getDatabase(MongoDatabaseName name) {
return getClient().getDatabase(name.get());
}
}

Repository

1. Create new package com.smeup.kokos.repository

2. Create new interface DAO

package com.smeup.kokos.repository;

import java.util.Map;
import java.util.Optional;
import java.util.List;

public interface DAO<T> {

Optional<T> create(T entity);

Map<String, T> createMany(List<T> entities);

Optional<T> getById(String id);

Map<String, T> getAll();

Optional<T> update(String id, T entity);

Optional<T> delete(String id);
}

3. Create new package com.smeup.kokos.repository.mongo

4. Create new class ActivitiesMongoDAO
Implement the DAO interface to interact with MongoDB and handles CRUD operations on activities.

package com.smeup.kokos.repository.mongo;

import java.util.Map;
import java.util.Optional;
import java.util.LinkedHashMap;
import java.util.List;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;

import org.bson.BsonValue;
import org.bson.Document;
import org.bson.types.ObjectId;

import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.result.InsertManyResult;
import com.mongodb.client.result.InsertOneResult;

import static com.mongodb.client.model.Filters.eq;

import com.smeup.kokos.db.mongo.MongoConnection;
import com.smeup.kokos.db.mongo.MongoDatabaseName;
import com.smeup.kokos.entity.activity.Activity;
import com.smeup.kokos.entity.activity.ActivityBuilder;
import com.smeup.kokos.repository.DAO;

public class ActivitiesMongoDAO implements DAO<Activity> {

public static final String COLLECTION_NAME = "activities";

public static final String ID = "_id";
public static final String DATE = "date";
public static final String DESCRIPTION = "description";
public static final String START = "start";
public static final String END = "end";
public static final String STYLE_CATEGORY = "styleCategory";
public static final String CATEGORY = "category";
public static final String HOURS = "hours";

private final MongoCollection<Document> activitiesCollection;

public ActivitiesMongoDAO() {
activitiesCollection = MongoConnection.getDatabase(MongoDatabaseName.TUTORIAL).getCollection(COLLECTION_NAME);
}

public ActivitiesMongoDAO(MongoCollection<Document> activitiesCollection) {
this.activitiesCollection = activitiesCollection;
}

@Override
public Optional<Activity> create(Activity activity) {

try {
Document docActivity = new Document()
.append(DATE, activity.getDate())
.append(DESCRIPTION, activity.getDescription())
.append(START, activity.getStart())
.append(END, activity.getEnd())
.append(STYLE_CATEGORY, activity.getStyleCategory())
.append(CATEGORY, activity.getCategory());
InsertOneResult result = activitiesCollection.insertOne(docActivity);
Activity newActivity = activity;
newActivity.setId(result.getInsertedId().asObjectId().getValue().toHexString());

return Optional.of(newActivity);
} catch (Exception e) {
System.err.println("New activity error: " + e);
return Optional.empty();
}
}

@Override
public Map<String, Activity> createMany(List<Activity> activities) {
Map<String, Activity> activitiesCreatedMap = new LinkedHashMap<>();
List<Document> docsToInsert = new ArrayList<>();

for (Activity activity : activities) {
Document docActivity = new Document()
.append(DATE, activity.getDate())
.append(DESCRIPTION, activity.getDescription())
.append(START, activity.getStart())
.append(END, activity.getEnd())
.append(STYLE_CATEGORY, activity.getStyleCategory())
.append(CATEGORY, activity.getCategory());
docsToInsert.add(docActivity);
}

try {
InsertManyResult result = activitiesCollection.insertMany(docsToInsert);

Map<Integer, BsonValue> activitiesCreatedIds = result.getInsertedIds();
for (int i = 0; i < docsToInsert.size(); i++) {
String id = activitiesCreatedIds.get(i).asObjectId().getValue().toHexString();
Activity activityCreated = activities.get(i);
activityCreated.setId(id);
activitiesCreatedMap.put(id, activityCreated);
}

} catch (Exception e) {
System.err.println("Create many activities error: " + e);
}
return activitiesCreatedMap;
}

@Override
public Optional<Activity> getById(String id) {
try {
Document doc = activitiesCollection.find(eq(ID, new ObjectId(id))).first();
return Optional.of(documentToActivity(doc));
} catch (Exception e) {
System.err.println("Get activity by id error: " + e);
return Optional.empty();
}
}

@Override
public Map<String, Activity> getAll() {
Map<String, Activity> activitiesMap = new LinkedHashMap<>();
try {
for (Document doc : activitiesCollection.find()) {
activitiesMap.put(doc.getObjectId(ID).toHexString(), documentToActivity(doc));
}
} catch (Exception e) {
System.err.println("Get all activities error: " + e);
}
return activitiesMap;
}

@Override
public Optional<Activity> update(String id, Activity activity) {
Document activityDoc = new Document();

try {
if (activity.getDate() != null) {
activityDoc.append(DATE, activity.getDate());
}
if (activity.getDescription() != null) {
activityDoc.append(DESCRIPTION, activity.getDescription());
}
if (activity.getStart() != null) {
activityDoc.append(START, activity.getStart());
}
if (activity.getEnd() != null) {
activityDoc.append(END, activity.getEnd());
}
if (activity.getStyleCategory() != null) {
activityDoc.append(STYLE_CATEGORY, activity.getStyleCategory());
}
if (activity.getCategory() != null) {
activityDoc.append(CATEGORY, activity.getCategory());
}

if (activityDoc.isEmpty()) {
System.out.println("No activity field to update");
return Optional.empty();
}

Document updatedActivityDoc = activitiesCollection.findOneAndUpdate(
eq(ID, new ObjectId(id)),
new Document("$set", activityDoc),
new com.mongodb.client.model.FindOneAndUpdateOptions()
.returnDocument(ReturnDocument.AFTER));

if (updatedActivityDoc == null) {
System.out.println("No activity found!");
return Optional.empty();
}
return Optional.of(documentToActivity(updatedActivityDoc));

} catch (Exception e) {
System.err.println("Update activity error: " + e);
return Optional.empty();
}
}

@Override
public Optional<Activity> delete(String id) {
try {
Document deletedActivityDoc = activitiesCollection.findOneAndDelete(Filters.eq(ID, new ObjectId(id)));
if (deletedActivityDoc != null) {
return Optional.of(documentToActivity(deletedActivityDoc));
}

System.out.println("No activity found!");
return Optional.empty();
} catch (Exception e) {
System.err.println("Delete activity error: " + e);
return Optional.empty();
}
}

private Activity documentToActivity(Document doc) {
LocalDate date = doc.getDate(DATE).toInstant().atZone(ZoneOffset.UTC).toLocalDate();
LocalTime start = doc.getDate(START).toInstant().atZone(ZoneOffset.UTC).toLocalTime();
LocalTime end = doc.getDate(END).toInstant().atZone(ZoneOffset.UTC).toLocalTime();

return ActivityBuilder.builder()
.withId(doc.getObjectId(ID).toHexString())
.withDate(date)
.withStart(start)
.withEnd(end)
.withStyleCategory(doc.getString(STYLE_CATEGORY))
.withCategory(doc.getString(CATEGORY))
.withDescription(doc.getString(DESCRIPTION))
.build();
}
}

Controller

1. Create new package com.smeup.kokos.controller

2. Create new class Controller
This class will be used by the Micro Executor services for CRUD operations on entities.
By injecting any DAO instance into its constructor, you can easily use different databases.

package com.smeup.kokos.controller;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.smeup.kokos.repository.DAO;
import com.smeup.kokos.util.FieldsValidator;
import com.smeup.kokos.util.WordUtil;

public class Controller<T> {
private DAO<T> repository;
private String entityName;

public Controller(DAO<T> repository) {
this.repository = repository;
entityName = getEntityName();
}

public T create(T entity) {
if (!FieldsValidator.isRequiredNotNull(entity)) {
System.err.println(String
.format("Create new %s error: some required fields are null", entityName) +
" " + entity);

return null;
}
Optional<T> newEntity = repository.create(entity);
if (newEntity.isPresent()) {
System.out.println(String.format("New %s created!", entityName));
}

return newEntity.orElse(null);
}

public Map<String, T> createMany(List<T> entities) {
Map<String, T> entitiesCreatedMap = new LinkedHashMap<>();
for (T entity : entities) {
if (!FieldsValidator.isRequiredNotNull(entity)) {
System.err.println(String
.format("Create new %s error: some required fields are null", WordUtil.getPlural(entityName)) +
" " + entity);

return entitiesCreatedMap;
}
}
entitiesCreatedMap = repository.createMany(entities);
if (!entitiesCreatedMap.isEmpty()) {
System.out.println(String.format("%s created!", WordUtil.getPlural(entityName)));
}

return entitiesCreatedMap;
}

public T getById(String id) {
Optional<T> entity = repository.getById(id);
if (entity.isEmpty()) {
System.out.println(String.format("%s not found!", entityName));
}

return entity.orElse(null);
}

public Map<String, T> getAll() {
Map<String, T> entitiesMap = repository.getAll();
if (entitiesMap.isEmpty()) {
System.out.println(String.format("There are no %s!", WordUtil.getPlural(entityName)));
}

return entitiesMap;
}

public T update(String id, T entity) {
Optional<T> updatedEntity = repository.update(id, entity);
if (updatedEntity.isPresent()) {
System.out.println(String.format("%s updated!", entityName));
}

return updatedEntity.orElse(null);
}

public T delete(String id) {
Optional<T> deltedEntity = repository.delete(id);
if (deltedEntity.isPresent()) {
System.out.println(String.format("%s deleted!", entityName));
}

return deltedEntity.orElse(null);
}

private String getEntityName() {
try {
Type[] genericInterfaces = repository.getClass().getGenericInterfaces();
if (genericInterfaces.length > 0 && genericInterfaces[0] instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericInterfaces[0];
Type actualType = paramType.getActualTypeArguments()[0];
if (actualType instanceof Class<?>) {
return ((Class<?>) actualType).getSimpleName();
}
}
throw new IllegalArgumentException("The repository does not specify a generic type");
} catch (Exception e) {
System.err.println("Unable to determine entity name from repository: " + e.getMessage());
}
return "UnknownEntity";
}
}