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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
application-local.properties
.env
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
Expand Down
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ cp src/test/resources/application-test.properties.template src/test/resources/ap
```

> ⚠️ This file is also git-ignored.
But for credentials and local overrides, use:

```
src/main/resources/application-local.properties
```

### Example for application.properties

```properties
spring.application.name=spring_boot_java_random_user
springdoc.swagger-ui.path=/api
spring.datasource.url=jdbc:postgresql://localhost:5432/<database_name>
spring.datasource.driver-class-name=org.postgresql.Driver
```

### Local credentials & security

To avoid exposing database credentials in source code, create a `.env` file at the project root.

Then, in `src/main/resources/application-local.properties`:

```properties
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
Comment thread
Theo-lbg marked this conversation as resolved.
```

Both `.env` and `application-local.properties` are in `.gitignore` (already set).

Comment thread
Theo-lbg marked this conversation as resolved.
To activate the local profile in IntelliJ or VS Code, add this environment variable to your run configuration:

```
SPRING_PROFILES_ACTIVE=local
```

This way, each developer can use their own credentials without risk of leaking them to GitHub.


> ⚠️ **Common startup error**:
> ```
> Failed to configure a DataSource: 'url' attribute is not specified
> and no embedded datasource could be configured.
> Reason: Failed to determine a suitable driver class
> ```
> This error occurs because the PostgreSQL driver is present in the dependencies but the datasource URL is not configured.
> **Fix**: either add the `spring.datasource.*` properties above, or exclude the DataSource auto-configuration if no database is needed:
> ```properties
> spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
> ```

---

Expand Down Expand Up @@ -231,11 +279,11 @@ This project consumes the public **Random User Generator** API:

- [x] [Add Sonarqube in the project](https://github.com/XPEHO/spring_boot_java_random_user/issues/2)
- [x] [Add PostgreSQL database with docker](https://github.com/XPEHO/spring_boot_java_random_user/issues/6)
- [ ] [Add this endpoint GET /user/random](https://github.com/XPEHO/spring_boot_java_random_user/issues/5)
- [ ] [Add this endpoint GET /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/8)
- [ ] [Add this endpoint PUT /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/9)
- [ ] [Add this endpoint DELETE /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/10)
- [ ] [Add this endpoint POST /user](https://github.com/XPEHO/spring_boot_java_random_user/issues/11)
- [X] [Add this endpoint get /user/random](https://github.com/XPEHO/spring_boot_java_random_user/issues/5)

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README marks GET /user/random as implemented, but the only controller mapping introduced is /random-users (see @RequestMapping("/random-users")). Please update either the API route or this checklist entry so they match.

Suggested change
- [X] [Add this endpoint get /user/random](https://github.com/XPEHO/spring_boot_java_random_user/issues/5)
- [X] [Add this endpoint get /random-users](https://github.com/XPEHO/spring_boot_java_random_user/issues/5)

Copilot uses AI. Check for mistakes.
- [ ] [Add this endpoint get /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/8)
- [ ] [Add this endpoint put /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/9)
- [ ] [Add this endpoint delete /user/{id}](https://github.com/XPEHO/spring_boot_java_random_user/issues/10)
- [ ] [Add this endpoint post /user](https://github.com/XPEHO/spring_boot_java_random_user/issues/11)
Comment thread
Theo-lbg marked this conversation as resolved.

---

Expand Down
6 changes: 5 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
Expand All @@ -50,7 +55,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.xpeho.spring_boot_java_random_user.data.converters;
import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.data.models.db.User;
import org.springframework.stereotype.Service;


@Service
public class UserConverter {
// Domain -> DAO
public User toDao(UserEntity entity) {
User user = new User();
user.setId(entity.id());
user.setGender(entity.gender());
user.setFirstname(entity.firstname());
user.setLastname(entity.lastname());
user.setCivility(entity.civility());
user.setEmail(entity.email());
user.setPhone(entity.phone());
user.setPicture(entity.picture());
user.setNationality(entity.nat());
return user;
}
// DAO -> Domain
public UserEntity toDomain(User user) {
return new UserEntity(
Comment thread
Theo-lbg marked this conversation as resolved.
user.getId(),
user.getGender(),
user.getFirstname(),
user.getLastname(),
user.getCivility(),
user.getEmail(),
user.getPhone(),
user.getPicture(),
user.getNationality()
);
Comment thread
Theo-lbg marked this conversation as resolved.
}
// API -> Domain
public UserEntity fromApiModel(RandomUserResultDAO model) {
return new UserEntity(
null,
model.getGender(),
model.getFirstName(),
model.getLastName(),
null,
model.getEmail(),
model.getPhone(),
model.getImage(),
null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.xpeho.spring_boot_java_random_user.data.models.api;

public class RandomUserNameDAO {
Comment thread
Theo-lbg marked this conversation as resolved.
private String title;
private String first;
private String last;

public String getTitle() { return title; }
Comment on lines +3 to +8

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API model class appears unused in the production code (only referenced by its own test). If the project is now using the DummyJSON /users schema (flat firstName/lastName/image), consider removing this class (and its test) to avoid dead code, or wire it into the actual API response model if it's still needed.

Copilot uses AI. Check for mistakes.
public void setTitle(String title) { this.title = title; }
public String getFirst() { return first; }
public void setFirst(String first) { this.first = first; }
public String getLast() { return last; }
public void setLast(String last) { this.last = last; }
}
Comment thread
Theo-lbg marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.xpeho.spring_boot_java_random_user.data.models.api;

public class RandomUserPictureDAO {
private String medium;

public String getMedium() {
Comment on lines +3 to +6

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API model class appears unused in the production code (only referenced by its own test). If the project is now using the DummyJSON /users schema (single image field), consider removing this class (and its test) to avoid dead code, or integrate it into the API response model if it is still intended to be used.

Copilot uses AI. Check for mistakes.
return medium;
}

public void setMedium(String medium) {
this.medium = medium;
}
}
Comment thread
Theo-lbg marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.xpeho.spring_boot_java_random_user.data.models.api;

import com.google.gson.annotations.SerializedName;
import java.util.List;

public class RandomUserResponse {
@SerializedName("users")
private List<RandomUserResultDAO> users;

public List<RandomUserResultDAO> getUsers() {
return users;
}

public void setUsers(List<RandomUserResultDAO> users) {
this.users = users;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.xpeho.spring_boot_java_random_user.data.models.api;

public class RandomUserResultDAO {
Comment thread
Theo-lbg marked this conversation as resolved.
private String gender;
private String firstName;
private String lastName;
private String email;
private String phone;
private String image;

public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getImage() { return image; }
public void setImage(String image) { this.image = image; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.xpeho.spring_boot_java_random_user.data.models.db;

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

@Table("users")
Comment thread
Theo-lbg marked this conversation as resolved.
public class User {
@Id
private Long id;
private String gender;
@Column("firstname")
private String firstname;
@Column("lastname")
private String lastname;
private String civility;
private String email;
private String phone;
private String picture;
private String nationality;

Comment thread
Theo-lbg marked this conversation as resolved.
// Required by Spring Data JDBC to instantiate the entity via reflection
public User() {
// No initialization needed; fields are populated by Spring Data JDBC after instantiation
}

public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getFirstname() { return firstname; }
public void setFirstname(String firstname) { this.firstname = firstname; }
public String getLastname() { return lastname; }
public void setLastname(String lastname) { this.lastname = lastname; }
public String getCivility() { return civility; }
public void setCivility(String civility) { this.civility = civility; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getPicture() { return picture; }
public void setPicture(String picture) { this.picture = picture; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.xpeho.spring_boot_java_random_user.data.services;

import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.services.UserService;
import com.xpeho.spring_boot_java_random_user.data.models.db.User;
import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.StreamSupport;

@Service

public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserConverter userConverter;

public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
this.userRepository = userRepository;
this.userConverter = userConverter;
}

@Override
public List<UserEntity> saveAll(List<UserEntity> users) {
Comment thread
Theo-lbg marked this conversation as resolved.

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveAll will throw a NullPointerException if users is null (users.stream()). Either enforce non-null at the service boundary (e.g., validate and throw an IllegalArgumentException) or make this method null-safe (e.g., treat null as an empty list) so the contract is explicit and consistent with callers/tests.

Suggested change
public List<UserEntity> saveAll(List<UserEntity> users) {
public List<UserEntity> saveAll(List<UserEntity> users) {
if (users == null) {
throw new IllegalArgumentException("users must not be null");
}

Copilot uses AI. Check for mistakes.
List<User> daoUsers = users.stream().map(userConverter::toDao).toList();
Iterable<User> saved = userRepository.saveAll(daoUsers);
Comment thread
Theo-lbg marked this conversation as resolved.
return StreamSupport.stream(saved.spliterator(), false)
.map(userConverter::toDomain)
.toList();
}
Comment thread
Theo-lbg marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.xpeho.spring_boot_java_random_user.data.sources.api;

import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface RandomUserApi {
@GET("/users")
Call<RandomUserResponse> getRandomUsers(@Query("limit") int limit);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.xpeho.spring_boot_java_random_user.data.sources.api;

import okhttp3.OkHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

@Configuration
public class RandomUserApiConfig {
@Bean
public Retrofit randomUserRetrofit(Environment env) {
String baseUrl = env.getProperty("randomuser.api.base-url", "https://dummyjson.com/");
OkHttpClient client = new OkHttpClient.Builder().build();
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
Comment thread
Theo-lbg marked this conversation as resolved.
.client(client)
.build();
}

@Bean
public RandomUserApi randomUserApi(Retrofit randomUserRetrofit) {
return randomUserRetrofit.create(RandomUserApi.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.xpeho.spring_boot_java_random_user.data.sources.api;

import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResponse;
import com.xpeho.spring_boot_java_random_user.data.models.api.RandomUserResultDAO;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.services.RandomUserProvider;
import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter;
import org.springframework.stereotype.Service;
import retrofit2.Response;

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

@Service
public class RandomUserProviderImpl implements RandomUserProvider {
private final RandomUserApi randomUserApi;
private final UserConverter userConverter;

public RandomUserProviderImpl(RandomUserApi randomUserApi, UserConverter userConverter) {
this.randomUserApi = randomUserApi;
this.userConverter = userConverter;
}

@Override
public List<UserEntity> fetchRandomUsers(int count) throws IOException {
Response<RandomUserResponse> response = randomUserApi.getRandomUsers(count).execute();
if (!response.isSuccessful() || response.body() == null) {
throw new IOException("Failed to fetch users: " + response.code());
}
List<RandomUserResultDAO> users = response.body().getUsers();
if (users == null) {
Comment thread
Theo-lbg marked this conversation as resolved.
throw new IOException("Failed to parse users from response");
}
return users.stream()
.map(userConverter::fromApiModel)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xpeho.spring_boot_java_random_user.data.sources.database;

import com.xpeho.spring_boot_java_random_user.data.models.db.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {
}
Loading