Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f6b6255
feat: add fields validation
xertrec Mar 2, 2026
4131745
test: add fields validation tests
xertrec Mar 2, 2026
068cd70
Merge pull request #2 from MireiaTerri/domain-entities-basic-field-va…
xertrec Mar 2, 2026
da735a9
Merge branch 'UdL-EPS-SoftArch-Igualada:main' into main
xertrec Mar 2, 2026
4e3a058
test: fix ScientificProject constructor to make invalid a null in the…
xertrec Mar 3, 2026
a08a0b2
refactor: remove constructor without params for entities
xertrec Mar 3, 2026
e7d28ce
feat: remove invalid constructor in team
xertrec Mar 3, 2026
155c687
feat: add validation of min foundationYear
xertrec Mar 3, 2026
6d90d32
feat: add validation of past birthDate
xertrec Mar 3, 2026
87be9d6
feat: add extra validations
xertrec Mar 3, 2026
39cdd65
refactor: rename to make it more readable
xertrec Mar 3, 2026
5a615c6
refactor: delete extra separation in imports
xertrec Mar 3, 2026
c0d85bf
fix: fix codeRabbit warnings
xertrec Mar 3, 2026
f5bff48
fix: fix codeRabbit warnings
xertrec Mar 3, 2026
c40a777
feat: remove empty constructor
xertrec Mar 3, 2026
30874ff
test: test fix
xertrec Mar 3, 2026
0091b0e
test: test fix
xertrec Mar 3, 2026
615c396
feat: change visibility constructor
xertrec Mar 7, 2026
30355a9
feat: add more validations
xertrec Mar 7, 2026
5eac2c5
Merge branch 'main' into main
xertrec Mar 7, 2026
827bf95
fix: fix constructor errors
xertrec Mar 7, 2026
87d8e4c
test: test fix
xertrec Mar 7, 2026
f3893be
feat: add judge secure constructor
xertrec Mar 7, 2026
f3b8df2
Update src/main/java/cat/udl/eps/softarch/fll/domain/Team.java
xertrec Mar 7, 2026
f2d38af
feat: consolidation functions
xertrec Mar 7, 2026
3ba7869
feat: CodeRabbit fixes
xertrec Mar 7, 2026
0858be6
Merge branch 'main' of https://github.com/MireiaTerri/FLL_backend
xertrec Mar 7, 2026
3b92d00
feat: CodeRabbit fixes
xertrec Mar 7, 2026
ed6cbf2
fix: fix compilation issue
xertrec Mar 7, 2026
45fe6ae
feat: improved validations
xertrec Mar 7, 2026
135bfdc
fix: fixes
xertrec Mar 7, 2026
4c75b7d
fix: fixes
xertrec Mar 7, 2026
f418645
fix: fixes
xertrec Mar 7, 2026
c66c952
fix: test fixes
xertrec Mar 9, 2026
2ed55ec
Merge branch 'main' into main
xertrec Mar 9, 2026
9709558
Merge branch 'main' of https://github.com/MireiaTerri/FLL_backend
xertrec Mar 9, 2026
d073cf4
fix: fix compile issues
xertrec Mar 9, 2026
97df320
fix: fix compile issues
xertrec Mar 9, 2026
31d0e72
fix: fix compile issues
xertrec Mar 9, 2026
6b0a202
fix: fix non-implementation issue
xertrec Mar 9, 2026
477969c
Merge remote-tracking branch 'upstream/main'
xertrec Mar 10, 2026
624070e
fix: fix equals and hash code for Match
xertrec Mar 11, 2026
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
12 changes: 12 additions & 0 deletions src/main/java/cat/udl/eps/softarch/fll/domain/Award.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public class Award extends UriEntity<Long> {
@JsonIdentityReference(alwaysAsId = true)
private Team winner;

public static Award create(String name, Edition edition, Team winner) {
DomainValidation.requireNonBlank(name, "name");
DomainValidation.requireNonNull(edition, "edition");
DomainValidation.requireNonNull(winner, "winner");

Award award = new Award();
award.name = name;
award.edition = edition;
award.winner = winner;
return award;
Comment on lines +53 to +62
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

Award.create(...) validates inputs, but Award still has a public no-arg constructor and Lombok-generated setters via @Data, so invalid awards (blank name, null edition/winner) can still be instantiated and mutated directly, bypassing the new validation. To fully enforce the invariant at creation time, consider making the no-arg constructor protected and validating/removing setters for mandatory fields.

Copilot uses AI. Check for mistakes.
}

@Override
public Long getId() {
return this.id;
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/cat/udl/eps/softarch/fll/domain/Coach.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package cat.udl.eps.softarch.fll.domain;

import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand All @@ -13,6 +11,8 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
Expand All @@ -30,11 +30,21 @@ public class Coach extends UriEntity<Integer> {
@NotBlank
@Email
@Column(unique = true)

private String emailAddress;

private String phoneNumber;

@ManyToMany(mappedBy = "trainedBy")
@ToString.Exclude
private Set<Team> teams = new HashSet<>();

public static Coach create(String name, String emailAddress) {
DomainValidation.requireNonBlank(name, "name");
DomainValidation.requireValidEmail(emailAddress, "emailAddress");

Coach coach = new Coach();
coach.name = name;
coach.emailAddress = emailAddress;
return coach;
}
Comment on lines +41 to +49
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

Coach.create(...) adds validation, but the class still has an implicit public no-arg constructor plus Lombok-generated setters (@Data), so callers can still instantiate new Coach() with null/blank required fields and bypass domain invariants. If the goal is to prevent invalid entities from being created, consider making the no-arg constructor protected (for JPA only) and validating required setters (or removing the setters for required fields).

Copilot uses AI. Check for mistakes.
}
75 changes: 41 additions & 34 deletions src/main/java/cat/udl/eps/softarch/fll/domain/CompetitionTable.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package cat.udl.eps.softarch.fll.domain;

import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
Expand All @@ -11,39 +9,56 @@
import jakarta.validation.constraints.Size;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "competition_tables")
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
public class CompetitionTable extends UriEntity<String> {

@Id
@EqualsAndHashCode.Include
private String id;

@OneToMany(mappedBy = "competitionTable", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@Getter
@OneToMany(mappedBy = "competitionTable", cascade = CascadeType.ALL)
@JsonManagedReference("table-matches")
@Setter(lombok.AccessLevel.NONE)
private List<Match> matches = new ArrayList<>();

@Getter
@OneToMany(mappedBy = "supervisesTable")
@Size(max = 3, message = "A table can have a maximum of 3 referees")
@JsonManagedReference("table-referees")
@Setter(lombok.AccessLevel.NONE)
private List<Referee> referees = new ArrayList<>();

public static CompetitionTable create(String id) {
DomainValidation.requireNonBlank(id, "id");
CompetitionTable table = new CompetitionTable();
table.id = id;
return table;
Comment on lines +32 to +36
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

Adding CompetitionTable.create(...) introduces validation, but CompetitionTable still has a public no-arg constructor and an unvalidated setId(...) method, so new CompetitionTable() / setId(null|blank) can still create an invalid entity. To align with the domain-invariant goal, consider making the constructor protected and validating/removing setId (or routing it through DomainValidation.requireNonBlank).

Copilot uses AI. Check for mistakes.
}
Comment on lines +32 to +37
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

CompetitionTable.create(...) validates id, but the public setId(...) still allows setting it to null/blank afterward, bypassing the new domain invariant. Consider validating in setId (or restricting its visibility) so CompetitionTable cannot be moved into an invalid state from application code.

Copilot uses AI. Check for mistakes.

@Override
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public void setMatches(List<Match> matches) {
new ArrayList<>(this.matches).forEach(this::removeMatch);
if (matches != null) {
matches.forEach(this::addMatch);
}
}

public void setReferees(List<Referee> referees) {
new ArrayList<>(this.referees).forEach(this::removeReferee);
if (referees != null) {
referees.forEach(this::addReferee);
}
}

public void addMatch(Match match) {
if (match == null) {
return;
Expand All @@ -55,10 +70,11 @@ public void addMatch(Match match) {

CompetitionTable previousTable = match.getCompetitionTable();
if (previousTable != null && previousTable != this) {
previousTable.getMatches().removeIf(m -> m == match);
previousTable.removeMatch(match);
}
if (!matches.contains(match)) {
matches.add(match);
}

this.matches.add(match);
match.setCompetitionTable(this);
}

Expand All @@ -67,18 +83,8 @@ public void removeMatch(Match match) {
return;
}

if (this.matches.removeIf(m -> m == match)) {
match.setCompetitionTable(null);
}
}

public void setReferees(List<Referee> referees) {
if (referees == this.referees) {
return;
}
List<Referee> incoming = (referees == null) ? List.of() : new ArrayList<>(referees);
new ArrayList<>(this.referees).forEach(this::removeReferee);
incoming.forEach(this::addReferee);
matches.remove(match);
match.setCompetitionTable(null);
}

public void addReferee(Referee referee) {
Expand All @@ -96,10 +102,12 @@ public void addReferee(Referee referee) {

CompetitionTable previousTable = referee.getSupervisesTable();
if (previousTable != null && previousTable != this) {
previousTable.getReferees().removeIf(r -> r == referee);
previousTable.removeReferee(referee);
}

this.referees.add(referee);
if (referees.contains(referee)) {
return;
}
referees.add(referee);
referee.setSupervisesTable(this);
}

Expand All @@ -108,8 +116,7 @@ public void removeReferee(Referee referee) {
return;
}

if (this.referees.removeIf(r -> r == referee)) {
referee.setSupervisesTable(null);
}
referees.remove(referee);
referee.setSupervisesTable(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cat.udl.eps.softarch.fll.domain;

import java.time.LocalDate;
import java.util.regex.Pattern;

public final class DomainValidation {

private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[\\w\\-.]+@([\\w-]+\\.)+[\\w-]{2,}$");

private DomainValidation() {
}

public static void requireNonBlank(String value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.isBlank()) {
throw new DomainValidationException(fieldName + " must not be blank");
}
}

public static void requireValidEmail(String email, String fieldName) {
requireNonBlank(email, fieldName);
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new DomainValidationException(fieldName + " must be a valid email address");
}
}

public static void requireNonNegative(Integer value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value < 0) {
throw new DomainValidationException(fieldName + " must not be negative");
}
}

public static void requireMin(Integer value, Integer minValue, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value < minValue) {
throw new DomainValidationException(fieldName + " must not be less than " + minValue);
}
}

public static void requirePast(LocalDate value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (!value.isBefore(LocalDate.now())) {
throw new DomainValidationException(fieldName + " must be in the past");
}
}

public static void requireLengthBetween(String value, Integer minLength, Integer maxLength, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.length() < minLength) {
throw new DomainValidationException(fieldName + " length must not be less than " + minLength);
}
if (value.length() > maxLength) {
throw new DomainValidationException(fieldName + " length must not be more than " + maxLength);
}
}

public static void requireMaxLength(String value, Integer maxLength, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.length() > maxLength) {
throw new DomainValidationException(fieldName + " length must not be more than " + maxLength);
}
}

public static void requireNonNull(Object value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
Comment on lines +11 to +80
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

DomainValidation currently throws a message-only DomainValidationException (e.g., "name must not be blank"). Issue #35 asks for specific validation error categories (e.g., EMPTY_NAME, NULL_ID, NEGATIVE_SCORE), and the codebase already has a coded cat.udl.eps.softarch.fll.exception.DomainValidationException. Consider throwing a coded exception (or adding a code field here) so callers/controllers can reliably distinguish validation failures without parsing messages.

Suggested change
private DomainValidation() {
}
public static void requireNonBlank(String value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.isBlank()) {
throw new DomainValidationException(fieldName + " must not be blank");
}
}
public static void requireValidEmail(String email, String fieldName) {
requireNonBlank(email, fieldName);
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new DomainValidationException(fieldName + " must be a valid email address");
}
}
public static void requireNonNegative(Integer value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value < 0) {
throw new DomainValidationException(fieldName + " must not be negative");
}
}
public static void requireMin(Integer value, Integer minValue, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value < minValue) {
throw new DomainValidationException(fieldName + " must not be less than " + minValue);
}
}
public static void requirePast(LocalDate value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (!value.isBefore(LocalDate.now())) {
throw new DomainValidationException(fieldName + " must be in the past");
}
}
public static void requireLengthBetween(String value, Integer minLength, Integer maxLength, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.length() < minLength) {
throw new DomainValidationException(fieldName + " length must not be less than " + minLength);
}
if (value.length() > maxLength) {
throw new DomainValidationException(fieldName + " length must not be more than " + maxLength);
}
}
public static void requireMaxLength(String value, Integer maxLength, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
}
if (value.length() > maxLength) {
throw new DomainValidationException(fieldName + " length must not be more than " + maxLength);
}
}
public static void requireNonNull(Object value, String fieldName) {
if (value == null) {
throw new DomainValidationException(fieldName + " must not be null");
private static final String ERROR_CODE_NULL_VALUE = "NULL_VALUE";
private static final String ERROR_CODE_INVALID_EMAIL = "INVALID_EMAIL";
private static final String ERROR_CODE_NEGATIVE_VALUE = "NEGATIVE_VALUE";
private static final String ERROR_CODE_BELOW_MIN_VALUE = "BELOW_MIN_VALUE";
private static final String ERROR_CODE_VALUE_NOT_IN_PAST = "VALUE_NOT_IN_PAST";
private static final String ERROR_CODE_STRING_TOO_SHORT = "STRING_TOO_SHORT";
private static final String ERROR_CODE_STRING_TOO_LONG = "STRING_TOO_LONG";
private static final String ERROR_CODE_EXCEEDS_MAX_LENGTH = "EXCEEDS_MAX_LENGTH";
private static final String ERROR_CODE_NULL_OBJECT = "NULL_OBJECT";
private DomainValidation() {
}
private static void throwValidationException(String errorCode, String message) {
throw new DomainValidationException("[" + errorCode + "] " + message);
}
public static void requireNonBlank(String value, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (value.isBlank()) {
throwValidationException(ERROR_CODE_STRING_TOO_SHORT, fieldName + " must not be blank");
}
}
public static void requireValidEmail(String email, String fieldName) {
requireNonBlank(email, fieldName);
if (!EMAIL_PATTERN.matcher(email).matches()) {
throwValidationException(ERROR_CODE_INVALID_EMAIL, fieldName + " must be a valid email address");
}
}
public static void requireNonNegative(Integer value, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (value < 0) {
throwValidationException(ERROR_CODE_NEGATIVE_VALUE, fieldName + " must not be negative");
}
}
public static void requireMin(Integer value, Integer minValue, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (value < minValue) {
throwValidationException(ERROR_CODE_BELOW_MIN_VALUE, fieldName + " must not be less than " + minValue);
}
}
public static void requirePast(LocalDate value, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (!value.isBefore(LocalDate.now())) {
throwValidationException(ERROR_CODE_VALUE_NOT_IN_PAST, fieldName + " must be in the past");
}
}
public static void requireLengthBetween(String value, Integer minLength, Integer maxLength, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (value.length() < minLength) {
throwValidationException(ERROR_CODE_STRING_TOO_SHORT, fieldName + " length must not be less than " + minLength);
}
if (value.length() > maxLength) {
throwValidationException(ERROR_CODE_STRING_TOO_LONG, fieldName + " length must not be more than " + maxLength);
}
}
public static void requireMaxLength(String value, Integer maxLength, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_VALUE, fieldName + " must not be null");
}
if (value.length() > maxLength) {
throwValidationException(ERROR_CODE_EXCEEDS_MAX_LENGTH, fieldName + " length must not be more than " + maxLength);
}
}
public static void requireNonNull(Object value, String fieldName) {
if (value == null) {
throwValidationException(ERROR_CODE_NULL_OBJECT, fieldName + " must not be null");

Copilot uses AI. Check for mistakes.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cat.udl.eps.softarch.fll.domain;

public class DomainValidationException extends RuntimeException {

public DomainValidationException(String message) {
super(message);
}
}
Comment on lines +1 to +8
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

There is already a cat.udl.eps.softarch.fll.exception.DomainValidationException (with an error code) and a DomainValidationExceptionHandler that catches that type. Introducing a second DomainValidationException in the domain package means exceptions thrown by DomainValidation won’t be handled consistently (and may surface as 500s) and creates ambiguous imports. Prefer using a single exception type (reuse the existing one, or update the handler + callers to use this one) and keep a structured error code as required by #35.

Copilot uses AI. Check for mistakes.
25 changes: 20 additions & 5 deletions src/main/java/cat/udl/eps/softarch/fll/domain/Edition.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package cat.udl.eps.softarch.fll.domain;

import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -18,6 +16,8 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
Expand Down Expand Up @@ -48,13 +48,28 @@ public class Edition extends UriEntity<Long> {

@ManyToMany
@JoinTable(
name = "edition_teams",
joinColumns = @JoinColumn(name = "edition_id"),
inverseJoinColumns = @JoinColumn(name = "team_name"))
name = "edition_teams",
joinColumns = @JoinColumn(name = "edition_id"),
inverseJoinColumns = @JoinColumn(name = "team_name"))
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Set<Team> teams = new HashSet<>();

protected Edition() {
}

public static Edition create(Integer year, String venueName, String description) {
DomainValidation.requireNonNull(year, "year");
DomainValidation.requireNonBlank(venueName, "venueName");
DomainValidation.requireNonBlank(description, "description");

Edition edition = new Edition();
edition.year = year;
edition.venueName = venueName;
edition.description = description;
return edition;
}

public boolean hasReachedMaxTeams() {
return teams.size() >= MAX_TEAMS;
}
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/cat/udl/eps/softarch/fll/domain/Floater.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
package cat.udl.eps.softarch.fll.domain;

import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "floaters")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Floater extends Volunteer {


@NotBlank(message = "Student code is mandatory")
@Column(unique = true)
private String studentCode;

@ManyToMany(mappedBy = "floaters")
@ToString.Exclude
private Set<Team> assistedTeams = new HashSet<>();

public static Floater create(String name, String emailAddress, String phoneNumber, String studentCode) {
DomainValidation.requireNonBlank(studentCode, "studentCode");

Floater floater = new Floater();
floater.initFields(name, emailAddress, phoneNumber);
floater.studentCode = studentCode;
return floater;
}
}

Loading
Loading