Skip to content

Commit a4d2b91

Browse files
authored
Merge pull request #455 from Daggerpov/simplify-profile-editing-apis
refactor (profile)!: Simplify profile editing (user interests) apis
2 parents 097e8f0 + e052df9 commit a4d2b91

5 files changed

Lines changed: 157 additions & 203 deletions

File tree

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.danielagapov.spawn.user.api;
22

3-
import com.danielagapov.spawn.shared.exceptions.Logger.ILogger;
43
import com.danielagapov.spawn.user.internal.services.IUserInterestService;
5-
import com.danielagapov.spawn.shared.util.LoggingUtils;
64
import org.springframework.beans.factory.annotation.Autowired;
75
import org.springframework.http.HttpStatus;
86
import org.springframework.http.ResponseEntity;
@@ -16,57 +14,46 @@
1614
public class UserInterestController {
1715

1816
private final IUserInterestService userInterestService;
19-
private final ILogger logger;
2017

2118
@Autowired
22-
public UserInterestController(IUserInterestService userInterestService, ILogger logger) {
19+
public UserInterestController(IUserInterestService userInterestService) {
2320
this.userInterestService = userInterestService;
24-
this.logger = logger;
2521
}
2622

2723
@GetMapping
2824
public ResponseEntity<List<String>> getUserInterests(@PathVariable UUID userId) {
29-
try {
30-
List<String> interests = userInterestService.getUserInterests(userId);
31-
interests = interests.stream()
32-
.map(interest -> interest.replaceAll("^\"|\"$", ""))
33-
.toList();
34-
return ResponseEntity.ok(interests);
35-
} catch (Exception e) {
36-
logger.error("Error getting user interests for user: " + LoggingUtils.formatUserIdInfo(userId) + ": " + e.getMessage());
37-
throw e;
38-
}
25+
List<String> interests = userInterestService.getUserInterests(userId);
26+
return ResponseEntity.ok(interests);
27+
}
28+
29+
@PutMapping
30+
public ResponseEntity<List<String>> replaceUserInterests(
31+
@PathVariable UUID userId,
32+
@RequestBody List<String> interests) {
33+
List<String> saved = userInterestService.replaceUserInterests(userId, interests);
34+
return ResponseEntity.ok(saved);
3935
}
4036

4137
@PostMapping
4238
public ResponseEntity<String> addUserInterest(
4339
@PathVariable UUID userId,
4440
@RequestBody String userInterestName) {
45-
userInterestName = userInterestName.replaceAll("^\"|\"$", "");
46-
try {
47-
String result = userInterestService.addUserInterest(userId, userInterestName);
48-
return new ResponseEntity<>(result, HttpStatus.CREATED);
49-
} catch (Exception e) {
50-
logger.error("Error adding user interest for user: " + LoggingUtils.formatUserIdInfo(userId) + " - interest: " + userInterestName + ": " + e.getMessage());
51-
throw e;
41+
// @RequestBody with a plain JSON string arrives wrapped in quotes; strip them.
42+
String cleaned = userInterestName.replaceAll("^\"|\"$", "").trim();
43+
if (cleaned.isEmpty()) {
44+
return ResponseEntity.badRequest().build();
5245
}
46+
String result = userInterestService.addUserInterest(userId, cleaned);
47+
return new ResponseEntity<>(result, HttpStatus.CREATED);
5348
}
5449

5550
@DeleteMapping("/{interest}")
5651
public ResponseEntity<Void> removeUserInterest(
5752
@PathVariable UUID userId,
5853
@PathVariable String interest) {
59-
try {
60-
boolean removed = userInterestService.removeUserInterest(userId, interest);
61-
62-
if (removed) {
63-
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
64-
} else {
65-
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
66-
}
67-
} catch (Exception e) {
68-
logger.error("Error removing user interest for user: " + LoggingUtils.formatUserIdInfo(userId) + " - interest: " + interest + ": " + e.getMessage());
69-
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
70-
}
54+
boolean removed = userInterestService.removeUserInterest(userId, interest);
55+
return removed
56+
? new ResponseEntity<>(HttpStatus.NO_CONTENT)
57+
: new ResponseEntity<>(HttpStatus.NOT_FOUND);
7158
}
7259
}

src/main/java/com/danielagapov/spawn/user/internal/repositories/UserInterestRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import com.danielagapov.spawn.user.internal.domain.UserInterest;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58
import org.springframework.stereotype.Repository;
69

710
import java.util.List;
@@ -12,4 +15,11 @@
1215
public interface UserInterestRepository extends JpaRepository<UserInterest, UUID> {
1316
List<UserInterest> findByUserId(UUID userId);
1417
Optional<UserInterest> findByUserIdAndInterest(UUID userId, String interest);
18+
19+
@Query("SELECT ui FROM UserInterest ui WHERE ui.user.id = :userId AND LOWER(ui.interest) = LOWER(:interest)")
20+
Optional<UserInterest> findByUserIdAndInterestIgnoreCase(@Param("userId") UUID userId, @Param("interest") String interest);
21+
22+
@Modifying
23+
@Query("DELETE FROM UserInterest ui WHERE ui.user.id = :userId")
24+
void deleteAllByUserId(@Param("userId") UUID userId);
1525
}

src/main/java/com/danielagapov/spawn/user/internal/services/IUserInterestService.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,8 @@
44
import java.util.UUID;
55

66
public interface IUserInterestService {
7-
/**
8-
* Get all interests for a user
9-
* @param userId The ID of the user
10-
* @return A list of user interest strings
11-
*/
127
List<String> getUserInterests(UUID userId);
13-
14-
/**
15-
* Add a new interest for a user
16-
* @param user id and interest name
17-
* @return The created user interest string
18-
*/
198
String addUserInterest(UUID userId, String interestName);
20-
21-
/**
22-
* Remove an interest for a user
23-
* @param userId The ID of the user
24-
* @param encodedInterestName The URL-encoded name of the interest to remove
25-
* @return true if the interest was successfully removed, false if not found
26-
*/
27-
boolean removeUserInterest(UUID userId, String encodedInterestName);
9+
boolean removeUserInterest(UUID userId, String interestName);
10+
List<String> replaceUserInterests(UUID userId, List<String> interests);
2811
}

src/main/java/com/danielagapov/spawn/user/internal/services/UserInterestService.java

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@
1010
import org.springframework.cache.annotation.CacheEvict;
1111
import org.springframework.cache.annotation.Cacheable;
1212
import org.springframework.stereotype.Service;
13+
import org.springframework.transaction.annotation.Transactional;
1314

14-
import java.net.URLDecoder;
15-
import java.nio.charset.StandardCharsets;
16-
import java.util.List;
17-
import java.util.Optional;
18-
import java.util.UUID;
15+
import java.util.*;
1916
import java.util.stream.Collectors;
2017

2118
@Service
@@ -35,63 +32,64 @@ public UserInterestService(UserInterestRepository userInterestRepository, IUserR
3532
@Override
3633
@Cacheable(value = "userInterests", key = "#userId")
3734
public List<String> getUserInterests(UUID userId) {
38-
List<UserInterest> interests = userInterestRepository.findByUserId(userId);
39-
return interests.stream()
35+
return userInterestRepository.findByUserId(userId).stream()
4036
.map(UserInterest::getInterest)
4137
.collect(Collectors.toList());
4238
}
4339

4440
@Override
4541
@CacheEvict(value = "userInterests", key = "#userId")
4642
public String addUserInterest(UUID userId, String interestName) {
43+
String trimmed = interestName.trim();
44+
45+
Optional<UserInterest> existing = userInterestRepository.findByUserIdAndInterestIgnoreCase(userId, trimmed);
46+
if (existing.isPresent()) {
47+
return existing.get().getInterest();
48+
}
49+
4750
User user = userRepository.findById(userId)
4851
.orElseThrow(() -> new RuntimeException("User not found with id: " + userId));
4952

50-
UserInterest userInterest = new UserInterest(user, interestName);
53+
UserInterest userInterest = new UserInterest(user, trimmed);
5154
userInterest = userInterestRepository.save(userInterest);
52-
5355
return userInterest.getInterest();
5456
}
5557

5658
@Override
5759
@CacheEvict(value = "userInterests", key = "#userId")
58-
public boolean removeUserInterest(UUID userId, String encodedInterestName) {
59-
try {
60-
// URL decode the interest name to handle spaces and special characters
61-
String decodedInterest = URLDecoder.decode(encodedInterestName, StandardCharsets.UTF_8);
62-
63-
logger.info("Attempting to remove interest '" + decodedInterest + "' (encoded: '" + encodedInterestName + "') for user: " + LoggingUtils.formatUserIdInfo(userId));
64-
65-
// Debug: Log all existing interests for this user
66-
List<UserInterest> allUserInterests = userInterestRepository.findByUserId(userId);
67-
logger.info("User " + LoggingUtils.formatUserIdInfo(userId) + " currently has " + allUserInterests.size() + " interests:");
68-
for (UserInterest existingInterest : allUserInterests) {
69-
logger.info(" - '" + existingInterest.getInterest() + "' (length: " + existingInterest.getInterest().length() + ")");
70-
}
71-
72-
Optional<UserInterest> userInterestOpt = userInterestRepository.findByUserIdAndInterest(userId, decodedInterest);
73-
74-
if (userInterestOpt.isPresent()) {
75-
userInterestRepository.delete(userInterestOpt.get());
76-
logger.info("Successfully removed interest '" + decodedInterest + "' for user: " + LoggingUtils.formatUserIdInfo(userId));
77-
return true;
78-
} else {
79-
logger.warn("Interest '" + decodedInterest + "' not found for user: " + LoggingUtils.formatUserIdInfo(userId));
80-
logger.warn("Exact search failed. Trying case-insensitive search...");
81-
82-
// Try case-insensitive search for debugging
83-
for (UserInterest existingInterest : allUserInterests) {
84-
if (existingInterest.getInterest().equalsIgnoreCase(decodedInterest)) {
85-
logger.warn("Found case-insensitive match: '" + existingInterest.getInterest() + "' vs '" + decodedInterest + "'");
86-
break;
87-
}
88-
}
89-
90-
return false;
60+
public boolean removeUserInterest(UUID userId, String interestName) {
61+
// Spring already URL-decodes @PathVariable, so no manual decoding needed.
62+
// Use case-insensitive lookup to be resilient to casing mismatches.
63+
Optional<UserInterest> userInterestOpt = userInterestRepository.findByUserIdAndInterestIgnoreCase(userId, interestName);
64+
65+
if (userInterestOpt.isPresent()) {
66+
userInterestRepository.delete(userInterestOpt.get());
67+
return true;
68+
}
69+
70+
logger.warn("Interest '" + interestName + "' not found for user: " + LoggingUtils.formatUserIdInfo(userId));
71+
return false;
72+
}
73+
74+
@Override
75+
@Transactional
76+
@CacheEvict(value = "userInterests", key = "#userId")
77+
public List<String> replaceUserInterests(UUID userId, List<String> interests) {
78+
User user = userRepository.findById(userId)
79+
.orElseThrow(() -> new RuntimeException("User not found with id: " + userId));
80+
81+
userInterestRepository.deleteAllByUserId(userId);
82+
83+
List<String> saved = new ArrayList<>();
84+
Set<String> seen = new HashSet<>();
85+
for (String interest : interests) {
86+
String trimmed = interest.trim();
87+
if (!trimmed.isEmpty() && seen.add(trimmed.toLowerCase())) {
88+
UserInterest entity = new UserInterest(user, trimmed);
89+
userInterestRepository.save(entity);
90+
saved.add(trimmed);
9191
}
92-
} catch (Exception e) {
93-
logger.error("Error removing interest '" + encodedInterestName + "' for user: " + LoggingUtils.formatUserIdInfo(userId) + ": " + e.getMessage());
94-
throw new RuntimeException("Failed to remove user interest", e);
9592
}
93+
return saved;
9694
}
9795
}

0 commit comments

Comments
 (0)