From a105d2c9e9914466d9252ab4e2b4c1bd5236dd8f Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 2 May 2025 17:17:27 +0200 Subject: [PATCH] added authentication --- pom.xml | 27 ++++++--- .../it/cnr/isti/workflow/manager/JwtUtil.java | 52 ++++++++++++++++ .../workflow/manager/TransformerService.java | 1 - .../manager/WorkflowManagerApplication.java | 11 +++- .../JwtAuthenticationFilter.java | 56 ++++++++++++++++++ .../configurations/SecurityConfig.java | 42 +++++++++---- .../manager/configurations/WebConfig.java | 3 +- .../manager/controllers/AuthController.java | 59 +++++++++++++++++++ .../manager/executors/ai/GeminiModel.java | 1 + .../workflow/manager/model/AuthRequest.java | 20 +++++++ .../manager/model/ChangePasswordRequest.java | 18 ++++++ .../workflow/manager/model/ExecutionStep.java | 26 +++++--- .../manager/model/entities/LoginEntity.java | 17 ++++++ .../manager/repositories/AuthRepository.java | 12 ++++ .../manager/services/AuthService.java | 47 +++++++++++++++ src/main/resources/saved-node-type.json | 2 +- 16 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 src/main/java/it/cnr/isti/workflow/manager/JwtUtil.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/configurations/JwtAuthenticationFilter.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/model/AuthRequest.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/model/ChangePasswordRequest.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/model/entities/LoginEntity.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/repositories/AuthRepository.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/services/AuthService.java diff --git a/pom.xml b/pom.xml index 5f337c2..6eccd8b 100644 --- a/pom.xml +++ b/pom.xml @@ -36,19 +36,32 @@ org.springframework.boot spring-boot-starter-data-jpa - + org.keycloak keycloak-spring-boot-starter 24.0.1 - + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + org.postgresql @@ -89,7 +102,7 @@ com.h2database h2 test - + diff --git a/src/main/java/it/cnr/isti/workflow/manager/JwtUtil.java b/src/main/java/it/cnr/isti/workflow/manager/JwtUtil.java new file mode 100644 index 0000000..2ad2637 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/JwtUtil.java @@ -0,0 +1,52 @@ +package it.cnr.isti.workflow.manager; + +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import it.cnr.isti.workflow.manager.model.entities.LoginEntity; + +@Component +public class JwtUtil { + + SecretKey secretKey = Keys.hmacShaKeyFor("la-tua-chiave-segreta-di-almeno-32-byte!".getBytes()); + + public String generateToken(String username) { + String jwt = Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 giorno + .signWith(secretKey, SignatureAlgorithm.HS256) // Usa una SecretKey (HMAC) + .compact(); + return jwt; + } + + public String extractUsername(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public boolean validateToken(String token, LoginEntity userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + Date expiration = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .getExpiration(); + return expiration.before(new Date()); + } +} \ No newline at end of file diff --git a/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java b/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java index d678784..a7ace77 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java @@ -16,7 +16,6 @@ import it.cnr.isti.workflow.manager.model.ExecutionStep; import it.cnr.isti.workflow.manager.model.types.NodeDefinition; import it.cnr.isti.workflow.manager.model.ui.Connection; import it.cnr.isti.workflow.manager.model.ui.Flow; -import it.cnr.isti.workflow.manager.model.ui.IOModel; import it.cnr.isti.workflow.manager.model.ui.Node; import it.cnr.isti.workflow.manager.repositories.NodeDefinitionRepository; diff --git a/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java b/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java index 8907b7f..4c2a113 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java +++ b/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java @@ -13,6 +13,7 @@ import org.springframework.util.ResourceUtils; import com.fasterxml.jackson.databind.ObjectMapper; import it.cnr.isti.workflow.manager.executors.ai.AIModel; +import it.cnr.isti.workflow.manager.model.entities.LoginEntity; import it.cnr.isti.workflow.manager.model.types.IOType; import it.cnr.isti.workflow.manager.model.types.Key; import it.cnr.isti.workflow.manager.model.types.NodeDefinition; @@ -21,6 +22,7 @@ import it.cnr.isti.workflow.manager.model.types.ParameterType; import it.cnr.isti.workflow.manager.model.types.Translator; import it.cnr.isti.workflow.manager.model.ui.Flow; import it.cnr.isti.workflow.manager.model.ui.FlowEntity; +import it.cnr.isti.workflow.manager.repositories.AuthRepository; import it.cnr.isti.workflow.manager.repositories.FlowRepository; import it.cnr.isti.workflow.manager.repositories.NodeDefinitionRepository; @@ -36,7 +38,7 @@ public class WorkflowManagerApplication { @Bean @ConditionalOnProperty(prefix = "app", name = "db.init.enabled", havingValue = "true") - CommandLineRunner init(NodeDefinitionRepository repository, FlowRepository flowRepository) { + CommandLineRunner init(NodeDefinitionRepository repository, FlowRepository flowRepository, AuthRepository authRepository) { return args -> { JSONObject rPars = new JSONObject().put("options", @@ -69,6 +71,8 @@ public class WorkflowManagerApplication { loadNodeDefinitionAndSave(repository); loadFlowsAndSave(flowRepository); + + insertUser(authRepository); }; } @@ -101,4 +105,9 @@ public class WorkflowManagerApplication { } } + private void insertUser(AuthRepository repository) { + LoginEntity le = new LoginEntity("lucio", "lucio"); + repository.save(le); + } + } diff --git a/src/main/java/it/cnr/isti/workflow/manager/configurations/JwtAuthenticationFilter.java b/src/main/java/it/cnr/isti/workflow/manager/configurations/JwtAuthenticationFilter.java new file mode 100644 index 0000000..bae2da5 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/configurations/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package it.cnr.isti.workflow.manager.configurations; + +import java.io.IOException; +import java.util.Collections; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import it.cnr.isti.workflow.manager.JwtUtil; +import it.cnr.isti.workflow.manager.model.entities.LoginEntity; +import it.cnr.isti.workflow.manager.repositories.AuthRepository; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private AuthRepository authRepository; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String jwt = authHeader.substring(7); + String username = jwtUtil.extractUsername(jwt); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + LoginEntity userDetails = authRepository.findById(username).orElse(null); + + if (jwtUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, Collections.emptyList()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/configurations/SecurityConfig.java b/src/main/java/it/cnr/isti/workflow/manager/configurations/SecurityConfig.java index 93a4e8f..d4e96fe 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/configurations/SecurityConfig.java +++ b/src/main/java/it/cnr/isti/workflow/manager/configurations/SecurityConfig.java @@ -1,22 +1,40 @@ package it.cnr.isti.workflow.manager.configurations; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -//import org.springframework.security.config.annotation.web.builders.HttpSecurity; -//import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@Configuration +@EnableWebSecurity public class SecurityConfig { -/* + + @Autowired + private JwtAuthenticationFilter jwtFilter; + @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authz -> authz - .requestMatchers("/public/**").permitAll() + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .cors(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/**").permitAll() .anyRequest().authenticated() ) - .oauth2ResourceServer(oauth2 -> oauth2.jwt()); - - return http.build(); + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .build(); } -*/ -} + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} \ No newline at end of file diff --git a/src/main/java/it/cnr/isti/workflow/manager/configurations/WebConfig.java b/src/main/java/it/cnr/isti/workflow/manager/configurations/WebConfig.java index e3fb1e8..d85995d 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/configurations/WebConfig.java +++ b/src/main/java/it/cnr/isti/workflow/manager/configurations/WebConfig.java @@ -19,7 +19,6 @@ public class WebConfig implements WebMvcConfigurer { registry.addMapping("/**") .allowedOrigins(origins) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); + .allowedHeaders("*"); } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java new file mode 100644 index 0000000..49f827f --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java @@ -0,0 +1,59 @@ +package it.cnr.isti.workflow.manager.controllers; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import it.cnr.isti.workflow.manager.JwtUtil; +import it.cnr.isti.workflow.manager.model.AuthRequest; +import it.cnr.isti.workflow.manager.model.ChangePasswordRequest; +import it.cnr.isti.workflow.manager.services.AuthService; + +import java.util.Collections; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AuthController.class); + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthRequest request) { + logger.info("Login attempt for user: {}", request.getUsername()); + if (!authService.validateUser(request.getUsername(), request.getPassword())) { + return ResponseEntity.status(401).body("Invalid credentials"); + } + String jwt = jwtUtil.generateToken(request.getUsername()); + return ResponseEntity.ok(Collections.singletonMap("token", jwt)); + + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody AuthRequest request) { + if (authService.registerUser(request.getUsername(), request.getPassword())) { + return ResponseEntity.ok().build(); + } + return ResponseEntity.badRequest().body("the user already exists"); + } + + @PostMapping("/change-password") + public ResponseEntity changePassword(@RequestBody ChangePasswordRequest request) { + if (authService.changePassword(request.getUsername(), request.getOldPassword(), request.getNewPassword())) { + return ResponseEntity.ok().build(); + } + return ResponseEntity.badRequest().body("Change password failed"); + } +} + diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java index 7501394..79956e4 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java @@ -46,6 +46,7 @@ public class GeminiModel implements AIModel { // Deserializzazione della risposta JSON in un oggetto GeminiResponse GeminiResponse response = objectMapper.readValue(result, GeminiResponse.class); + // Estrazione del testo dal primo candidato String candidateText = response.getCandidates().get(0).getContent().getParts().get(0).getText(); diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/AuthRequest.java b/src/main/java/it/cnr/isti/workflow/manager/model/AuthRequest.java new file mode 100644 index 0000000..109bb32 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/model/AuthRequest.java @@ -0,0 +1,20 @@ +package it.cnr.isti.workflow.manager.model; + + +public class AuthRequest { + private String username; + private String password; + + // Getters e Setters + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + boolean isValid() { + return username.contains(" ") && username != null && !username.isEmpty() && username.length()>5 && + password != null && username.contains(" ") && password.length()>5 && !password.isEmpty(); + } +} + diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/ChangePasswordRequest.java b/src/main/java/it/cnr/isti/workflow/manager/model/ChangePasswordRequest.java new file mode 100644 index 0000000..2807b38 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/model/ChangePasswordRequest.java @@ -0,0 +1,18 @@ +package it.cnr.isti.workflow.manager.model; + + +public class ChangePasswordRequest { + private String username; + private String oldPassword; + private String newPassword; + + // Getters e Setters + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getOldPassword() { return oldPassword; } + public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; } + + public String getNewPassword() { return newPassword; } + public void setNewPassword(String newPassword) { this.newPassword = newPassword; } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java index f793c47..686102e 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; @@ -78,16 +79,25 @@ public class ExecutionStep implements InputReadyListener { if (areAllInputsReady()) new Thread(() -> { this.context.getStepsUnderExecution().add(this.id); + log.info("starting executing of step {}", this.nodeDefinition.getType()); try { Map realInputs = prepareInputs(); //log.debug("input translation for step {} is {}", id, realInputs); Map returned = executor.execute(this.runtimeParameters, null, realInputs); - this.context.getNodeResult().put(this.id, returned); - for (InputReadyListener nextStep : nextSteps) - for (Map.Entry entry : returned.entrySet()) - nextStep.inputReady(!nodeDefinition.getExecutorToOutputTranslationMappings().isEmpty() - ? nodeDefinition.getExecutorToOutputTranslationMappings().get(entry.getKey()) - : entry.getKey(), entry.getValue()); + + //mapping executor output to current node output + Map mappedReturn = returned.entrySet().stream().collect(Collectors.toMap( + e -> { return !nodeDefinition.getExecutorToOutputTranslationMappings().isEmpty() + ? nodeDefinition.getExecutorToOutputTranslationMappings().get(e.getKey()) + : e.getKey(); + }, + e -> e.getValue())); + + context.nodeResult.put(this.id, mappedReturn); + + //sendig output to next steps + nextSteps.forEach(n -> mappedReturn.forEach((k, v) -> n.inputReady(k,v))); + } catch (ExecutionException e) { log.error("error during execution of step {}: {}", id, e.getMessage()); this.context.getErrors().add(String.format("[%S] %s", this.id, e.getMessage())); @@ -95,7 +105,9 @@ public class ExecutionStep implements InputReadyListener { return; } finally { this.context.getStepsUnderExecution().remove(this.id); - } + log.info("finished executing of step {}", this.nodeDefinition.getType()); + } + }).start(); else log.debug("not all inputs are ready for step {}", id); diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/entities/LoginEntity.java b/src/main/java/it/cnr/isti/workflow/manager/model/entities/LoginEntity.java new file mode 100644 index 0000000..9176446 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/model/entities/LoginEntity.java @@ -0,0 +1,17 @@ +package it.cnr.isti.workflow.manager.model.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginEntity { + @Id + private String username; + private String password; +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/repositories/AuthRepository.java b/src/main/java/it/cnr/isti/workflow/manager/repositories/AuthRepository.java new file mode 100644 index 0000000..1855817 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/repositories/AuthRepository.java @@ -0,0 +1,12 @@ +package it.cnr.isti.workflow.manager.repositories; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import it.cnr.isti.workflow.manager.model.entities.LoginEntity; + +@Repository +public interface AuthRepository extends JpaRepository { + +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/services/AuthService.java b/src/main/java/it/cnr/isti/workflow/manager/services/AuthService.java new file mode 100644 index 0000000..b4f78d2 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/services/AuthService.java @@ -0,0 +1,47 @@ +package it.cnr.isti.workflow.manager.services; + + +import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule.Auth; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import it.cnr.isti.workflow.manager.model.entities.LoginEntity; +import it.cnr.isti.workflow.manager.repositories.AuthRepository; + + +@Service +public class AuthService { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AuthService.class); + + @Autowired + AuthRepository authRepository; + + public boolean validateUser(String username, String password) { + LoginEntity le = authRepository.findById(username).orElse(null); + if (le == null) { + return false; // User not found + } + return le.getPassword().equals(password); // Validate password + } + + public boolean registerUser(String username, String password) { + if (authRepository.existsById(username)) { + return false; // User already exists + } + authRepository.save(new LoginEntity(username, password)); + return true; + } + + public boolean changePassword(String username, String oldPassword, String newPassword) { + return authRepository.findById(username) + .filter(user -> user.getPassword().equals(oldPassword)) + .map(user -> { + user.setPassword(newPassword); + authRepository.save(user); + return true; + }) + .orElse(false); + } +} diff --git a/src/main/resources/saved-node-type.json b/src/main/resources/saved-node-type.json index 338bbba..994d98f 100644 --- a/src/main/resources/saved-node-type.json +++ b/src/main/resources/saved-node-type.json @@ -256,7 +256,7 @@ "inputToExecutorTranslationMappings": { }, "inputTranslators": { "prompt": { - "translation": "Act as a **Senior Software Engineer** specializing in **model-driven development**. Generate clean, maintainable code that:\n1. Implements all validated requirements (from CSV) \n2. Matches the structure of the provided class diagram \n3. Follows the interaction flows from the sequence diagram \n\n**Input:** \n1. requirements (ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. class diagram (PlantUML) :\n\n${{class-diagram}}\n\n3. sequence diagram (PlantUML) :\n\n\n\n\n**Output Rules:** \n- **Language**: Python\n- **Architecture**: \n - 1:1 mapping of PlantUML classes to code \n - Sequence diagram steps → method logic \n- **Quality**: \n - Type hints (Python)\n - Docstrings referencing requirement IDs (e.g., `# F1`) \n - Appropriate error handling \n- **Format**:\n - Output only executable code, do not add any text, besides comments\n\n**Example Output (Python Calculator):** \n\nfrom enum import Enum, auto\n\nclass OperationType(Enum):\n \"\"\"Supported operations (matches class diagram <>)\"\"\"\n ADD = auto() # F3: Basic arithmetic\n SUBTRACT = auto() # F3\n MULTIPLY = auto() # F3\n DIVIDE = auto() # F3\n\nclass Calculator:\n \"\"\"Core calculator logic (from class diagram)\"\"\"\n def __init__(self):\n self.current_input = 0 # F1: Digit entry\n self.stored_value = None # For multi-step operations\n self.selected_op = None # F3: Operation selection\n \n def calculate(self) -> float:\n \"\"\"Execute the current operation (sequence diagram '=' press)\"\"\"\n if not self.selected_op:\n return self.current_input\n \n # Sequence diagram compute step\n match self.selected_op:\n case OperationType.ADD:\n result = self.stored_value + self.current_input\n case OperationType.SUBTRACT:\n result = self.stored_value - self.current_input\n # ... other operations (F3)\n \n self.current_input = result\n return result\n\nclass InputHandler:\n \"\"\"Manages user input flow (sequence diagram interactions)\"\"\"\n def __init__(self, calculator: Calculator, display: Display):\n self.calc = calculator\n self.display = display\n \n def handle_digit(self, digit: int): # F1\n \"\"\"Process digit press (sequence diagram step 1)\"\"\"\n self.calc.current_input = self.calc.current_input * 10 + digit\n self.display.update(str(self.calc.current_input))\n\n def handle_operator(self, op: OperationType): # F3\n \"\"\"Process +,-,*,/ press (sequence diagram step 2)\"\"\"\n self.calc.stored_value = self.calc.current_input\n self.calc.selected_op = op\n self.calc.current_input = 0\n" + "translation": "Act as a **Senior Software Engineer** specializing in **model-driven development**. Generate clean, maintainable code that:\n1. Implements all validated requirements (from CSV) \n2. Matches the structure of the provided class diagram \n3. Follows the interaction flows from the sequence diagram \n\n**Input:** \n1. requirements (ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. class diagram (PlantUML) :\n\n${{class-diagram}}\n\n3. sequence diagram (PlantUML) :\n\n${{sequence-diagram}}\n\n**Output Rules:** \n- **Language**: Python\n- **Architecture**: \n - 1:1 mapping of PlantUML classes to code \n - Sequence diagram steps → method logic \n- **Quality**: \n - Type hints (Python)\n - Docstrings referencing requirement IDs (e.g., `# F1`) \n - Appropriate error handling \n- **Format**:\n - Output only executable code, do not add any text, besides comments\n\n**Example Output (Python Calculator):** \n\nfrom enum import Enum, auto\n\nclass OperationType(Enum):\n \"\"\"Supported operations (matches class diagram <>)\"\"\"\n ADD = auto() # F3: Basic arithmetic\n SUBTRACT = auto() # F3\n MULTIPLY = auto() # F3\n DIVIDE = auto() # F3\n\nclass Calculator:\n \"\"\"Core calculator logic (from class diagram)\"\"\"\n def __init__(self):\n self.current_input = 0 # F1: Digit entry\n self.stored_value = None # For multi-step operations\n self.selected_op = None # F3: Operation selection\n \n def calculate(self) -> float:\n \"\"\"Execute the current operation (sequence diagram '=' press)\"\"\"\n if not self.selected_op:\n return self.current_input\n \n # Sequence diagram compute step\n match self.selected_op:\n case OperationType.ADD:\n result = self.stored_value + self.current_input\n case OperationType.SUBTRACT:\n result = self.stored_value - self.current_input\n # ... other operations (F3)\n \n self.current_input = result\n return result\n\nclass InputHandler:\n \"\"\"Manages user input flow (sequence diagram interactions)\"\"\"\n def __init__(self, calculator: Calculator, display: Display):\n self.calc = calculator\n self.display = display\n \n def handle_digit(self, digit: int): # F1\n \"\"\"Process digit press (sequence diagram step 1)\"\"\"\n self.calc.current_input = self.calc.current_input * 10 + digit\n self.display.update(str(self.calc.current_input))\n\n def handle_operator(self, op: OperationType): # F3\n \"\"\"Process +,-,*,/ press (sequence diagram step 2)\"\"\"\n self.calc.stored_value = self.calc.current_input\n self.calc.selected_op = op\n self.calc.current_input = 0\n" } } }