Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dummy.api.base-url=https://dummyjson.com/
| `POST` | `/random-users` | Create a new user | ✅ |
| `PUT` | `/random-users/{id}` | Update user | ✅ |
| `DELETE` | `/random-users/{id}` | Delete user | ✅ |
| `GET` | `/random-users/filter` | Filter users by criteria | ✅ |

### Example Request

Expand Down Expand Up @@ -115,6 +116,12 @@ curl -X POST "http://localhost:8080/random-users" \
"nat": "FR"
}'

# Filter users by gender and nationality
curl -X GET "http://localhost:8080/random-users/filter?gender=MALE&nat=FR"

# Filter users by firstname (partial match, case-insensitive)
curl -X GET "http://localhost:8080/random-users/filter?firstname=john"

# Update user
curl -X PUT "http://localhost:8080/random-users/1" \
-H "Content-Type: application/json" \
Expand Down
7 changes: 1 addition & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -78,11 +78,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
package com.xpeho.spring_boot_java_random_user.data.models.database;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

@Table("users")
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {
@Id
@Column("id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column("gender")
@Column(name = "gender")
private String gender;
@Column("firstname")
@Column(name = "firstname")
private String firstname;
@Column("lastname")
@Column(name = "lastname")
private String lastname;
@Column("civility")
@Column(name = "civility")
private String civility;
@Column("email")
@Column(name = "email")
private String email;
@Column("phone")
@Column(name = "phone")
private String phone;
@Column("picture")
@Column(name = "picture")
private String picture;
@Column("nationality")
@Column(name = "nationality")
private String nationality;

// Required by Spring Data JDBC to instantiate the entity via reflection
// Required by JPA
public User() {
// No initialization needed; fields are populated by Spring Data JDBC after instantiation
// No initialization needed
}

public Long getId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter;
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository;
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserSpecifications;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import com.xpeho.spring_boot_java_random_user.domain.services.LocalUserService;
import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -47,4 +49,11 @@ public UserEntity save(UserEntity user) {
public void deleteById(long id) {
userRepository.deleteById(id);
}

@Override
public List<UserEntity> filterUsers(UserFilter filter) {
return userRepository.findAll(UserSpecifications.byFilter(filter)).stream()
.map(userConverter::toDomain)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.xpeho.spring_boot_java_random_user.data.sources.database;

import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserRepository extends CrudRepository<User, Long> {
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.xpeho.spring_boot_java_random_user.data.sources.database;

import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;

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

public final class UserSpecifications {
private UserSpecifications() {
}

public static Specification<User> byFilter(UserFilter filter) {
return (user, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();

if (filter.gender() != null) {
predicates.add(criteriaBuilder.equal(
criteriaBuilder.lower(user.get("gender")),
filter.gender().name().toLowerCase()
));
}

addContainsPredicate(predicates, criteriaBuilder, user.get("firstname"), filter.firstname());
addContainsPredicate(predicates, criteriaBuilder, user.get("lastname"), filter.lastname());
addContainsPredicate(predicates, criteriaBuilder, user.get("civility"), filter.civility());
addContainsPredicate(predicates, criteriaBuilder, user.get("email"), filter.email());
addContainsPredicate(predicates, criteriaBuilder, user.get("phone"), filter.phone());
addContainsPredicate(predicates, criteriaBuilder, user.get("nationality"), filter.nat());

return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}

private static void addContainsPredicate(
List<Predicate> predicates,
CriteriaBuilder criteriaBuilder,
Path<String> field,
String value
) {
if (!StringUtils.hasText(value)) {
return;
}

predicates.add(criteriaBuilder.like(
criteriaBuilder.lower(field),
"%" + value.toLowerCase() + "%"
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.xpeho.spring_boot_java_random_user.domain.entities;

import com.xpeho.spring_boot_java_random_user.domain.enums.Gender;

public record UserFilter(
Gender gender,
String firstname,
String lastname,
String civility,
String email,
String phone,
String nat
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.xpeho.spring_boot_java_random_user.domain.enums;

public enum Gender {
MALE,
FEMALE
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.xpeho.spring_boot_java_random_user.domain.services;

import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;

import java.util.List;
import java.util.Optional;
Expand All @@ -13,5 +14,7 @@ public interface LocalUserService {
UserEntity save(UserEntity user);

void deleteById(long id);

List<UserEntity> filterUsers(UserFilter filter);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.xpeho.spring_boot_java_random_user.domain.usecases;

import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import com.xpeho.spring_boot_java_random_user.domain.services.LocalUserService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class FilterUsersUseCase {
private final LocalUserService userService;

public FilterUsersUseCase(LocalUserService userService) {
this.userService = userService;
}

public List<UserEntity> execute(UserFilter filter) {
return userService.filterUsers(filter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest;
import com.xpeho.spring_boot_java_random_user.domain.enums.Gender;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
import com.xpeho.spring_boot_java_random_user.presentation.dto.UserResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -13,6 +14,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;



@RequestMapping("/random-users")
Expand Down Expand Up @@ -92,6 +95,32 @@ ResponseEntity<UserEntity> updateRandomUser(
ResponseEntity<UserEntity> createUser(@RequestBody UserRequest user);


@GetMapping("/filter")
@Operation(
summary = "Filter users",
description = "Search users by optional filters on gender, firstname, lastname, civility, email, phone and nationality. All filters are case-insensitive and support partial matching.",
parameters = {
@Parameter(name = "gender", description = "Filter by gender (MALE or FEMALE)"),
@Parameter(name = "firstname", description = "Filter by firstname"),
@Parameter(name = "lastname", description = "Filter by lastname"),
@Parameter(name = "civility", description = "Filter by civility"),
@Parameter(name = "email", description = "Filter by email"),
@Parameter(name = "phone", description = "Filter by phone"),
@Parameter(name = "nat", description = "Filter by nationality")
}
)
@ApiResponse(responseCode = "200", description = "Filtered list of users")
@ApiResponse(responseCode = "500", description = "Internal server error")
ResponseEntity<List<UserEntity>> filterUsers(
@RequestParam(required = false) Gender gender,
@RequestParam(required = false) String firstname,
@RequestParam(required = false) String lastname,
@RequestParam(required = false) String civility,
@RequestParam(required = false) String email,
@RequestParam(required = false) String phone,
@RequestParam(required = false) String nat
);

@DeleteMapping("/{id}")
@Operation(
summary = "Delete user by id",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.xpeho.spring_boot_java_random_user.domain.entities.PaginatedUsers;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserRequest;
import com.xpeho.spring_boot_java_random_user.domain.enums.Gender;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
import com.xpeho.spring_boot_java_random_user.domain.exceptions.InvalidPaginationException;
import com.xpeho.spring_boot_java_random_user.domain.exceptions.UserNotFoundException;
Expand All @@ -17,6 +19,7 @@
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;


@RestController
Expand All @@ -30,20 +33,22 @@ public class UserHandler implements UserController {
private final GetUserByIdUseCase getUserByIdUseCase;
private final CreateUserUseCase createUserUseCase;
private final DeleteUserByIdUseCase deleteUserUseCase;
private final FilterUsersUseCase filterUsersUseCase;

public UserHandler(
FetchAndSaveRandomUsersUseCase fetchAndSaveRandomUsersUseCase,
UpdateRandomUserUseCase updateRandomUserUseCase,
GetUserByIdUseCase getUserByIdUseCase,
CreateUserUseCase createUserUseCase,
DeleteUserByIdUseCase deleteUserUseCase
DeleteUserByIdUseCase deleteUserUseCase,
FilterUsersUseCase filterUsersUseCase
) {
this.fetchAndSaveRandomUsersUseCase = fetchAndSaveRandomUsersUseCase;
this.updateRandomUserUseCase = updateRandomUserUseCase;
this.getUserByIdUseCase = getUserByIdUseCase;
this.createUserUseCase = createUserUseCase;
this.deleteUserUseCase = deleteUserUseCase;

this.filterUsersUseCase = filterUsersUseCase;
}


Expand Down Expand Up @@ -99,6 +104,16 @@ public ResponseEntity<UserEntity> createUser(@RequestBody UserRequest user) {
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}

@Override
public ResponseEntity<List<UserEntity>> filterUsers(
Gender gender, String firstname, String lastname,
String civility, String email, String phone, String nat
) {
UserFilter filter = new UserFilter(gender, firstname, lastname, civility, email, phone, nat);
List<UserEntity> users = filterUsersUseCase.execute(filter);
return ResponseEntity.ok(users);
}

@Override
public void deleteUserById(int id) {
try {
Expand Down
Loading
Loading