Skip to content

Commit

Permalink
add more and fix javadoc
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur- committed Dec 18, 2024
1 parent 6dcba3a commit b433077
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.vaadin.flow.spring.data;

import org.springframework.lang.Nullable;
import com.vaadin.flow.Nullable;

import com.vaadin.flow.spring.data.filter.Filter;

/**
* A browser-callable service that can count the given type of objects with a
* A service that can count the given type of objects with a
* given filter.
*/
public interface CountService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.vaadin.flow.spring.data;

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;

import com.vaadin.flow.Nullable;

/**
* A service that delegates crud operations to a JPA repository.
*/
public class CrudRepositoryService<T, ID, R extends CrudRepository<T, ID> & JpaSpecificationExecutor<T>>
extends ListRepositoryService<T, ID, R> implements CrudService<T, ID> {

/*
* Creates the service by autodetecting the type of repository and entity to
* use from the generics.
*/
public CrudRepositoryService() {
super();
}

/**
* Creates the service using the given repository.
*
* @param repository
* the JPA repository
*/
public CrudRepositoryService(R repository) {
super(repository);
}

@Override
public @Nullable T save(T value) {
return getRepository().save(value);
}

/**
* Saves the given objects and returns the (potentially) updated objects.
* <p>
* The returned objects might have new ids or updated consistency versions.
*
* @param values
* the objects to save
* @return the fresh objects
*/
public List<T> saveAll(Iterable<T> values) {
List<T> saved = new ArrayList<>();
getRepository().saveAll(values).forEach(saved::add);
return saved;
}

@Override
public void delete(ID id) {
getRepository().deleteById(id);
}

/**
* Deletes the objects with the given ids.
*
* @param ids
* the ids of the objects to delete
*/
public void deleteAll(Iterable<ID> ids) {
getRepository().deleteAllById(ids);
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.vaadin.flow.spring.data;

/**
* A browser-callable service that can create, read, update, and delete a given
* A service that can create, read, update, and delete a given
* type of object.
*/
public interface CrudService<T, ID> extends ListService<T>, FormService<T, ID> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.vaadin.flow.spring.data;

import org.springframework.lang.Nullable;
import com.vaadin.flow.Nullable;

/**
* A browser-callable service that can create, update, and delete a given type
* A service that can create, update, and delete a given type
* of object.
*/
public interface FormService<T, ID> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.util.Optional;

/**
* A browser-callable service that can fetch the given type of object.
* A service that can fetch the given type of object.
*/
public interface GetService<T, ID> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.vaadin.flow.spring.data;

import jakarta.persistence.EntityManager;

import com.vaadin.flow.spring.data.filter.AndFilter;
import com.vaadin.flow.spring.data.filter.Filter;
import com.vaadin.flow.spring.data.filter.OrFilter;
import com.vaadin.flow.spring.data.filter.PropertyStringFilter;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;

/**
* Utility class for converting Hilla {@link Filter} specifications into JPA
* filter specifications. This class can be used to implement filtering for
* custom {@link ListService} or {@link CrudService} implementations that use
* JPA as the data source.
* <p>
* This class requires an EntityManager to be available as a Spring bean and
* thus should be injected into the bean that wants to use the converter.
* Manually creating new instances of this class will not work.
*/
@Component
public class JpaFilterConverter {

@Autowired
private EntityManager em;

/**
* Converts the given Hilla filter specification into a JPA filter
* specification for the specified entity class.
* <p>
* If the filter contains {@link PropertyStringFilter} instances, their
* properties, or nested property paths, need to match the structure of the
* entity class. Likewise, their filter values should be in a format that
* can be parsed into the type that the property is of.
*
* @param <T>
* the type of the entity
* @param rawFilter
* the filter to convert
* @param entity
* the entity class
* @return a JPA filter specification for the given filter
*/
public <T> Specification<T> toSpec(Filter rawFilter, Class<T> entity) {
if (rawFilter == null) {
return Specification.anyOf();
}
if (rawFilter instanceof AndFilter filter) {
return Specification.allOf(filter.getChildren().stream()
.map(f -> toSpec(f, entity)).toList());
} else if (rawFilter instanceof OrFilter filter) {
return Specification.anyOf(filter.getChildren().stream()
.map(f -> toSpec(f, entity)).toList());
} else if (rawFilter instanceof PropertyStringFilter filter) {
Class<?> javaType = extractPropertyJavaType(entity,
filter.getPropertyId());
return new PropertyStringFilterSpecification<>(filter, javaType);
} else {
if (rawFilter != null) {
throw new IllegalArgumentException("Unknown filter type "
+ rawFilter.getClass().getName());
}
return Specification.anyOf();
}
}

private Class<?> extractPropertyJavaType(Class<?> entity,
String propertyId) {
if (propertyId.contains(".")) {
String[] parts = propertyId.split("\\.");
Root<?> root = em.getCriteriaBuilder().createQuery(entity)
.from(entity);
Path<?> path = root.get(parts[0]);
int i = 1;
while (i < parts.length) {
path = path.get(parts[i]);
i++;
}
return path.getJavaType();
} else {
return em.getMetamodel().entity(entity).getAttribute(propertyId)
.getJavaType();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.vaadin.flow.spring.data;

import jakarta.annotation.PostConstruct;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;

import com.googlecode.gentyref.GenericTypeReflector;
import com.vaadin.flow.Nullable;
import com.vaadin.flow.spring.data.filter.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;

/**
* A service that delegates list operations to a JPA repository.
*/
public class ListRepositoryService<T, ID, R extends CrudRepository<T, ID> & JpaSpecificationExecutor<T>>
implements ListService<T>, GetService<T, ID>, CountService {

@Autowired
private JpaFilterConverter jpaFilterConverter;

@Autowired
private ApplicationContext applicationContext;

private R repository;
private final Class<T> entityClass;

/*
* Creates the service by autodetecting the type of repository and entity to
* use from the generics.
*/
public ListRepositoryService() {
this.entityClass = resolveEntityClass();
}

/**
* Creates the service using the given repository.
*
* @param repository
* the JPA repository
*/
public ListRepositoryService(R repository) {
this.repository = repository;
this.entityClass = resolveEntityClass();
}

@PostConstruct
private void init() {
if (repository == null) {
repository = resolveRepository();
}
}

/**
* Accessor for the repository instance.
*
* @return the repository instance
*/
protected R getRepository() {
return repository;
}

@Override
public List<T> list(Pageable pageable, @Nullable Filter filter) {
Specification<T> spec = toSpec(filter);
return getRepository().findAll(spec, pageable).getContent();
}

@Override
public Optional<T> get(ID id) {
return getRepository().findById(id);
}

@Override
public boolean exists(ID id) {
return getRepository().existsById(id);
}

/**
* Counts the number of entities that match the given filter.
*
* @param filter
* the filter, or {@code null} to use no filter
* @return
*/
@Override
public long count(@Nullable Filter filter) {
return getRepository().count(toSpec(filter));
}

/**
* Converts the given filter to a JPA specification.
*
* @param filter
* the filter to convert
* @return a JPA specification
*/
protected Specification<T> toSpec(@Nullable Filter filter) {
return jpaFilterConverter.toSpec(filter, entityClass);
}

@SuppressWarnings("unchecked")
private R resolveRepository() {
var repositoryTypeParam = ListRepositoryService.class
.getTypeParameters()[2];
Type entityType = GenericTypeReflector.getTypeParameter(getClass(),
repositoryTypeParam);
if (entityType == null) {
throw new IllegalStateException(String.format(
"Unable to detect the type for the class '%s' in the "
+ "class '%s'.",
repositoryTypeParam, getClass()));
}
Class<R> repositoryClass = (Class<R>) GenericTypeReflector
.erase(entityType);
return applicationContext.getBean(repositoryClass);
}

@SuppressWarnings("unchecked")
protected Class<T> resolveEntityClass() {
var entityTypeParam = ListRepositoryService.class
.getTypeParameters()[0];
Type entityType = GenericTypeReflector.getTypeParameter(getClass(),
entityTypeParam);
if (entityType == null) {
throw new IllegalStateException(String.format(
"Unable to detect the type for the class '%s' in the "
+ "class '%s'.",
entityTypeParam, getClass()));
}
return (Class<T>) GenericTypeReflector.erase(entityType);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.vaadin.flow.spring.data.filter;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
Expand All @@ -20,6 +22,6 @@
@JsonSubTypes({ @Type(value = OrFilter.class, name = "or"),
@Type(value = AndFilter.class, name = "and"),
@Type(value = PropertyStringFilter.class, name = "propertyString") })
public class Filter {
public class Filter implements Serializable {

}

0 comments on commit b433077

Please sign in to comment.