Skip to content

Commit

Permalink
feat: Add interfaces for CRUD services
Browse files Browse the repository at this point in the history
These were previously in Hilla but are equally useful in Flow applications
  • Loading branch information
Artur- committed Dec 18, 2024
1 parent fb02577 commit 83ece96
Show file tree
Hide file tree
Showing 19 changed files with 1,005 additions and 0 deletions.
36 changes: 36 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/Nonnull.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2022 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to mark either field, method, parameter or type parameter as
* non-nullable. It is used by Typescript Generator as a source of type
* nullability information.
*
* This annotation exists because the traditional `jakarta.annotation.Nonnull`
* annotation is not applicable to type parameters.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE_USE })
public @interface Nonnull {
}
36 changes: 36 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/Nullable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2022 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to mark either field, method, parameter or type parameter as
* nullable. It is used by Typescript Generator as a source of type nullability
* information.
*
* This annotation exists because the traditional `jakarta.annotation.Nullable`
* annotation is not applicable to type parameters.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE_USE })
public @interface Nullable {
}
20 changes: 20 additions & 0 deletions vaadin-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@
<artifactId>spring-data-commons</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>aspectjrt</artifactId>
<groupId>org.aspectj</groupId>
</exclusion>
<exclusion>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-data</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.vaadin.flow.spring.data;

import com.vaadin.flow.Nullable;

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

/**
* A service that can count the given type of objects with a given filter.
*/
public interface CountService {

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

}
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
@@ -0,0 +1,7 @@
package com.vaadin.flow.spring.data;

/**
* 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> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.vaadin.flow.spring.data;

import com.vaadin.flow.Nullable;

/**
* A service that can create, update, and delete a given type of object.
*/
public interface FormService<T, ID> {

/**
* Saves the given object and returns the (potentially) updated object.
* <p>
* If you store the object in a SQL database, the returned object might have
* a new id or updated consistency version.
*
* @param value
* the object to save
* @return the fresh object or {@code null} if no object was found to update
*/
@Nullable
T save(T value);

/**
* Deletes the object with the given id.
*
* @param id
* the id of the object to delete
*/
void delete(ID id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.vaadin.flow.spring.data;

import java.util.Optional;

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

/**
* Gets the object with the given id.
*
* @param id
* the id of the object
* @return the object, or an empty optional if no object with the given id
*/
Optional<T> get(ID id);

/**
* Checks if an object with the given id exists.
*
* @param id
* the id of the object
* @return {@code true} if the object exists, {@code false} otherwise
*/
default boolean exists(ID id) {
return get(id).isPresent();
}

}
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();
}
}

}
Loading

0 comments on commit 83ece96

Please sign in to comment.