Import an attendee list into an event
Java · Spring Boot · beginner · greenfield
Organisers have been adding attendees one at a time; this adds a bulk import. `AttendeeImportController` exposes `POST /api/events/{eventId}/attendees/import`, validates the body and checks the caller organises the event. `Attendee` is the request value object, with the bean-validation rules for an entry and value equality on the email. `AttendeeImportService` drops repeated people (keeping the first entry), filters out emails already registered for the event, and saves the rest in one transaction.
Event registration backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL 16, Spring Security. Every `/api/**` request needs a signed-in session; `@AuthenticationPrincipal` resolves an `Organiser(long id, String email)` record. Existing and unchanged: `EventAccess.requireOrganiser(eventId, organiserId)` throws a 404 `ResponseStatusException` unless the event exists and the caller organises it. The `Registration` entity (`id`, `eventId`, `email`, `name`) maps the `registrations` table, which has a unique constraint on `(event_id, email)`; every write path stores `email` lower-cased, so the constraint also rejects case variants. `RegistrationRepository.findEmailsByEventId(eventId)` returns the event's stored (lower-cased) emails as a `Set<String>`. An existing `@RestControllerAdvice` turns `DataIntegrityViolationException` into a 409 with "The attendee list changed while you were importing, try again", for the rare case of two organisers registering the same person at the same moment; a 409 from that race is acceptable. Bean-validation failures on `@Valid @RequestBody` become 400. An event has at most a few thousand registrations.
Requirements
- `POST /api/events/{eventId}/attendees/import` takes a JSON body `{"attendees": [{"email": ..., "name": ...}, ...]}` with 1 to 1,000 entries, sent by the event's organiser (anyone else gets 404). The organiser pastes it together from several sources (a sign-up sheet, a partner's list, last year's guests), so the same person can appear more than once.
- Each `email` is printable ASCII with no spaces, at most 254 characters and a valid address per `@Email`; each `name` is at most 100 UTF-16 code units (what `@Size` counts, so a character outside the Basic Multilingual Plane counts as two) and not blank, where blank means empty or only whitespace as `String.isBlank()` defines it (so a name of only U+3000 ideographic spaces is blank). A request that breaks any of these gets 400 and nothing is stored; the body is validated before the organiser check, so an invalid body gets 400 whoever sends it.
- Two entries are the same attendee when their emails are equal ignoring letter case (`[email protected]` and `[email protected]` are one person). Each attendee is registered once, from their first entry in the list, with the email lower-cased and the name stripped of surrounding whitespace (`String.strip()`, the same whitespace definition as `isBlank()`). Later entries for the same person are skipped, and so is anyone already registered for the event.
- The response is `{"added": n, "skipped": m}`, where `n + m` is the number of entries sent. An import of a list with repeated people must succeed like any other: the repeats are counted in `skipped`, never an error.
Files touched
- src/main/java/com/acme/events/attendees/Attendee.java
- src/main/java/com/acme/events/attendees/AttendeeImportService.java
- src/main/java/com/acme/events/attendees/AttendeeImportController.java
--- src/main/java/com/acme/events/attendees/Attendee.java
+package com.acme.events.attendees;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+import java.util.Locale;
+
+public final class Attendee {
+
+ @NotBlank
+ @Email
+ @Pattern(regexp = "[\\x21-\\x7E]+")
+ @Size(max = 254)
+ private final String email;
+
+ @NotNull
+ @Size(max = 100)
+ private final String name;
+
+ @JsonCreator
+ public Attendee(@JsonProperty("email") String email, @JsonProperty("name") String name) {
+ this.email = email;
+ this.name = name;