auth readded

This commit is contained in:
Lucio Lelii 2025-08-08 13:24:33 +02:00
parent c4a1a7ce98
commit ea25273601
50 changed files with 822 additions and 675 deletions

View File

@ -3,6 +3,8 @@ package it.cnr.isti.workflow.manager;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import it.cnr.isti.workflow.manager.WorkflowManagerApplication;
public class ServletInitializer extends SpringBootServletInitializer {
@Override

View File

@ -1,14 +1,19 @@
package it.cnr.isti.workflow.manager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import it.cnr.isti.workflow.manager.auth.services.UserImportComponent;
@SpringBootApplication
public class WorkflowManagerApplication {
@Autowired
UserImportComponent userImportComponent;
public static void main(String[] args) {
SpringApplication.run(WorkflowManagerApplication.class, args);
@ -18,7 +23,8 @@ public class WorkflowManagerApplication {
@ConditionalOnProperty(prefix = "app", name = "db.init.enabled", havingValue = "true")
CommandLineRunner init() {
return args -> {
System.out.println("Initializing database...");
userImportComponent.start();
};
}

View File

@ -0,0 +1,14 @@
package it.cnr.isti.workflow.manager.app;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class ObjectMapperHolder {
public static ObjectMapper mapper;
public ObjectMapperHolder(ObjectMapper objectMapper) {
ObjectMapperHolder.mapper = objectMapper;
}
}

View File

@ -1,4 +1,4 @@
package it.cnr.isti.workflow.manager;
package it.cnr.isti.workflow.manager.app;
public interface Validator<T> {

View File

@ -0,0 +1,30 @@
package it.cnr.isti.workflow.manager.app.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
OpenAPI customOpenAPI() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.info(new Info().title("API Docs").version("v1"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
}

View File

@ -0,0 +1,35 @@
package it.cnr.isti.workflow.manager.app.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.validation.Validator;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${cors.allowed-origins}")
private String allowedOrigins;
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
String[] origins = allowedOrigins.split(",");
registry.addMapping("/**")
.allowedOrigins(origins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
@Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
}

View File

@ -0,0 +1,66 @@
package it.cnr.isti.workflow.manager.auth.config;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
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.auth.repo.AuthRepository;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
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;
private static final List<String> EXCLUDED_PATHS = List.of("/auth/", "/health");
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();
if (EXCLUDED_PATHS.stream().anyMatch(path::startsWith)) {
filterChain.doFilter(request, response);
return;
}
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);
}
}

View File

@ -0,0 +1,58 @@
package it.cnr.isti.workflow.manager.auth.config;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
@Component
public class JwtUtil {
SecretKey secretKey;
public JwtUtil( @Value("${app.security.key}") String secretKeyString) {
this.secretKey = Keys.hmacShaKeyFor(secretKeyString.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());
}
}

View File

@ -0,0 +1,46 @@
package it.cnr.isti.workflow.manager.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.method.configuration.EnableMethodSecurity;
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
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/swagger-ui.html").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

View File

@ -0,0 +1,24 @@
package it.cnr.isti.workflow.manager.auth.model;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
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();
}
}

View File

@ -0,0 +1,18 @@
package it.cnr.isti.workflow.manager.auth.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; }
}

View File

@ -0,0 +1,11 @@
package it.cnr.isti.workflow.manager.auth.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AuthRepository extends JpaRepository<LoginEntity, String> {
}

View File

@ -0,0 +1,17 @@
package it.cnr.isti.workflow.manager.auth.model;
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;
}

View File

@ -0,0 +1,50 @@
package it.cnr.isti.workflow.manager.auth.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import it.cnr.isti.workflow.manager.auth.repo.AuthRepository;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
import static it.cnr.isti.workflow.manager.auth.services.PasswordHasher.*;
@Service
public class AuthService {
@Autowired
AuthRepository authRepository;
public boolean validateUser(String username, String password) throws UsernameNotFoundException {
LoginEntity le = authRepository.findById(username).orElse(null);
if (le == null) {
throw new UsernameNotFoundException("User not found");
}
return checkPassword(password, le.getPassword()); // Validate password
}
public boolean registerUser(String username, String password) {
if (authRepository.existsById(username)) {
return false; // User already exists
}
String hashedPassword = hashPassword(password);
authRepository.save(new LoginEntity(username, hashedPassword));
return true;
}
public boolean changePassword(String username, String oldPassword, String newPassword) {
return authRepository.findById(username)
.filter(user -> checkPassword(oldPassword, user.getPassword()))
.map(user -> {
user.setPassword(hashPassword(newPassword));
authRepository.save(user);
return true;
})
.orElse(false);
}
}

View File

@ -0,0 +1,16 @@
package it.cnr.isti.workflow.manager.auth.services;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordHasher {
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public static String hashPassword(String plainPassword) {
return passwordEncoder.encode(plainPassword);
}
public static boolean checkPassword(String plainPassword, String hashedPassword) {
return passwordEncoder.matches(plainPassword, hashedPassword);
}
}

View File

@ -0,0 +1,69 @@
package it.cnr.isti.workflow.manager.auth.services;
import java.io.File;
import java.util.List;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.auth.repo.AuthRepository;
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
@Component
public class UserImportComponent {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(UserImportComponent.class);
private static final String USERS_FILE_NAME = "users.json";
@Autowired
AuthRepository authRepository;
@Value("${app.import.enabled}")
boolean enabled;
@Value("${app.import.path}")
String path;
public void start() {
if (!enabled) {
logger.warn("ImportComponent is disabled");
return;
}
File usersFile = new File(path, USERS_FILE_NAME);
if (usersFile.exists())
insertUser(usersFile);
else
logger.warn("Users file does not exist: {}", usersFile.getAbsolutePath());
}
private void insertUser(File file) {
try {
List<LoginEntity> users = ObjectMapperHolder.mapper.readValue(file,
ObjectMapperHolder.mapper.getTypeFactory().constructCollectionType(List.class, LoginEntity.class));
for (LoginEntity user : users) {
authRepository.findById(user.getUsername()).ifPresentOrElse(
existingUser -> {
logger.debug("User {} already present in the database, updating password",
user.getUsername());
existingUser.setPassword(PasswordHasher.hashPassword(user.getPassword()));
authRepository.save(existingUser);
},
() -> {
logger.debug("User {} not found, saving new user", user.getUsername());
user.setPassword(PasswordHasher.hashPassword(user.getPassword()));
authRepository.save(user);
});
}
} catch (Exception e) {
logger.error("Error loading users: {}", e.getMessage(), e);
}
}
}

View File

@ -1,5 +1,9 @@
package it.cnr.isti.workflow.manager.blocks.types;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
public interface BlockType {
String getName();
@ -8,5 +12,5 @@ public interface BlockType {
boolean validate();
String getBlockConfigurationSchema();
Class<? extends BlockConfiguration<?>> getBlockConfigurationClass();
}

View File

@ -2,6 +2,8 @@ package it.cnr.isti.workflow.manager.blocks.types;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
@Component(HumanInteractionBlockType.TYPE)
public class HumanInteractionBlockType implements BlockType {
@ -23,7 +25,7 @@ public class HumanInteractionBlockType implements BlockType {
}
@Override
public String getBlockConfigurationSchema() {
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return null;
}

View File

@ -2,6 +2,9 @@ package it.cnr.isti.workflow.manager.blocks.types;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
@Component(LLMBlockType.TYPE)
public class LLMBlockType implements BlockType {
@ -23,8 +26,8 @@ public class LLMBlockType implements BlockType {
}
@Override
public String getBlockConfigurationSchema() {
return "Inputs: AT_LEAST_ONE, Outputs: SINGLE, Models: SINGLE";
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return LLMBlockConfiguration.class;
}
}

View File

@ -2,6 +2,8 @@ package it.cnr.isti.workflow.manager.blocks.types;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
@Component(SourceBlockType.TYPE)
public class SourceBlockType implements BlockType {
@ -23,8 +25,8 @@ public class SourceBlockType implements BlockType {
}
@Override
public String getBlockConfigurationSchema() {
return null;
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return null; // Assuming no specific configuration class for SourceBlockType
}
}

View File

@ -10,15 +10,15 @@ import lombok.NoArgsConstructor;
public abstract class Brick {
private String id;
private String brickProvider;
private String brickManager;
protected Brick(String id, String brickProvider) {
this.brickProvider = brickProvider;
this.id = this.brickProvider + ":" + id;
public Brick(String id, String brickManager) {
this.brickManager = brickManager;
this.id = this.brickManager + ":" + id;
}
public String getBrickProvider() {
return brickProvider;
public String getBrickManager() {
return brickManager;
}
public Map<String, Object> getExecutionTimeConfiguration() {

View File

@ -8,8 +8,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public abstract class BrickManager<T extends Brick> {
public abstract List<String> possibleValues(String key);
public abstract List<T> getBricks();
}

View File

@ -7,8 +7,8 @@ public class LLMBrick extends Brick {
private final String model;
public LLMBrick(String provider, String model, String brickProvider) {
super(provider + "_" + model, brickProvider);
public LLMBrick(String provider, String model, String brickManager) {
super(provider + "_" + model, brickManager);
this.provider = provider;
this.model = model;
}

View File

@ -25,10 +25,5 @@ public class LLMBrickManager extends BrickManager<LLMBrick> {
.forEach(brick -> bricks.add(brick)));
return bricks;
}
public List<String> possibleValues(String key) {
return List.of("model1", "model2", "model3"); // Example values, replace with actual logic
}
}

View File

@ -1,5 +0,0 @@
package it.cnr.isti.workflow.manager.bricks.configurations;
public interface BrickConfiguration {
}

View File

@ -1,18 +0,0 @@
package it.cnr.isti.workflow.manager.bricks.configurations;
import it.cnr.isti.workflow.manager.schema.DynamicMultiValues;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class LLMBrickConfiguration implements BrickConfiguration {
@NotBlank
String provider;
@DynamicMultiValues(key = "models", provider = "llmBrickProvider")
String model;
}

View File

@ -0,0 +1,72 @@
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.auth.config.JwtUtil;
import it.cnr.isti.workflow.manager.auth.model.AuthRequest;
import it.cnr.isti.workflow.manager.auth.model.ChangePasswordRequest;
import it.cnr.isti.workflow.manager.auth.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.security.core.userdetails.UsernameNotFoundException;
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 (request.getUsername() == null || request.getPassword() == null) {
return ResponseEntity.badRequest().body("Username and password are required");
}
try {
if (!authService.validateUser(request.getUsername(), request.getPassword())) {
return ResponseEntity.status(401).body("Invalid password");
}
String jwt = jwtUtil.generateToken(request.getUsername());
return ResponseEntity.ok(Collections.singletonMap("token", jwt));
} catch (UsernameNotFoundException e) {
return ResponseEntity.status(404).body("User "+request.getUsername()+" not found");
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody AuthRequest request) {
logger.info("Register attempt for user {} with pwd {}", request.getUsername(), request.getPassword());
if (request.getUsername() == null || request.getPassword() == null) {
return ResponseEntity.badRequest().body("Username and password are required");
}
if (request.getUsername().length() < 3 || request.getPassword().length() < 6) {
return ResponseEntity.badRequest()
.body("Username must be at least 3 characters and password at least 6 characters");
}
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");
}
}

View File

@ -2,17 +2,16 @@ 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.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
import it.cnr.isti.workflow.manager.schema.JsonSchemaProducer;
import jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -29,22 +28,29 @@ public class BlocksController {
@Autowired
List<BlockFactory<?, ?>> blockFactories;
@GetMapping("")
@Autowired
JsonSchemaProducer schemaProducer;
@GetMapping("types")
public List<BlockType> getTypes() {
return new ArrayList<>(blockTypes.values());
}
@GetMapping("/{type}/configuration")
public String getConfigurations(@PathVariable String type) {
BlockType blockType = blockTypes.get(type);
if (blockType == null) {
throw new IllegalArgumentException("Block type not found: " + type);
}
return blockType.getBlockConfigurationSchema();
@GetMapping("/types/configurations/schemas")
public List<String> getSchemaConfigurations() {
return blockTypes.values().stream()
.map(b -> schemaProducer.generateSchema(b.getBlockConfigurationClass()))
.collect(Collectors.toList());
}
@GetMapping("/types/{type}/configuration/schemas")
public String getSchemaConfigurationForType(@PathVariable String type) {
return schemaProducer.generateSchema(
blockTypes.get(type).getBlockConfigurationClass());
}
@SuppressWarnings("unchecked")
@PostMapping()
@PostMapping
public <T extends BlockType, C extends BlockConfiguration<T>> Block<T, C> create(@RequestBody @Valid C blockConfiguration) {
BlockFactory<T, C> factory = (BlockFactory<T, C>) blockFactories.stream()
.filter(f -> f.getBlockType().equals(blockConfiguration.getType()))

View File

@ -9,8 +9,6 @@ import it.cnr.isti.workflow.manager.bricks.BrickManager;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

View File

@ -0,0 +1,21 @@
package it.cnr.isti.workflow.manager.controllers;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
@RestController
@RequestMapping("/flows")
public class FlowController {
@PostMapping
public ResponseEntity<FlowEntity> createFlow(@RequestBody FlowEntity flowEntity) {
FlowEntity createdFlow = flowService.createFlow(flowEntity);
}
}

View File

@ -0,0 +1,29 @@
package it.cnr.isti.workflow.manager.flows;
import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import it.cnr.isti.workflow.manager.flows.model.Flow;
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
@Service
public class FlowService {
@Autowired
FlowRepository flowRepository;
public FlowEntity createFlow(String name, String description, String owner, Flow flow) {
FlowEntity flowEntity = new FlowEntity();
LocalDateTime now = LocalDateTime.now();
flowEntity.setCreatedAt(now);
flowEntity.setLastUpdateAt(now);
flowEntity.setFlow(flow);
flowEntity.setOwner(owner);
flowEntity.setName(name);
flowEntity.setDescription(description);
return flowRepository.save(flowEntity);
}
}

View File

@ -1,4 +1,4 @@
package it.cnr.isti.workflow.manager.flows;
package it.cnr.isti.workflow.manager.flows.model;
import java.util.List;

View File

@ -0,0 +1,55 @@
package it.cnr.isti.workflow.manager.flows.repo;
import java.sql.Date;
import java.time.LocalDateTime;
import it.cnr.isti.workflow.manager.flows.model.Flow;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class FlowEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private String id;
@NotBlank
private String name;
private String description;
@NotBlank
@Column(nullable = false)
private LocalDateTime createdAt;
@NotBlank
@Column(nullable = false)
private LocalDateTime lastUpdateAt;
@NotBlank
private String owner;
@Builder.Default
private boolean published = false;
@Builder.Default
private boolean finalized = false;
@Lob
@Column(name = "flow_data", columnDefinition = "TEXT")
private Flow flow;
}

View File

@ -0,0 +1,16 @@
package it.cnr.isti.workflow.manager.flows.repo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface FlowRepository extends JpaRepository<FlowEntity, String> {
@Query("SELECT f FROM FlowEntity f WHERE f.owner = :owner OR f.published = true")
List<FlowEntity> findFlowsByOwnerOrPublic(@Param("owner") String owner);
}

View File

@ -18,6 +18,5 @@ public @interface DynamicMultiValues {
String message() default "value not valid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String key();
String provider();
String[] acceptedValues();
}

View File

@ -21,14 +21,13 @@ public class DynamicSchemaGenerator {
private final ObjectMapper objectMapper;
private final JsonSchemaGenerator baseGenerator;
private Map<String, BrickManager<?>> brickManagers;
public DynamicSchemaGenerator(@Autowired ObjectMapper objectMapper, @Autowired Map<String, BrickManager<?>> brickManagers) {
this.objectMapper = objectMapper;
public DynamicSchemaGenerator(@Autowired ObjectMapper objectMapper) {
JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4()
.withFailOnUnknownProperties(false);
this.baseGenerator = new JsonSchemaGenerator(objectMapper, config); // bean validation = true
this.brickManagers = brickManagers;
this.objectMapper = objectMapper;
}
public JsonNode generateJsonSchema(Class<?> clazz) {
@ -38,15 +37,14 @@ public class DynamicSchemaGenerator {
for (Field field : clazz.getDeclaredFields()) {
DynamicMultiValues annotation = field.getAnnotation(DynamicMultiValues.class);
if (annotation != null) {
String key = annotation.key();
String provider = annotation.provider();
List<String> enumValues = brickManagers.get(provider).possibleValues(key);
if (enumValues != null) {
String[] acceptedValues = annotation.acceptedValues();
if (acceptedValues != null) {
// Naviga nello schema e inietta "enum"
JsonNode properties = schema.get("properties");
if (properties != null && properties.has(field.getName())) {
ArrayNode enumArray = objectMapper.createArrayNode();
for (String value : enumValues) {
for (String value : acceptedValues) {
enumArray.add(value);
}
((com.fasterxml.jackson.databind.node.ObjectNode) properties.get(field.getName()))

View File

@ -3,6 +3,7 @@ package it.cnr.isti.workflow.manager.schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -15,8 +16,12 @@ public class JsonSchemaProducer {
@Autowired
private ObjectMapper mapper;
public String generateSchema(Class<?> clazz) throws Exception {
public String generateSchema(Class<?> clazz) {
JsonNode schema = schemaGen.generateJsonSchema(clazz);
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema);
try {
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error generating JSON schema for class: " + clazz.getName(), e);
}
}
}

View File

@ -14,8 +14,7 @@ import java.util.Map;
@Component
public class MultiValuesValidator implements ConstraintValidator<DynamicMultiValues, String> {
private String key;
private String provider;
private String[] acceptedValues;
private Map<String, BrickManager<?>> brickManagers;
@ -25,13 +24,11 @@ public class MultiValuesValidator implements ConstraintValidator<DynamicMultiVal
@Override
public void initialize(DynamicMultiValues annotation) {
this.key = annotation.key();
this.provider = annotation.provider();
this.acceptedValues = annotation.acceptedValues();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
List<String> allowed = brickManagers.get(provider).possibleValues(key);
return value == null || allowed.contains(value);
return value == null || List.of(acceptedValues).contains(value);
}
}

View File

@ -1,298 +0,0 @@
[
{
"createdBy": "user.test",
"name": "testFlow",
"description": null,
"nodes": [
{
"key": "e5f1f8d6-7717-4e32-ab57-f25f38f55401",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "2967ca92-3977-44ad-8e19-318b9a71d227",
"type": "text",
"name": "value"
}
],
"inputs": [],
"color": "#A8E6CF",
"position": {
"x": -270,
"y": -16
},
"parameters": {
"name": "interview\n"
},
"description": null,
"type": "Text Input"
},
{
"key": "1a4d1254-2c44-49fa-8aee-3865d1be9f95",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "f36c7ae1-78da-4d71-8b44-58a1f87f17cb",
"type": "text",
"name": "requirements"
}
],
"inputs": [
{
"key": "7860945a-506b-45a3-97b6-cbf08023e421",
"type": "text",
"name": "interview"
}
],
"color": "#A8E6CF",
"position": {
"x": -11,
"y": -23
},
"parameters": {
"LLM": "llama3.1:8b"
},
"description": null,
"type": "Requirement Generator"
},
{
"key": "394f3a84-f8b0-4cf6-92c7-042e6be03a26",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "a73eb6d8-9010-428e-a0cd-0831fcba6c42",
"type": "text",
"name": "corrected-requirements"
}
],
"inputs": [
{
"key": "e83aebf3-05dd-4588-b9a3-03fd7253e5fc",
"type": "text",
"name": "requirement-smells"
}
],
"color": "#A8E6CF",
"position": {
"x": 321,
"y": -21
},
"parameters": {
"LLM": "gemma3:12b"
},
"description": null,
"type": "Requirements Corrector"
},
{
"key": "511a2b09-dde6-4877-97ed-92819da4b778",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "96750771-855b-4013-9959-ac8b3fd17133",
"type": "text",
"name": "requirement-smells"
}
],
"inputs": [
{
"key": "059dad32-a3ea-4a34-b77e-f747c2baf864",
"type": "text",
"name": "requirements"
}
],
"color": "#A8E6CF",
"position": {
"x": 707,
"y": -22
},
"parameters": {
"LLM": "codellama:latest"
},
"description": null,
"type": "Requirements Analyzer"
}
],
"connections": [
{
"key": "f8b4dbaa-a9cc-4470-89a2-7b999d905d9b",
"from": "2967ca92-3977-44ad-8e19-318b9a71d227",
"to": "7860945a-506b-45a3-97b6-cbf08023e421"
},
{
"key": "9b2da750-f8c2-425a-a4c0-67869a60f69b",
"from": "f36c7ae1-78da-4d71-8b44-58a1f87f17cb",
"to": "e83aebf3-05dd-4588-b9a3-03fd7253e5fc"
},
{
"key": "ec5ed95b-e546-4ee2-957b-039649f3fac4",
"from": "a73eb6d8-9010-428e-a0cd-0831fcba6c42",
"to": "059dad32-a3ea-4a34-b77e-f747c2baf864"
}
],
"public": true
},
{
"createdBy": "lucio.lelii",
"name": "new flow",
"description": null,
"nodes": [
{
"key": "e5f1f8d6-7717-4e32-ab57-f25f38f55401",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "2967ca92-3977-44ad-8e19-318b9a71d227",
"type": "text",
"name": "value"
}
],
"inputs": [],
"color": "#A8E6CF",
"position": {
"x": -270,
"y": -16
},
"parameters": {
"name": "interview"
},
"description": null,
"type": "Text Input"
},
{
"key": "1a4d1254-2c44-49fa-8aee-3865d1be9f95",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "f36c7ae1-78da-4d71-8b44-58a1f87f17cb",
"type": "text",
"name": "requirements"
}
],
"inputs": [
{
"key": "7860945a-506b-45a3-97b6-cbf08023e421",
"type": "text",
"name": "interview"
}
],
"color": "#A8E6CF",
"position": {
"x": -11,
"y": -23
},
"parameters": {
"LLM": "llama3.1:8b"
},
"description": null,
"type": "Requirement Generator"
},
{
"key": "394f3a84-f8b0-4cf6-92c7-042e6be03a26",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "a73eb6d8-9010-428e-a0cd-0831fcba6c42",
"type": "text",
"name": "corrected-requirements"
}
],
"inputs": [
{
"key": "e83aebf3-05dd-4588-b9a3-03fd7253e5fc",
"type": "text",
"name": "requirement-smells"
}
],
"color": "#A8E6CF",
"position": {
"x": 321,
"y": -21
},
"parameters": {
"LLM": "gemma3:12b"
},
"description": null,
"type": "Requirements Corrector"
},
{
"key": "511a2b09-dde6-4877-97ed-92819da4b778",
"name": "newNode",
"createdBy": null,
"outputs": [
{
"key": "96750771-855b-4013-9959-ac8b3fd17133",
"type": "text",
"name": "requirement-smells"
}
],
"inputs": [
{
"key": "059dad32-a3ea-4a34-b77e-f747c2baf864",
"type": "text",
"name": "requirements"
}
],
"color": "#A8E6CF",
"position": {
"x": 707,
"y": -22
},
"parameters": {
"LLM": "codellama:latest"
},
"description": null,
"type": "Requirements Analyzer"
},
{
"key": "529bb583-05fe-4eb9-b194-1de65f577155",
"name": "newNode",
"createdBy": null,
"outputs": [],
"inputs": [
{
"key": "3677287f-e276-4869-913b-e985eedbfe08",
"type": "text",
"name": "value"
}
],
"color": "#A8E6CF",
"position": {
"x": 1075,
"y": 118
},
"parameters": null,
"description": null,
"type": "Text Output"
}
],
"connections": [
{
"key": "f8b4dbaa-a9cc-4470-89a2-7b999d905d9b",
"from": "2967ca92-3977-44ad-8e19-318b9a71d227",
"to": "7860945a-506b-45a3-97b6-cbf08023e421"
},
{
"key": "9b2da750-f8c2-425a-a4c0-67869a60f69b",
"from": "f36c7ae1-78da-4d71-8b44-58a1f87f17cb",
"to": "e83aebf3-05dd-4588-b9a3-03fd7253e5fc"
},
{
"key": "ec5ed95b-e546-4ee2-957b-039649f3fac4",
"from": "a73eb6d8-9010-428e-a0cd-0831fcba6c42",
"to": "059dad32-a3ea-4a34-b77e-f747c2baf864"
},
{
"key": "0aabf90f-d65d-4a21-8832-ad8ea6755547",
"from": "96750771-855b-4013-9959-ac8b3fd17133",
"to": "3677287f-e276-4869-913b-e985eedbfe08"
}
],
"public": true
}
]

View File

@ -1,263 +0,0 @@
[
{
"name": "Requirement Generator",
"category": "Generators",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": {},
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"interview": "Text"
},
"outputs": {
"requirements": "Text"
},
"executorToOutputTranslationMappings": {
"response": "requirements"
},
"inputToExecutorTranslationMappings": {},
"inputTranslators": {
"prompt": {
"translation": "Analyze the following interview transcript and extract a list of functional requirements. \n\n${{interview}}\n\nFormat the output as a CSV with the following columns: \n1. **ID**: Unique identifier (e.g., F1, F2, F3…). \n2. **Requirement Text**: Clear description of what the system must do (in plain language). \n3. **Customer Quote**: Exact phrase from the transcript that justifies the requirement. \n\n**Rules:** \n- Include **only functional requirements** (no qualities or non-functional requirements). \n- Keep requirements concise and actionable. \n- List requirements in logical order.\n- Make sure requirements are free of smells.\n- Output only the CSV, do not add any text.\n\n**CSV Format Example:**\n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”.\nF2: The system shall ask users to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”.\nF3: The system shall ask users to select the second operand of the operation; “I want to express the entire operation verbally, and get the results”.\n"
}
}
},
{
"name": "Requirements Analyzer",
"category": "Analyzers",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": {},
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"requirements": "Text"
},
"outputs": {
"requirement-smells": "Text"
},
"executorToOutputTranslationMappings": {
"response": "requirement-smells"
},
"inputToExecutorTranslationMappings": {},
"inputTranslators": {
"prompt": {
"translation": "giving the following requirements as csv:\n${{requirements}}\nAct as a **Requirements Analyst** tasked with reviewing a set of functional requirements derived from a customer interview. Analyze each requirement for potential **requirement smells** (e.g., ambiguity, incompleteness, inconsistency, or vagueness) and generate a CSV report with the following columns: \n\n1. **ID**: Unique identifier of the requirement (e.g., F1, F2). \n2. **Requirement**: The exact text of the requirement. \n3. **Customer Quote**: Supporting quote from the stakeholder. \n4. **Smell Description**: \n - If a smell is detected, briefly describe it (e.g., \"Ambiguous: 'user-friendly' is subjective\"). \n - If no smell is found, leave this field empty (null). \n\n**Rules:** \n- Read the requirement carefully, and think whether it can be tested\n- If it cannot be tested, understand what the smell is that makes it not testable. \n- Typical smells are: \nAmbiguity: Subjective terms (e.g., \"easy,\" \"fast\").\nIncompleteness: Missing steps/triggers (e.g., \"remind users\" but no timing specified).\nInconsistency: Conflicts with other requirements.\nVagueness: Unquantifiable metrics (e.g., \"often,\" \"sometimes\").\n- Output only the CSV, do not add any text.\n\n**Output CSV Format Example:** \n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”; null\nF2; The system shall ask users to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”; users is plural, can be ambiguous\n"
}
}
},
{
"name": "Requirements Corrector",
"category": "Correctors",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": { },
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [ ],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"requirement-smells": "Text"
},
"outputs": {
"corrected-requirements": "Text"
},
"executorToOutputTranslationMappings": {
"response": "corrected-requirements"
},
"inputToExecutorTranslationMappings": { },
"inputTranslators": {
"prompt": {
"translation": "giving the input:\n\n${{requirement-smells}}\n\nthat is a CSV of requirements with identified smells (columns: `ID`, `Requirement`, `Customer Quote`, `Smell Description`). \n\nAct as a **Requirements Corrector**. Your task is to **revise smelly requirements** (ambiguity, vagueness, inconsistency, incompleteness) into **clear, actionable, and testable** functional requirements. \n\n**Output Rules:** \n- Generate a **new CSV** with columns: \n 1. **ID**: Original requirement identifier (e.g., `F1`, `F2`). \n 2. **Corrected Requirement**: Rewritten version that resolves the smell. \n 3. **Customer Quote**: Original supporting quote from the transcript. \n- **Remove the \"Smell Description\" column** (corrections should speak for themselves). \n- **Preserve intent**: Ensure the Corrected requirement aligns with the stakeholder’s original need. \n- **For non-smelly requirements**: Copy them verbatim to the new CSV.\n- Output only the CSV, do not add any text.\n\n**Output CSV Format Example:** \n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”\nF2; The system shall ask the user to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”;\n"
}
}
},
{
"name": "Class Modeller (UML)",
"category": "Modellers",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": { },
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [ ],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"corrected-requirements": "Text",
"interview-script": "Text"
},
"outputs": {
"plant-uml": "Text"
},
"executorToOutputTranslationMappings": {
"response": "plant-uml"
},
"inputToExecutorTranslationMappings": { },
"inputTranslators": {
"prompt": {
"translation": "Act as a **Requirements Modeler** specializing in structural design. Create a PlantUML class diagram that captures the core domain model implied by the functional requirements and interview transcript.\n\n**Input:** \n1. Corrected requirements (CSV with `ID`, `Corrected Requirement`, `Customer Quote`) :\n\n${{corrected-requirements}}\n \n2. Interview transcript for context :\n\n${{interview-script}}\n\n**Output Rules:** \n1. Generate **one concise class diagram** as a textual output in PlantUML syntax \n2. Focus on: \n - Key entities (classes) and their attributes \n - Critical relationships (associations, compositions) \n - Basic operations/methods implied by requirements \n3. Output only the model, do not add any text.\n\n**Example Output (PlantUML):**\n\n@startuml Calculator_Class_Diagram\n\nclass Calculator {\n + currentInput: Number\n + storedValue: Number\n + selectedOperation: OperationType\n + calculate(): Number\n + clear(): void\n}\n\nclass InputHandler {\n + handleDigitPress(digit: Integer): void\n + handleOperator(op: OperationType): void\n + handleEquals(): void\n}\n\nclass Display {\n + updateScreen(value: String): void\n}\n\nenum OperationType <<enum>> {\n ADD\n SUBTRACT\n MULTIPLY\n DIVIDE\n}\n\nCalculator \"1\" *-- \"1\" OperationType\nCalculator --> Display : updates\nInputHandler --> Calculator : commands\n\nnote top of Calculator : \"Must support basic arithmetic\\noperations (F2)\"\nnote right of InputHandler : \"User quote: 'I need buttons for\\nnumbers and functions...'\"\n\n@enduml\n"
}
}
},
{
"name": "InputDispatcher",
"category": "Dispachers",
"executor": "NoOp",
"color": "black",
"createdBy": "admin",
"fixedParameters": { },
"runtimeParameters": { },
"inputs": {
"input": "Text"
},
"outputs": {
"output": "Text"
},
"executorToOutputTranslationMappings": {
"output": "output"
},
"inputToExecutorTranslationMappings": {
"input": "input"
},
"inputTranslators": { }
},
{
"name": "Sequence Modeler",
"category": "Modellers",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": { },
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [ ],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"interview": "Text",
"plantuml-class-diagram": "Text",
"corrected-requirements": "Text"
},
"outputs": {
"plantuml-sequence-diagram": "Text"
},
"executorToOutputTranslationMappings": {
"response": "plantuml-sequence-diagram"
},
"inputToExecutorTranslationMappings": { },
"inputTranslators": {
"prompt": {
"translation": "Act as a **Requirements Modeler** specializing in interaction design. Create a PlantUML sequence diagram that visually captures the key system interactions described in the functional requirements and interview transcript.\n\n**Input:**\n1. Cleaned requirements (CSV format with ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. Original interview transcript for context:\n\n${{interview}}\n\n3. Class diagram:\n\n\n\n**Output Rules:**\n1. Generate **one comprehensive sequence diagram** in PlantUML syntax\n2. Consider only the classes from the input class diagram\n3. Focus on showing:\n - Primary user-to-system interactions\n - Critical system responses\n - Important timing constraints (where specified)\n4. Include only the most essential interactions (5-10 steps max)\n5. Output only the model, do not add any text.\n\n**Example Output (PlantUML):**\n\n@startuml Calculator_Sequence_Diagram \n\nactor User \nparticipant InputHandler \nparticipant Calculator \nparticipant Display \n\nUser -> InputHandler : Press digit \"5\" \nnote right of User: \"I need buttons for numbers...\" (Requirement F1) \nInputHandler -> Calculator : Store input (currentInput = 5) \nCalculator -> Display : Update screen (\"5\") \n\nUser -> InputHandler : Press operator \"+\" \nnote right of User: \"Basic arithmetic is essential\" (F3) \nInputHandler -> Calculator : Set operation (ADD) \n\nUser -> InputHandler : Press digit \"3\" \nInputHandler -> Calculator : Store input (currentInput = 3) \nCalculator -> Display : Update screen (\"3\") \n\nUser -> InputHandler : Press \"=\" \nInputHandler -> Calculator : calculate() \nCalculator -> Calculator : Compute 5 + 3 \nCalculator -> Display : Update screen (\"8\") \n\n@enduml \n"
}
}
},
{
"name": "Code Generator",
"category": "Generators",
"executor": "GENERIC-AI",
"color": "black",
"createdBy": "admin",
"fixedParameters": { },
"runtimeParameters": {
"LLM": {
"name": "LLM",
"label": "Select the LLM to use",
"description": "The LLM to use for generating requirements",
"type": "Select",
"required": true,
"validations": [ ],
"specificAttributes": {
"options": [
"google-gemini",
"open-router"
]
}
}
},
"inputs": {
"corrected-requirements": "Text",
"class-diagram": "Text",
"sequence-diagram": "Text"
},
"outputs": {
"source-code": "Text"
},
"executorToOutputTranslationMappings": {
"response": "source-code"
},
"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${{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 <<enum>>)\"\"\"\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"
}
}
}
]

View File

@ -0,0 +1,6 @@
[
{
"username": "testuser",
"password": "testpassword"
}
]

View File

@ -4,35 +4,27 @@ import java.util.List;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.context.annotation.Profile;
import it.cnr.isti.workflow.manager.bricks.Brick;
import it.cnr.isti.workflow.manager.bricks.BrickManager;
import it.cnr.isti.workflow.manager.bricks.LLMBrick;
@TestConfiguration
@Profile("test")
public class MyTestConfiguration {
@Bean(name = "brickTestManager")
BrickManager<LLMBrick> brickManager() {
return new BrickManager<LLMBrick>() {
@Bean(name = "testProvider")
BrickManager<Brick> brickManager() {
return new BrickManager<Brick>() {
@Override
public List<String> possibleValues(String key) {
return null;
}
@Override
public List<LLMBrick> getBricks() {
return List.of(new LLMBrick("llmProvider", "model", "provider"));
public List<Brick> getBricks() {
Brick b = new Brick("id", "testProvider") {
};
return List.of(b);
}
};
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.build();
}
}

View File

@ -0,0 +1,20 @@
package it.cnr.isti.workflow.manager;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@TestConfiguration
@Profile("test")
public class TestSecurityConfig {
@Bean(name = "testSecurityFilterChain")
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(csrf -> csrf.disable())
.build();
}
}

View File

@ -16,6 +16,6 @@ public class TestConfig {
@Test
public void generateJSONSchema() throws Exception {
System.out.println(schemaProducer.generateSchema(LLMBrickConfiguration.class));
}
}

View File

@ -0,0 +1,23 @@
package it.cnr.isti.workflow.manager.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import it.cnr.isti.workflow.manager.auth.model.AuthRequest;
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
public class AuthControllerTest {
@Autowired
private AuthController authController;
@Test
public void testLogin() {
ResponseEntity<?> response = authController.login(new AuthRequest("testuser", "testpassword"));
assert response.getStatusCode().is2xxSuccessful();
}
}

View File

@ -8,16 +8,29 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import it.cnr.isti.workflow.manager.MyTestConfiguration;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
import it.cnr.isti.workflow.manager.bricks.BrickManager;
import it.cnr.isti.workflow.manager.bricks.LLMBrick;
@SpringBootTest
@Import(MyTestConfiguration.class)
@ActiveProfiles("test")
public class BlocksControllerTest {
@Qualifier("brickTestManager")
@Autowired
BrickManager<LLMBrick> llmBrickManager;
@Autowired
private BlocksController blocksController;
@ -36,6 +49,7 @@ public class BlocksControllerTest {
LLMBlockConfiguration config = LLMBlockConfiguration.builder()
.prompt("Hello, ${{name}}!")
.name("master")
.brick(llmBrickManager.getBricks().get(0))
.build();
Block<LLMBlockType, LLMBlockConfiguration> block = blocksController.create(config);
assertNotNull(block);

View File

@ -2,29 +2,37 @@ package it.cnr.isti.workflow.manager.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import it.cnr.isti.workflow.manager.MyTestConfiguration;
import it.cnr.isti.workflow.manager.bricks.Brick;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
@AutoConfigureMockMvc
import java.util.List;
@SpringBootTest()
@Import(MyTestConfiguration.class)
@ActiveProfiles("test")
public class BricksControllerTest {
@Autowired
private MockMvc mockMvc;
private BricksController bricksController;
@Test
public void testGetBricks() throws Exception {
mockMvc.perform(get("/bricks/providers/testProvider"))
.andExpect(status().isOk())
.andExpect(content().json("[{\"id\":\"provider:llmProvider_model\",\"provider\":\"llmProvider\",\"model\":\"model\"}]"));
String provider = bricksController.getProviders().stream().filter(p -> p.equals("testProvider")).findFirst().orElse(null);
assertNotNull(provider);
assertEquals("testProvider", provider);
List<? extends Brick> bricks = bricksController.getBricksByProvider(provider);
assertTrue(!bricks.isEmpty());
assertEquals(1, bricks.size());
Brick brick = bricks.get(0);
assertNotNull(brick);
assertEquals("testProvider:id", brick.getId());
}
}

View File

@ -8,5 +8,5 @@ spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.jakarta.persistence.validation.mode=none
app.db.init.enabled=true
app.import.path=src/main/resources/workflow-editor-init
app.import.enabled=false
app.import.path=src/test/resources/workflow-editor-init
app.import.enabled=true

View File

@ -0,0 +1,6 @@
[
{
"username": "testuser",
"password": "testpassword"
}
]