Skip to content

Instantly share code, notes, and snippets.

@mands
Last active July 30, 2025 14:45
Show Gist options
  • Select an option

  • Save mands/2f5af1e27840bf88e66bf61943eee0f4 to your computer and use it in GitHub Desktop.

Select an option

Save mands/2f5af1e27840bf88e66bf61943eee0f4 to your computer and use it in GitHub Desktop.
Hilla MongoDB AutoCrud Support
package common.hilla;
import module java.base;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import com.vaadin.hilla.EndpointExposed;
import com.vaadin.hilla.crud.CountService;
import com.vaadin.hilla.crud.CrudService;
import com.vaadin.hilla.crud.GetService;
import com.vaadin.hilla.crud.filter.Filter;
@Slf4j
@EndpointExposed
public class MongoCrudService<T, ID> implements CrudService<T, ID>, GetService<T, ID>, CountService {
private final MongoTemplate mongoTemplate;
private final Class<T> entityClass;
private final String idField;
public MongoCrudService(MongoTemplate mongoTemplate, Class<T> entityClass) {
this(mongoTemplate, entityClass, "_id");
}
public MongoCrudService(MongoTemplate mongoTemplate, Class<T> entityClass, String idField) {
this.mongoTemplate = mongoTemplate;
this.entityClass = entityClass;
this.idField = idField;
}
// Public API methods
@Override
public List<@NonNull T> list(Pageable pageable, @Nullable Filter filter) {
var query = buildQuery(filter);
query.with(pageable);
return mongoTemplate.find(query, entityClass);
}
@Override
@Nullable
public T save(T value) {
return mongoTemplate.save(value);
}
@Override
public void delete(ID id) {
var query = new Query(Criteria.where(idField).is(id));
mongoTemplate.remove(query, entityClass);
}
// Additional methods
@Override
public long count(@Nullable Filter filter) {
var query = buildQuery(filter);
return mongoTemplate.count(query, entityClass);
}
@Override
public boolean exists(ID id) {
var query = new Query(Criteria.where(idField).is(id));
return mongoTemplate.exists(query, entityClass);
}
@Nullable
public T findById(ID id) {
return mongoTemplate.findById(id, entityClass);
}
@Override
public Optional<@NonNull T> get(ID id) {
return Optional.ofNullable(findById(id));
}
/// build the query from the filter for the entity
private Query buildQuery(@Nullable Filter filter) {
var query = new Query();
if (filter != null) {
var criteria = MongoFilterConverter.toCriteria(filter, entityClass);
query.addCriteria(criteria);
}
return query;
}
// curently unused
public record PageResponse<T>(
List<T> content,
long totalElements,
int totalPages,
int currentPage
) {
}
public PageResponse<T> listWithCount(Pageable pageable, @Nullable Filter filter) {
var query = buildQuery(filter);
var totalElements = mongoTemplate.count(query, entityClass);
var totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize());
query.with(pageable);
var content = mongoTemplate.find(query, entityClass);
return new PageResponse<>(content, totalElements, totalPages, pageable.getPageNumber());
}
}
package common.hilla;
import module java.base;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.query.Criteria;
import com.vaadin.hilla.crud.filter.AndFilter;
import com.vaadin.hilla.crud.filter.Filter;
import com.vaadin.hilla.crud.filter.OrFilter;
import com.vaadin.hilla.crud.filter.PropertyStringFilter;
@Slf4j
public final class MongoFilterConverter {
private MongoFilterConverter() {
}
public static <T> Criteria toCriteria(Filter filter, Class<T> entityClass) {
return switch (filter) {
case AndFilter f when !f.getChildren().isEmpty() -> {
var criteriaList = f.getChildren().stream()
.map(f1 -> toCriteria(f1, entityClass))
.toArray(Criteria[]::new);
yield new Criteria().andOperator(criteriaList);
}
case OrFilter f when !f.getChildren().isEmpty() -> {
var criteriaList = f.getChildren().stream()
.map(f1 -> toCriteria(f1, entityClass))
.toArray(Criteria[]::new);
yield new Criteria().orOperator(criteriaList);
}
case PropertyStringFilter f -> buildPropertyCriteria(f, entityClass);
// case null -> new Criteria();
default -> {
log.warn("Unknown/empty filter type: {}, ignoring", filter.getClass());
yield new Criteria();
// throw new IllegalArgumentException("Unknown filter type: " + filter.getClass());
}
};
}
private static <T> Criteria buildPropertyCriteria(PropertyStringFilter filter, Class<T> entityClass) {
var propertyPath = filter.getPropertyId();
var filterValue = filter.getFilterValue();
Object convertedValue = convertValue(filterValue, propertyPath, entityClass);
var criteria = Criteria.where(propertyPath);
return switch (filter.getMatcher()) {
case EQUALS -> criteria.is(convertedValue);
case CONTAINS -> switch (convertedValue) {
case String s -> criteria.regex(".*" + s + ".*", "i");
default -> criteria.is(convertedValue); // Fallback for non-strings
};
case LESS_THAN -> criteria.lt(convertedValue);
case GREATER_THAN -> criteria.gt(convertedValue);
};
}
private static <T> Object convertValue(String value, String propertyPath, Class<T> entityClass) {
// if (value == null) return null;
try {
var parts = propertyPath.split("\\.");
Class<?> currentClass = entityClass;
Field field = null;
for (var part : parts) {
field = currentClass.getDeclaredField(part);
currentClass = field.getType();
}
if (field == null) return value;
return convertToType(value, field.getType());
} catch (Exception e) {
return value; // Default to string
}
}
private static Object convertToType(String value, Class<?> targetType) {
return switch (targetType) {
case Class<?> c when c == String.class -> value;
case Class<?> c when c == Long.class || c == long.class -> Long.parseLong(value);
case Class<?> c when c == Integer.class || c == int.class -> Integer.parseInt(value);
case Class<?> c when c == Double.class || c == double.class -> Double.parseDouble(value);
case Class<?> c when c == Float.class || c == float.class -> Float.parseFloat(value);
case Class<?> c when c == Boolean.class || c == boolean.class -> Boolean.parseBoolean(value);
case Class<?> c when c == Short.class || c == short.class -> Short.parseShort(value);
case Class<?> c when c == Byte.class || c == byte.class -> Byte.parseByte(value);
case Class<?> c when c == Character.class || c == char.class -> value.isEmpty() ? '\0' : value.charAt(0);
case Class<?> c when c == LocalDate.class -> LocalDate.parse(value);
case Class<?> c when c == LocalDateTime.class -> LocalDateTime.parse(value);
case Class<?> c when c == Instant.class -> Instant.parse(value);
case Class<?> c when c == UUID.class -> UUID.fromString(value);
case Class<?> c when c == BigDecimal.class -> new BigDecimal(value);
case Class<?> c when c.isEnum() -> convertEnum(value, c);
default -> value;
};
}
@SuppressWarnings("unchecked")
private static Object convertEnum(String value, Class<?> enumClass) {
return Enum.valueOf((Class<? extends Enum>) enumClass, value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment