Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

# Ignore env files
.env
.env.properties

### STS ###
.apt_generated
Expand Down
6 changes: 5 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ services:
- 'POSTGRES_USER=myuser'
ports:
- '5432:5432'

app:
build:
context: .
dockerfile: Dockerfile
image: 'catinder:latest'
Comment thread
LinusAltemark marked this conversation as resolved.
depends_on:
- postgres
image: 'catinder:latest'
env_file:
- .env
environment:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.example.springboot25.controller;

import org.example.springboot25.service.AiRecommendationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/ai-recommendations")
public class AiRecommendationRestController {

private final AiRecommendationService aiRecommendationService;

public AiRecommendationRestController(AiRecommendationService aiRecommendationService) {
this.aiRecommendationService = aiRecommendationService;
}

// Return recommendation based on cat breed with structured JSON and error handling
@GetMapping("/{breed}")
public ResponseEntity<?> getRecommendation(@PathVariable String breed) {
String recommendation = aiRecommendationService.getRecommendationForBreed(breed);

// If the breed is not recognized, return error message with 404
if (recommendation.contains("Unknown breed")) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Unknown cat breed");
errorResponse.put("message", recommendation);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

// Success: return breed and recommendation
Map<String, String> successResponse = new HashMap<>();
successResponse.put("breed", breed);
successResponse.put("recommendation", recommendation);
return ResponseEntity.ok(successResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.example.springboot25.controller;

import org.example.springboot25.service.AiRecommendationService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/ai-recommendations")
public class AiRecommendationViewController {

private final AiRecommendationService aiRecommendationService;

public AiRecommendationViewController(AiRecommendationService aiRecommendationService) {
this.aiRecommendationService = aiRecommendationService;
}

// Displays the form where the user can input the cat breed
@GetMapping
public String showForm() {
return "ai_recommendation_form";
}

@PostMapping
public String showResult(@RequestParam String breed, Model model) {
// Validate input: ensure the breed is not null or empty
if (breed == null || breed.trim().isEmpty()) {
model.addAttribute("error", "Please enter a breed name");
return "ai_recommendation_form";
}

breed = breed.trim();
String recommendation = aiRecommendationService.getRecommendationForBreed(breed);
model.addAttribute("breed", breed);
model.addAttribute("recommendation", recommendation);
return "ai_recommendation_result";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.example.springboot25.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@Service
public class AiRecommendationService {

@Value("${openai.api.key}")
private String apiKey;
Comment thread
LinusAltemark marked this conversation as resolved.

private final RestTemplate restTemplate = new RestTemplate();
private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";

public String getRecommendationForBreed(String breed) {
if (breed == null || breed.trim().isEmpty()) {
return "Unknown breed. Please enter a valid cat breed.";
}

breed = breed.trim();

Comment thread
LinusAltemark marked this conversation as resolved.
try {
// Prepare the OpenAI API request
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiKey);

Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", "Give me a short but useful care tip for the cat breed: " + breed);

Map<String, Object> body = new HashMap<>();
body.put("model", "gpt-3.5-turbo");
body.put("messages", new Object[]{message});
body.put("max_tokens", 50);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

ResponseEntity<Map> response = restTemplate.exchange(
OPENAI_API_URL,
HttpMethod.POST,
entity,
Map.class
);

if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
Map<String, Object> choice = ((java.util.List<Map<String, Object>>) response.getBody().get("choices")).get(0);
Map<String, Object> messageContent = (Map<String, Object>) choice.get("message");
return (String) messageContent.get("content");
} else {
return "Something went wrong while getting recommendation. Try again later.";
}
Comment on lines +51 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve response handling with proper model classes

The current response parsing uses multiple casts and map operations which is error-prone.

Create proper model classes for the OpenAI API response to make the code more maintainable and type-safe.

- if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
-     Map<String, Object> choice = ((java.util.List<Map<String, Object>>) response.getBody().get("choices")).get(0);
-     Map<String, Object> messageContent = (Map<String, Object>) choice.get("message");
-     return (String) messageContent.get("content");
- } else {
-     return "Something went wrong while getting recommendation. Try again later.";
- }

+ // Create response model classes
+ if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
+     try {
+         OpenAiResponse openAiResponse = objectMapper.convertValue(response.getBody(), OpenAiResponse.class);
+         if (openAiResponse.getChoices() != null && !openAiResponse.getChoices().isEmpty()) {
+             return openAiResponse.getChoices().get(0).getMessage().getContent();
+         }
+     } catch (Exception e) {
+         logger.error("Error parsing OpenAI response", e);
+     }
+ }
+ return "Something went wrong while getting recommendation. Try again later.";

And define these model classes:

public class OpenAiResponse {
    private List<OpenAiChoice> choices;
    // getters, setters
}

public class OpenAiChoice {
    private OpenAiMessage message;
    // getters, setters
}

public class OpenAiMessage {
    private String role;
    private String content;
    // getters, setters
}


} catch (Exception e) {
return "Error calling AI service: " + e.getMessage();
}
Comment on lines +59 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add logging for exceptions and sanitize error messages

The current exception handling includes the raw exception message in the response, which could expose sensitive information and doesn't provide logging for debugging.

Add proper logging and sanitize error messages:

+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;

@Service
public class AiRecommendationService {
+   private static final Logger logger = LoggerFactory.getLogger(AiRecommendationService.class);

    // Other code...

    } catch (Exception e) {
+       logger.error("Error calling OpenAI API for breed {}: {}", breed, e.getMessage(), e);
-       return "Error calling AI service: " + e.getMessage();
+       return "Error calling AI service. Please try again later.";
    }

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>AI Recommendation</title>
</head>
<body>
<h1>Cat AI Recommendation</h1>
<form action="#" th:action="@{/ai-recommendations}" method="post">
<label for="breed">Cat Breed:</label>
<input type="text" id="breed" name="breed" required>
<button type="submit">Get Recommendation</button>
</form>
Comment on lines +8 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve form usability with a dropdown menu instead of text input

Since you have a fixed set of cat breeds defined in the CatBreed enum, a dropdown would be more user-friendly than free text input, preventing invalid entries.

<form action="#" th:action="@{/ai-recommendations}" method="post">
    <label for="breed">Cat Breed:</label>
-    <input type="text" id="breed" name="breed" required>
+    <select id="breed" name="breed" required>
+        <option value="">-- Select a breed --</option>
+        <option th:each="breedOption : ${T(org.example.springboot25.entities.CatBreed).values()}" 
+                th:value="${breedOption.name()}" 
+                th:text="${breedOption.name().replace('_', ' ')}">
+        </option>
+    </select>
    <button type="submit">Get Recommendation</button>
</form>

This requires passing the CatBreed enum to the template from your controller:

@GetMapping
public String showForm(Model model) {
    return "ai_recommendation/ai_recommendation_form";
}

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Recommendation Result</title>
</head>
<body>
<h2>Recommendation for <span th:text="${breed}"></span>:</h2>
<p th:text="${recommendation}"></p>
<a href="/ai-recommendations">Try another</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use Thymeleaf for the back link

Using a direct link may cause issues if the application context path changes. Replace it with a Thymeleaf link.

-<a href="/ai-recommendations">Try another</a>
+<a th:href="@{/ai-recommendations}">Try another</a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href="/ai-recommendations">Try another</a>
<a th:href="@{/ai-recommendations}">Try another</a>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.example.springboot25.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

public class AiRecommendationServiceTest {

private AiRecommendationService aiRecommendationService;

@BeforeEach
void setUp() {
aiRecommendationService = new AiRecommendationService();
}

@Test
void shouldReturnCorrectRecommendationForValidBreed() {
String result = aiRecommendationService.getRecommendationForBreed("PERSIAN");
assertThat(result).contains("Groom daily");
}

@Test
void shouldReturnCorrectRecommendationForBreedWithSpaces() {
String result = aiRecommendationService.getRecommendationForBreed("american shorthair");
assertThat(result).contains("Easy-going");
}

@Test
void shouldReturnCorrectRecommendationForLowercaseInput() {
String result = aiRecommendationService.getRecommendationForBreed("siamese");
assertThat(result).contains("Talkative");
}

@Test
void shouldHandleUnknownBreedGracefully() {
String result = aiRecommendationService.getRecommendationForBreed("fluffybean");
assertThat(result).contains("Unknown breed");
}

@Test
void shouldHandleEmptyStringInput() {
String result = aiRecommendationService.getRecommendationForBreed("");
assertThat(result).contains("Unknown breed");
}

@Test
void shouldHandleWhitespaceOnlyInput() {
String result = aiRecommendationService.getRecommendationForBreed(" ");
assertThat(result).contains("Unknown breed");
}

@Test
void shouldHandleNullInput() {
String result = aiRecommendationService.getRecommendationForBreed(null);
assertThat(result).contains("Unknown breed");
}
}