diff --git a/.gitignore b/.gitignore index 2a1cf43c..61330eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ gamslice.txt # Listing files compiled from our GAMS scripts *.lst *.op2 +gams/**/225a +*.out diff --git a/build.gradle b/build.gradle index 64d3e7ad..3fbf5545 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,9 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.4' implementation 'com.bpodgursky:jbool_expressions:1.24' implementation files('lib/de.ovgu.featureide.lib.fm-v3.9.1.jar', 'lib/uvl-parser.jar') testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/edu/kit/provideq/toolbox/MetaSolverProvider.java b/src/main/java/edu/kit/provideq/toolbox/MetaSolverProvider.java new file mode 100644 index 00000000..05b8cd96 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/MetaSolverProvider.java @@ -0,0 +1,53 @@ +package edu.kit.provideq.toolbox; + +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * The meta solver provide can be used to find all or specific {@link MetaSolver}s. + */ +@Component +public class MetaSolverProvider { + private final Map> metaSolvers; + + /** + * Initializes a meta solver provider bean. + * This provider will fetch available meta solvers once through the given {@code context}. + * + * @param context used to find available meta solvers. + */ + @Autowired + public MetaSolverProvider(ApplicationContext context) { + this.metaSolvers = + context.getBeansOfType(MetaSolver.class) + .values() + .stream() + .collect(Collectors.toMap( + MetaSolver::getProblemType, + metaSolver -> (MetaSolver) metaSolver)); + } + + /** + * Finds a meta solver for the given problem type. + * + * @param problemType the type of problem to find the meta solver of. + * @return the meta solver that manages solvers of the given type. + */ + public MetaSolver getMetaSolver(ProblemType problemType) { + return metaSolvers.get(problemType); + } + + /** + * Returns all registered meta solvers for all available problem types. + */ + public Collection> getMetaSolvers() { + return Collections.unmodifiableCollection(metaSolvers.values()); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/ProblemController.java b/src/main/java/edu/kit/provideq/toolbox/ProblemController.java deleted file mode 100644 index d172af0c..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/ProblemController.java +++ /dev/null @@ -1,103 +0,0 @@ -package edu.kit.provideq.toolbox; - -import edu.kit.provideq.toolbox.meta.MetaSolver; -import edu.kit.provideq.toolbox.meta.Problem; -import edu.kit.provideq.toolbox.meta.ProblemSolver; -import edu.kit.provideq.toolbox.meta.ProblemType; -import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -/** - * Abstract controller, offers generic post and get methods. - * - * @param the type in which problem input is expected to arrive - * @param the type in which a solution will be formatted - * @param the type of solver that is to be used to solve a problem - */ -@Component -@RestController -public abstract class ProblemController> { - private ApplicationContext context; - private final SolutionManager solutionManager = new SolutionManager<>(); - - public abstract ProblemType getProblemType(); - - public abstract MetaSolver getMetaSolver(); - - @Autowired - public void setApplicationContext(ApplicationContext context) { - this.context = context; - } - - public Solution solve(SolveRequest request) { - Solution solution = solutionManager.createSolution(); - Problem problem = new Problem<>(request.requestContent, getProblemType()); - - SolverT solver = getMetaSolver() - .getSolver(request.requestedSolverId) - .orElseGet(() -> getMetaSolver().findSolver(problem, request.requestedMetaSolverSettings)); - - solution.setSolverName(solver.getName()); - - SubRoutinePool subRoutinePool = - request.requestedSubSolveRequests == null - ? context.getBean(SubRoutinePool.class) - : context.getBean(SubRoutinePool.class, request.requestedSubSolveRequests); - - long start = System.currentTimeMillis(); - solver.solve(problem, solution, subRoutinePool); - long finish = System.currentTimeMillis(); - - solution.setExecutionMilliseconds(finish - start); - - return solution; - } - - public Solution findSolution(long id) { - var solution = solutionManager.getSolution(id); - if (solution == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - String.format("Unable to find solution process with id %d", id)); - } - - return solution; - } - - public SolverT getSolver(String id) { - Optional solver = getMetaSolver() - .getAllSolvers() - .stream() - .filter(s -> id.equals(s.getId())) - .findFirst(); - - if (solver.isEmpty()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - String.format("Unable to find solver %s", id)); - } - - return solver.get(); - } - - public Set getSolvers() { - return getMetaSolver() - .getAllSolvers() - .stream() - .map(s -> new ProblemSolverInfo(s.getId(), s.getName())) - .collect(Collectors.toSet()); - } - - public List getSubRoutines(String id) { - SolverT solver = getSolver(id); - return solver.getSubRoutines(); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/ProblemControllerProvider.java b/src/main/java/edu/kit/provideq/toolbox/ProblemControllerProvider.java deleted file mode 100644 index c1398920..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/ProblemControllerProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package edu.kit.provideq.toolbox; - -import edu.kit.provideq.toolbox.meta.ProblemType; -import java.util.Map; -import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -@Component -public class ProblemControllerProvider { - private final Map> problemControllers; - - @Autowired - public ProblemControllerProvider( - ApplicationContext context) { - problemControllers = - context.getBeansOfType(ProblemController.class) - .values() - .stream() - .collect(Collectors.toMap( - ProblemController::getProblemType, - problemController -> (ProblemController) problemController)); - } - - public ProblemController getProblemController(ProblemType problemType) { - return problemControllers.get(problemType); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/Solution.java b/src/main/java/edu/kit/provideq/toolbox/Solution.java index c54b9fe7..02b11234 100644 --- a/src/main/java/edu/kit/provideq/toolbox/Solution.java +++ b/src/main/java/edu/kit/provideq/toolbox/Solution.java @@ -46,7 +46,7 @@ public void setStatus(SolutionStatus newStatus) { } @Override - public SolutionHandle toStringSolution() { + public Solution toStringSolution() { return toStringSolution(Object::toString); } @@ -58,7 +58,7 @@ public SolutionHandle toStringSolution() { * to a String. * @return the solution with the stringified solution data. */ - public SolutionHandle toStringSolution(@NotNull Function stringSelector) { + public Solution toStringSolution(@NotNull Function stringSelector) { Objects.requireNonNull(stringSelector, "Missing String selector!"); var stringSolution = new Solution(getId()); diff --git a/src/main/java/edu/kit/provideq/toolbox/SolutionHandle.java b/src/main/java/edu/kit/provideq/toolbox/SolutionHandle.java index daacb9a0..ece3663e 100644 --- a/src/main/java/edu/kit/provideq/toolbox/SolutionHandle.java +++ b/src/main/java/edu/kit/provideq/toolbox/SolutionHandle.java @@ -10,5 +10,8 @@ public interface SolutionHandle { void setStatus(SolutionStatus newStatus); - SolutionHandle toStringSolution(); + /** + * Converts this solution handle to a string-based {@link Solution}. + */ + Solution toStringSolution(); } diff --git a/src/main/java/edu/kit/provideq/toolbox/SubRoutinePool.java b/src/main/java/edu/kit/provideq/toolbox/SubRoutinePool.java index 061ab955..4bd13e9c 100644 --- a/src/main/java/edu/kit/provideq/toolbox/SubRoutinePool.java +++ b/src/main/java/edu/kit/provideq/toolbox/SubRoutinePool.java @@ -1,5 +1,6 @@ package edu.kit.provideq.toolbox; +import edu.kit.provideq.toolbox.meta.MetaSolver; import edu.kit.provideq.toolbox.meta.ProblemSolver; import edu.kit.provideq.toolbox.meta.ProblemType; import java.util.Collections; @@ -15,7 +16,7 @@ public class SubRoutinePool { private final Map> subRoutineCalls; - private ProblemControllerProvider problemControllerProvider; + private MetaSolverProvider metaSolverProvider; public SubRoutinePool() { subRoutineCalls = Collections.emptyMap(); @@ -31,8 +32,8 @@ public SubRoutinePool(Map> requestedSubRoutines) { } @Autowired - public void setProblemControllerProvider(ProblemControllerProvider problemControllerProvider) { - this.problemControllerProvider = problemControllerProvider; + public void setProblemControllerProvider(MetaSolverProvider metaSolverProvider) { + this.metaSolverProvider = metaSolverProvider; } /** @@ -53,16 +54,10 @@ public Function> getSubRouti var newSolveRequest = subRoutine.replaceContent(content); - ProblemController< - ProblemT, - SolutionT, - ? extends ProblemSolver> problemController - = (ProblemController< - ProblemT, - SolutionT, - ? extends ProblemSolver>) - problemControllerProvider.getProblemController(problemType); - return problemController.solve(newSolveRequest); + MetaSolver> metaSolver = + (MetaSolver>) + metaSolverProvider.getMetaSolver(problemType); + return metaSolver.solve(newSolveRequest); }; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/api/CorsConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/api/CorsConfiguration.java new file mode 100644 index 00000000..f788c55f --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/CorsConfiguration.java @@ -0,0 +1,19 @@ +package edu.kit.provideq.toolbox.api; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +/** + * Spring configuration to enable CORS between the API and the web frontend. + */ +@Configuration +@EnableWebFlux +public class CorsConfiguration implements WebFluxConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*"); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java new file mode 100644 index 00000000..db4304a7 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java @@ -0,0 +1,77 @@ +package edu.kit.provideq.toolbox.api; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * This router handles requests to the GET {@code /meta-solver/{problemType}/settings} endpoints. + * Responses are generated from the settings reported by the available meta-solvers + * (see {@link MetaSolver#getSettings()}). + */ +@Configuration +@EnableWebFlux +public class MetaSolverSettingsRouter { + private final MetaSolverProvider metaSolverProvider; + + @Autowired + public MetaSolverSettingsRouter(MetaSolverProvider metaSolverProvider) { + this.metaSolverProvider = metaSolverProvider; + } + + @Bean + RouterFunction getMetaSolverSettingsRoutes() { + return metaSolverProvider.getMetaSolvers().stream() + .map(this::defineMetaSolverSettingsRouteForMetaSolver) + .reduce(RouterFunction::and) + .orElseThrow(); + } + + private RouterFunction defineMetaSolverSettingsRouteForMetaSolver( + MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + return route().GET( + getRouteForProblemType(problemType), + req -> handleMetaSolverSettingsRouteForMetaSolver(metaSolver), + ops -> ops + .operationId(getRouteForProblemType(problemType)) + .tag(problemType.getId()) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(MetaSolverSetting.class))) + ) + ) + ).build(); + } + + private Mono handleMetaSolverSettingsRouteForMetaSolver( + MetaSolver metaSolver) { + return ok().body(Mono.just(metaSolver.getSettings()), new ParameterizedTypeReference<>() { + }); + } + + private String getRouteForProblemType(ProblemType type) { + return "/meta-solver/settings/" + type.getId(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java new file mode 100644 index 00000000..8b86875e --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java @@ -0,0 +1,152 @@ +package edu.kit.provideq.toolbox.api; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.SolutionHandle; +import edu.kit.provideq.toolbox.SolveRequest; +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; + +/** + * This router handles problem-solving requests to the GET and POST {@code /solve/{problemType}} + * endpoints. + * Requests are validated and relayed to the corresponding {@link MetaSolver}. + */ +@Configuration +@EnableWebFlux +public class SolveRouter { + private final MetaSolverProvider metaSolverProvider; + private final Validator validator; + + public SolveRouter(MetaSolverProvider metaSolverProvider, Validator validator) { + this.metaSolverProvider = metaSolverProvider; + this.validator = validator; + } + + @Bean + RouterFunction getSolveRoutes() { + return metaSolverProvider.getMetaSolvers().stream() + .map(this::defineRouteForMetaSolver) + .reduce(RouterFunction::and) + .orElseThrow(); // we should always have at least one route or the toolbox is useless + } + + private RouterFunction defineRouteForMetaSolver(MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + return route().POST( + getSolveRouteForProblemType(problemType), + accept(APPLICATION_JSON), + req -> handleRouteForMetaSolver(metaSolver, req), + ops -> ops + .operationId(getSolveRouteForProblemType(problemType)) + .tag(problemType.getId()) + .requestBody(requestBodyBuilder() + .content(contentBuilder() + .schema(schemaBuilder().implementation( + metaSolver.getProblemType().getRequestType())) + .mediaType(APPLICATION_JSON_VALUE) + ) + .required(true) + ) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .implementation(SolutionHandle.class) + ) + ).build(); + } + + private Mono handleRouteForMetaSolver( + MetaSolver metaSolver, ServerRequest req) { + var solutionMono = req + .bodyToMono(new ParameterizedTypeReference>() { + }) + .doOnNext(this::validate) + .map(metaSolver::solve) + .map(Solution::toStringSolution); + return ok().body(solutionMono, new ParameterizedTypeReference<>() { + }); + } + + private void validate(SolveRequest request) { + Errors errors = new BeanPropertyBindingResult(request, "request"); + validator.validate(request, errors); + if (errors.hasErrors()) { + throw new ServerWebInputException(errors.toString()); + } + } + + @Bean + RouterFunction getSolutionRoutes() { + return metaSolverProvider.getMetaSolvers().stream() + .map(this::defineSolutionRouteForMetaSolver) + .reduce(RouterFunction::and) + .orElseThrow(); // we should always have at least one route or the toolbox is useless + } + + private RouterFunction defineSolutionRouteForMetaSolver( + MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + return route().GET( + // FIXME this is intentionally SOLVE instead of SOLUTION to avoid breaking things + // but maybe we should switch the name at some point + getSolveRouteForProblemType(problemType), + accept(APPLICATION_JSON), + req -> handleSolutionRouteForMetaSolver(metaSolver, req), + ops -> ops + .operationId(getSolutionRouteForProblemType(problemType)) + .tag(problemType.getId()) + .parameter(parameterBuilder().in(ParameterIn.QUERY).name("id")) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .implementation(SolutionHandle.class) + ) + ).build(); + } + + private Mono handleSolutionRouteForMetaSolver(MetaSolver metaSolver, + ServerRequest req) { + var solution = req.queryParam("id") + .map(Long::parseLong) + .map(solutionId -> metaSolver.getSolutionManager().getSolution(solutionId)) + .map(Solution::toStringSolution) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Could not find a solution for this problem with this solution id!")); + + return ok().body(Mono.just(solution), new ParameterizedTypeReference<>() { + }); + } + + private String getSolveRouteForProblemType(ProblemType type) { + return "/solve/" + type.getId(); + } + + private String getSolutionRouteForProblemType(ProblemType type) { + return "/solution/" + type.getId(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/SolversRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/SolversRouter.java new file mode 100644 index 00000000..a9bc93a4 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/SolversRouter.java @@ -0,0 +1,81 @@ +package edu.kit.provideq.toolbox.api; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.ProblemSolverInfo; +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * This router handles solver discovery requests to the GET {@code /solvers/{problemType}} + * endpoints. + * Responses are generated from the solvers reported by the meta-solver registered for the given + * problem type. + */ +@Configuration +@EnableWebFlux +public class SolversRouter { + private final MetaSolverProvider metaSolverProvider; + + @Autowired + public SolversRouter(MetaSolverProvider metaSolverProvider) { + this.metaSolverProvider = metaSolverProvider; + } + + @Bean + RouterFunction getSolversRoutes() { + return metaSolverProvider.getMetaSolvers().stream() + .map(this::defineSolversRouteForMetaSolver) + .reduce(RouterFunction::and) + .orElseThrow(); + } + + private RouterFunction defineSolversRouteForMetaSolver( + MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + return route().GET( + getSolversRouteForProblemType(problemType), + req -> handleSolversRouteForMetaSolver(metaSolver), + ops -> ops + .operationId(getSolversRouteForProblemType(problemType)) + .tag(problemType.getId()) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(ProblemSolverInfo.class))) + ) + ) + ).build(); + } + + private Mono handleSolversRouteForMetaSolver(MetaSolver metaSolver) { + var solvers = metaSolver.getAllSolvers().stream() + .map(solver -> new ProblemSolverInfo(solver.getId(), solver.getName())) + .toList(); + + return ok().body(Mono.just(solvers), new ParameterizedTypeReference<>() { + }); + } + + private String getSolversRouteForProblemType(ProblemType type) { + return "/solvers/" + type.getId(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineRouter.java new file mode 100644 index 00000000..8e56cc4b --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineRouter.java @@ -0,0 +1,90 @@ +package edu.kit.provideq.toolbox.api; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder; +import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; + +/** + * This router handles sub-routine discovery requests to the GET {@code /sub-routines/{problemType}} + * endpoints. + * Responses are generated from the sub-routine data reported by the given solver itself + * (see {@link ProblemSolver#getSubRoutines()}). + */ +@Configuration +@EnableWebFlux +public class SubRoutineRouter { + private final MetaSolverProvider metaSolverProvider; + + @Autowired + public SubRoutineRouter(MetaSolverProvider metaSolverProvider) { + this.metaSolverProvider = metaSolverProvider; + } + + @Bean + RouterFunction getSubRoutineRoutes() { + return metaSolverProvider.getMetaSolvers().stream() + .map(this::defineSubRoutineRouteForMetaSolver) + .reduce(RouterFunction::and) + .orElseThrow(); + } + + private RouterFunction defineSubRoutineRouteForMetaSolver( + MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + return route().GET( + getSubRoutinesRouteForProblemType(problemType), + req -> handleSubRoutineRouteForMetaSolver(metaSolver, req), + ops -> ops + .operationId(getSubRoutinesRouteForProblemType(problemType)) + .parameter(parameterBuilder().in(ParameterIn.QUERY).name("id")) + .tag(problemType.getId()) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(SubRoutineDefinition.class))) + ) + ) + ).build(); + } + + private Mono handleSubRoutineRouteForMetaSolver(MetaSolver metaSolver, + ServerRequest req) { + var subroutines = req.queryParam("id") + .flatMap(metaSolver::getSolver) + .map(ProblemSolver::getSubRoutines) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Could not find a solver for this problem with this solver id!")); + + return ok().body(Mono.just(subroutines), new ParameterizedTypeReference<>() { + }); + } + + private String getSubRoutinesRouteForProblemType(ProblemType type) { + return "/sub-routines/" + type.getId(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomaly.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomaly.java deleted file mode 100644 index 5af274c7..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomaly.java +++ /dev/null @@ -1,27 +0,0 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly; - -public enum FeatureModelAnomaly { - /** - * Occurs when no configuration of the feature model is ever valid. - */ - VOID("Void Feature Model"), - /** - * Occurs when a feature is never true in any configuration. - */ - DEAD("Dead Features"), - /** - * Occurs when a feature is flagged as optional, but in reality is mandatory and exists in all - * configurations. - */ - FALSE_OPTIONAL("False-optional Features"), - /** - * Constraints that don't have an impact on the valid configurations. - */ - REDUNDANT_CONSTRAINTS("Redundant Constraints"); - - public final String name; - - FeatureModelAnomaly(String name) { - this.name = name; - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyController.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyController.java deleted file mode 100644 index 5a2b5cb7..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyController.java +++ /dev/null @@ -1,106 +0,0 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly; - -import edu.kit.provideq.toolbox.ProblemController; -import edu.kit.provideq.toolbox.ProblemSolverInfo; -import edu.kit.provideq.toolbox.Solution; -import edu.kit.provideq.toolbox.SolutionHandle; -import edu.kit.provideq.toolbox.featuremodel.SolveFeatureModelRequest; -import edu.kit.provideq.toolbox.featuremodel.anomaly.solvers.FeatureModelAnomalySolver; -import edu.kit.provideq.toolbox.meta.MetaSolver; -import edu.kit.provideq.toolbox.meta.ProblemType; -import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; -import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; -import jakarta.validation.Valid; -import java.util.List; -import java.util.Set; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class FeatureModelAnomalyController - extends ProblemController { - - private final MetaSolver< - FeatureModelAnomalyProblem, - String, - FeatureModelAnomalySolver> metaSolver; - - public FeatureModelAnomalyController(MetaSolver< - FeatureModelAnomalyProblem, - String, - FeatureModelAnomalySolver> metaSolver) { - this.metaSolver = metaSolver; - } - - @Override - public ProblemType getProblemType() { - return ProblemType.FEATURE_MODEL_ANOMALY; - } - - @Override - public MetaSolver getMetaSolver() { - return metaSolver; - } - - @CrossOrigin - @PostMapping("/solve/feature-model/anomaly/void") - public SolutionHandle findVoidFeatureModel(@RequestBody @Valid SolveFeatureModelRequest request) { - return solveAnomaly(request, FeatureModelAnomaly.VOID); - } - - @CrossOrigin - @PostMapping("/solve/feature-model/anomaly/dead") - public SolutionHandle findDeadFeatures(@RequestBody @Valid SolveFeatureModelRequest request) { - return solveAnomaly(request, FeatureModelAnomaly.DEAD); - } - - @CrossOrigin - @PostMapping("/solve/feature-model/anomaly/false-optional") - public SolutionHandle findFalseOptionalFeatures( - @RequestBody @Valid SolveFeatureModelRequest request) { - return solveAnomaly(request, FeatureModelAnomaly.FALSE_OPTIONAL); - } - - @CrossOrigin - @PostMapping("/solve/feature-model/anomaly/redundant-constraints") - public SolutionHandle findRedundantConstraints( - @RequestBody @Valid SolveFeatureModelRequest request) { - return solveAnomaly(request, FeatureModelAnomaly.REDUNDANT_CONSTRAINTS); - } - - private SolutionHandle solveAnomaly(SolveFeatureModelRequest request, - FeatureModelAnomaly anomaly) { - var solution = (Solution) super.solve( - request.replaceContent(new FeatureModelAnomalyProblem(request.requestContent, anomaly))); - solution.setSolverName(solution.getSolverName() + ": " + anomaly.name); - return solution; - } - - @CrossOrigin - @GetMapping("/solve/feature-model/anomaly/") - public SolutionHandle getSolution(@RequestParam(name = "id") long id) { - return super.findSolution(id); - } - - @CrossOrigin - @GetMapping("/sub-routines/feature-model/anomaly") - public List getSubRoutines(@RequestParam(name = "id") String solverId) { - return super.getSubRoutines(solverId); - } - - @CrossOrigin - @GetMapping("/meta-solver/settings/feature-model/anomaly") - public List getMetaSolverSettings() { - return metaSolver.getSettings(); - } - - @CrossOrigin - @GetMapping("/solvers/feature-model/anomaly") - public Set getSolvers() { - return super.getSolvers(); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyProblem.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyProblem.java deleted file mode 100644 index 6ab374ee..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/FeatureModelAnomalyProblem.java +++ /dev/null @@ -1,4 +0,0 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly; - -public record FeatureModelAnomalyProblem(String featureModel, FeatureModelAnomaly anomaly) { -} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/MetaSolverFeatureModelAnomaly.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/MetaSolverFeatureModelAnomaly.java deleted file mode 100644 index 58e35cab..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/MetaSolverFeatureModelAnomaly.java +++ /dev/null @@ -1,34 +0,0 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly; - -import edu.kit.provideq.toolbox.featuremodel.anomaly.solvers.FeatureModelAnomalySolver; -import edu.kit.provideq.toolbox.meta.MetaSolver; -import edu.kit.provideq.toolbox.meta.Problem; -import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * Simple {@link MetaSolver} for FeatureModel problems. - */ -@Component -public class MetaSolverFeatureModelAnomaly extends MetaSolver< - FeatureModelAnomalyProblem, - String, - FeatureModelAnomalySolver> { - - @Autowired - public MetaSolverFeatureModelAnomaly(FeatureModelAnomalySolver anomalySolver) { - super(anomalySolver); - } - - @Override - public FeatureModelAnomalySolver findSolver( - Problem problem, - List metaSolverSettings) { - // todo add decision - return (new ArrayList<>(this.solvers)).get((new Random()).nextInt(this.solvers.size())); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureMetaSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureMetaSolver.java new file mode 100644 index 00000000..e100718a --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureMetaSolver.java @@ -0,0 +1,27 @@ +package edu.kit.provideq.toolbox.featuremodel.anomaly.dead; + +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; +import java.util.List; +import org.springframework.stereotype.Component; + +/** + * This is the meta solver for the {@link ProblemType#FEATURE_MODEL_ANOMALY_DEAD} problem. + */ +@Component +public class DeadFeatureMetaSolver + extends MetaSolver> { + public DeadFeatureMetaSolver(SatBasedDeadFeatureSolver solver) { + super(ProblemType.FEATURE_MODEL_ANOMALY_DEAD, solver); + } + + @Override + public ProblemSolver findSolver(Problem problem, + List metaSolverSettings) { + // we only have one solver at this point + return getAllSolvers().stream().findAny().orElseThrow(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelAnomalySolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java similarity index 59% rename from src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelAnomalySolver.java rename to src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java index d06c47b3..b9c93358 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelAnomalySolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/SatBasedDeadFeatureSolver.java @@ -1,15 +1,15 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly.solvers; +package edu.kit.provideq.toolbox.featuremodel.anomaly.dead; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.SolutionStatus; import edu.kit.provideq.toolbox.SubRoutinePool; import edu.kit.provideq.toolbox.convert.UvlToDimacsCnf; import edu.kit.provideq.toolbox.exception.ConversionException; -import edu.kit.provideq.toolbox.featuremodel.anomaly.FeatureModelAnomalyProblem; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnf; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.format.cnf.dimacs.Variable; import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemSolver; import edu.kit.provideq.toolbox.meta.ProblemType; import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; import java.util.ArrayList; @@ -17,51 +17,43 @@ import java.util.function.Function; import org.springframework.stereotype.Component; +/** + * This problem solver solves the {@link ProblemType#FEATURE_MODEL_ANOMALY_DEAD} problem by building + * {@link ProblemType#SAT} formulae that are solved by a corresponding solver. + */ @Component -public class FeatureModelAnomalySolver extends FeatureModelSolver { +public class SatBasedDeadFeatureSolver implements ProblemSolver { @Override public String getName() { - return "Feature Model Anomaly"; + return "SAT-based Dead Feature Solver"; } @Override public List getSubRoutines() { return List.of( - new SubRoutineDefinition(ProblemType.SAT, "sat", + new SubRoutineDefinition(ProblemType.SAT, "Used to find valid configurations in the Feature Model")); } @Override - public boolean canSolve(Problem problem) { - //TODO: assess problemData - return problem.type() == ProblemType.FEATURE_MODEL_ANOMALY; + public boolean canSolve(Problem problem) { + return problem.type() == ProblemType.FEATURE_MODEL_ANOMALY_DEAD; } @Override - public void solve(Problem problem, Solution solution, + public void solve(Problem problem, Solution solution, SubRoutinePool subRoutinePool) { // Convert uvl to cnf String cnf; try { - cnf = UvlToDimacsCnf.convert(problem.problemData().featureModel()); + cnf = UvlToDimacsCnf.convert(problem.problemData()); } catch (ConversionException e) { solution.setDebugData("Conversion error: " + e.getMessage()); return; } var satSolve = subRoutinePool.getSubRoutine(ProblemType.SAT); - switch (problem.problemData().anomaly()) { - case VOID -> checkVoidFeatureModel(solution, cnf, satSolve); - case DEAD -> checkDeadFeatures(solution, cnf, satSolve); - case FALSE_OPTIONAL, REDUNDANT_CONSTRAINTS -> { - solution.setDebugData("Not implemented yet!"); - solution.abort(); - } - default -> { - solution.setDebugData("Unknown anomaly type " + problem.problemData().anomaly() + "!"); - solution.abort(); - } - } + checkDeadFeatures(solution, cnf, satSolve); } private static void checkDeadFeatures(Solution solution, String cnf, @@ -118,27 +110,4 @@ private static void checkDeadFeatures(Solution solution, String cnf, solution.setSolutionData(builder.toString()); solution.complete(); } - - private static void checkVoidFeatureModel(Solution solution, - String cnf, - Function> satSolve) { - // Check if the feature model is not a void feature model - var voidSolution = satSolve.apply(cnf); - - solution.setDebugData("Dimacs CNF of Feature Model:\n" + cnf); - if (voidSolution.getStatus() == SolutionStatus.SOLVED) { - // If there is a valid configuration, the feature model is not a void feature model - var dimacsCnfSolution = voidSolution.getSolutionData(); - - solution.setSolutionData(voidSolution.getSolutionData().isVoid() - ? "The feature model is a void feature model. The configuration is never valid." - : "The feature model has valid configurations, for example: \n" - + dimacsCnfSolution.toHumanReadableString()); - solution.complete(); - } else { - solution.setDebugData(voidSolution.getDebugData()); - solution.abort(); - } - } } diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelSolver.java deleted file mode 100644 index 08ac67d6..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/solvers/FeatureModelSolver.java +++ /dev/null @@ -1,8 +0,0 @@ -package edu.kit.provideq.toolbox.featuremodel.anomaly.solvers; - -import edu.kit.provideq.toolbox.featuremodel.anomaly.FeatureModelAnomalyProblem; -import edu.kit.provideq.toolbox.meta.ProblemSolver; - -public abstract class FeatureModelSolver - implements ProblemSolver { -} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java new file mode 100644 index 00000000..0b039bec --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/SatBasedVoidFeatureSolver.java @@ -0,0 +1,79 @@ +package edu.kit.provideq.toolbox.featuremodel.anomaly.voidmodel; + +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.SolutionStatus; +import edu.kit.provideq.toolbox.SubRoutinePool; +import edu.kit.provideq.toolbox.convert.UvlToDimacsCnf; +import edu.kit.provideq.toolbox.exception.ConversionException; +import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; +import java.util.List; +import java.util.function.Function; +import org.springframework.stereotype.Component; + +/** + * This problem solver solves the {@link ProblemType#FEATURE_MODEL_ANOMALY_VOID} problem by building + * {@link ProblemType#SAT} formula that is solved by a corresponding solver. + */ +@Component +public class SatBasedVoidFeatureSolver + implements ProblemSolver { + @Override + public String getName() { + return "SAT-based Void Feature Model Solver"; + } + + @Override + public List getSubRoutines() { + return List.of( + new SubRoutineDefinition(ProblemType.SAT, + "Used to find valid configurations in the Feature Model")); + } + + @Override + public boolean canSolve(Problem problem) { + return problem.type() == ProblemType.FEATURE_MODEL_ANOMALY_VOID; + } + + @Override + public void solve(Problem problem, Solution solution, + SubRoutinePool subRoutinePool) { + // Convert uvl to cnf + String cnf; + try { + cnf = UvlToDimacsCnf.convert(problem.problemData()); + } catch (ConversionException e) { + solution.setDebugData("Conversion error: " + e.getMessage()); + return; + } + + var satSolve = subRoutinePool.getSubRoutine(ProblemType.SAT); + checkVoidFeatureModel(solution, cnf, satSolve); + } + + private static void checkVoidFeatureModel(Solution solution, + String cnf, + Function> satSolve) { + // Check if the feature model is not a void feature model + var voidSolution = satSolve.apply(cnf); + + solution.setDebugData("Dimacs CNF of Feature Model:\n" + cnf); + if (voidSolution.getStatus() == SolutionStatus.SOLVED) { + // If there is a valid configuration, the feature model is not a void feature model + var dimacsCnfSolution = voidSolution.getSolutionData(); + + solution.setSolutionData(voidSolution.getSolutionData().isVoid() + ? "The feature model is a void feature model. The configuration is never valid." + : "The feature model has valid configurations, for example: \n" + + dimacsCnfSolution.toHumanReadableString()); + solution.complete(); + } else { + solution.setDebugData(voidSolution.getDebugData()); + solution.abort(); + } + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidFeatureMetaSolver.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidFeatureMetaSolver.java new file mode 100644 index 00000000..bdac824c --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidFeatureMetaSolver.java @@ -0,0 +1,27 @@ +package edu.kit.provideq.toolbox.featuremodel.anomaly.voidmodel; + +import edu.kit.provideq.toolbox.meta.MetaSolver; +import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; +import java.util.List; +import org.springframework.stereotype.Component; + +/** + * This is the meta solver for the {@link ProblemType#FEATURE_MODEL_ANOMALY_VOID} problem. + */ +@Component +public class VoidFeatureMetaSolver + extends MetaSolver> { + public VoidFeatureMetaSolver(SatBasedVoidFeatureSolver solver) { + super(ProblemType.FEATURE_MODEL_ANOMALY_VOID, solver); + } + + @Override + public ProblemSolver findSolver(Problem problem, + List metaSolverSettings) { + // we only have one solver at this point + return getAllSolvers().stream().findAny().orElseThrow(); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutController.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutController.java deleted file mode 100644 index af1a987b..00000000 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutController.java +++ /dev/null @@ -1,69 +0,0 @@ -package edu.kit.provideq.toolbox.maxcut; - -import edu.kit.provideq.toolbox.ProblemController; -import edu.kit.provideq.toolbox.ProblemSolverInfo; -import edu.kit.provideq.toolbox.SolutionHandle; -import edu.kit.provideq.toolbox.maxcut.solvers.MaxCutSolver; -import edu.kit.provideq.toolbox.meta.MetaSolver; -import edu.kit.provideq.toolbox.meta.ProblemType; -import edu.kit.provideq.toolbox.meta.SubRoutineDefinition; -import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; -import jakarta.validation.Valid; -import java.util.List; -import java.util.Set; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class MaxCutController extends ProblemController { - - private final MetaSolver metaSolver; - - public MaxCutController(MetaSolver metaSolver) { - this.metaSolver = metaSolver; - } - - @Override - public ProblemType getProblemType() { - return ProblemType.MAX_CUT; - } - - @Override - public MetaSolver getMetaSolver() { - return metaSolver; - } - - @CrossOrigin - @PostMapping("/solve/max-cut") - public SolutionHandle solveMaxCut(@RequestBody @Valid SolveMaxCutRequest request) { - return super.solve(request); - } - - @CrossOrigin - @GetMapping("/solve/max-cut") - public SolutionHandle getSolution(@RequestParam(name = "id") long id) { - return super.findSolution(id); - } - - @CrossOrigin - @GetMapping("/sub-routines/max-cut") - public List getSubRoutines(@RequestParam(name = "id") String solverId) { - return super.getSubRoutines(solverId); - } - - @CrossOrigin - @GetMapping("/meta-solver/settings/max-cut") - public List getMetaSolverSettings() { - return metaSolver.getSettings(); - } - - @CrossOrigin - @GetMapping("/solvers/max-cut") - public Set getSolvers() { - return super.getSolvers(); - } -} diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java index 309b8f03..d2c0a97d 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java @@ -5,6 +5,7 @@ import edu.kit.provideq.toolbox.maxcut.solvers.QiskitMaxCutSolver; import edu.kit.provideq.toolbox.meta.MetaSolver; import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemType; import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; import java.util.ArrayList; import java.util.List; @@ -21,7 +22,7 @@ public class MetaSolverMaxCut extends MetaSolver { @Autowired public MetaSolverMaxCut(QiskitMaxCutSolver qiskitMaxCutSolver, GamsMaxCutSolver gamsMaxCutSolver) { - super(qiskitMaxCutSolver, gamsMaxCutSolver); + super(ProblemType.MAX_CUT, qiskitMaxCutSolver, gamsMaxCutSolver); } @Override diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java b/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java index 807e66b9..ab6be8b9 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java @@ -1,10 +1,16 @@ package edu.kit.provideq.toolbox.meta; +import edu.kit.provideq.toolbox.Solution; +import edu.kit.provideq.toolbox.SolutionManager; +import edu.kit.provideq.toolbox.SolveRequest; +import edu.kit.provideq.toolbox.SubRoutinePool; import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; /** * Decides which known {@link ProblemSolver} is suited best for a given problem, @@ -19,38 +25,25 @@ public abstract class MetaSolver< SolutionT, SolverT extends ProblemSolver> { - protected Set solvers = new HashSet<>(); - - public MetaSolver() { - } - - public MetaSolver(List problemSolvers) { - solvers.addAll(problemSolvers); - } + private final SolutionManager solutionManager = new SolutionManager<>(); + private ApplicationContext context; - @SafeVarargs - public MetaSolver(SolverT... problemSolvers) { - solvers.addAll(List.of(problemSolvers)); - } + protected Set solvers = new HashSet<>(); + private final ProblemType problemType; /** - * Adds a new solver to this meta solvers list of known solvers. - * - * @param problemSolver the new problem solver - * @return true in case the addition was successful, false otherwise + * Configures this meta solver to find the correct solver among {@code problemSolvers}, all of + * which solve problems of type {@code problemType}. */ - public boolean registerSolver(SolverT problemSolver) { - return solvers.add(problemSolver); + @SafeVarargs + public MetaSolver(ProblemType problemType, SolverT... problemSolvers) { + solvers.addAll(List.of(problemSolvers)); + this.problemType = problemType; } - /** - * Removes a solver from this meta solvers list of known solvers. - * - * @param problemSolver the solver - * @return true in case the removal was successful, false otherwise - */ - public boolean unregisterSolver(SolverT problemSolver) { - return solvers.remove(problemSolver); + @Autowired + public void setContext(ApplicationContext context) { + this.context = context; } /** @@ -63,13 +56,18 @@ public abstract SolverT findSolver( Problem problem, List metaSolverSettings); - public Optional getSolver(String id) { - if (id == null) { + /** + * Returns the solver from {@link #getAllSolvers()} with the given {@code solverId}. + * The optional is empty if there is no solver with the given {@code solverId} known by this + * meta-solver. + */ + public Optional getSolver(String solverId) { + if (solverId == null) { return Optional.empty(); } return solvers.stream() - .filter(solver -> solver.getId().equals(id)) + .filter(solver -> solver.getId().equals(solverId)) .findFirst(); } @@ -90,4 +88,41 @@ public List getSettings() { public int hashCode() { return this.solvers.hashCode(); } + + public ProblemType getProblemType() { + return problemType; + } + + public SolutionManager getSolutionManager() { + return solutionManager; + } + + /** + * Solves a given {@link SolveRequest} by using either the requested {@link ProblemSolver} + * (if specified) or the solver recommended by {@link #findSolver(Problem, List)}, and returns + * the solution. + */ + public Solution solve(SolveRequest request) { + Solution solution = this.getSolutionManager().createSolution(); + Problem problem = new Problem<>(request.requestContent, this.getProblemType()); + + SolverT solver = this + .getSolver(request.requestedSolverId) + .orElseGet(() -> this.findSolver(problem, request.requestedMetaSolverSettings)); + + solution.setSolverName(solver.getName()); + + SubRoutinePool subRoutinePool = + request.requestedSubSolveRequests == null + ? context.getBean(SubRoutinePool.class) + : context.getBean(SubRoutinePool.class, request.requestedSubSolveRequests); + + long start = System.currentTimeMillis(); + solver.solve(problem, solution, subRoutinePool); + long finish = System.currentTimeMillis(); + + solution.setExecutionMilliseconds(finish - start); + + return solution; + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java index 7c0ba8e1..00ca4498 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java @@ -1,5 +1,10 @@ package edu.kit.provideq.toolbox.meta; +import edu.kit.provideq.toolbox.SolveRequest; +import edu.kit.provideq.toolbox.featuremodel.SolveFeatureModelRequest; +import edu.kit.provideq.toolbox.maxcut.SolveMaxCutRequest; +import edu.kit.provideq.toolbox.sat.SolveSatRequest; + /** * The type of problem to solve. */ @@ -8,20 +13,51 @@ public enum ProblemType { * A satisfiability problem: * For a given boolean formula, check if there is an interpretation that satisfies the formula. */ - SAT, + SAT("sat", SolveSatRequest.class), /** * An optimization problem: * For a given graph, find the optimal separation of vertices that maximises the cut crossing edge * weight sum. */ - MAX_CUT, + MAX_CUT("max-cut", SolveMaxCutRequest.class), /** * A searching problem: - * For a given feature model, check for Void Feature Model, Dead Features, - * False-Optional Features, Redundant Constraints. - * More information + * For a given feature model, check if the model contains dead features. + * + * @see + * "Explaining Anomalies in Feature Models", Kowal et al., 2026 + */ + FEATURE_MODEL_ANOMALY_DEAD("feature-model-anomaly-dead", SolveFeatureModelRequest.class), + /** + * A searching problem: + * For a given feature model, check if the model is void. + * + * @see + * "Explaining Anomalies in Feature Models", Kowal et al., 2026 + */ + FEATURE_MODEL_ANOMALY_VOID("feature-model-anomaly-void", SolveFeatureModelRequest.class); + + private final String id; + private final Class> requestType; + + ProblemType(String id, Class> requestType) { + this.id = id; + this.requestType = requestType; + } + + /** + * Returns a unique identifier for this problem type. + */ + public String getId() { + return id; + } + + /** + * Returns the java class representing the body of a REST request to solve a problem of this type. */ - FEATURE_MODEL_ANOMALY, + public Class> getRequestType() { + return requestType; + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java b/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java index 997a7252..7597252f 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java @@ -1,12 +1,24 @@ package edu.kit.provideq.toolbox.meta; /** - * Definition of a sub routine that holds basic information. + * A sub-routine definition describes which problem type needs to be solved by a sub-routine and why + * it needs to be solved. * - * @param type type of the problem that is used in the sub routine - * @param url url of the problem to call the sub routine - * @param description description of the sub routine call to provide information where and why it is - * needed + * @param problemTypeId {@link ProblemType#getId() id} of the problem type that needs to be solved + * by this sub-routine. + * @param description description of the sub-routine call to provide information where and why it is + * needed. + * @see #SubRoutineDefinition(ProblemType, String) */ -public record SubRoutineDefinition(ProblemType type, String url, String description) { +public record SubRoutineDefinition(String problemTypeId, String description) { + /** + * Creates a sub-routine definition for a given problem type with a given description. + * + * @param type problem type that needs to be solved by this sub-routine. + * @param description description of the sub-routine call to provide information where and why it + * is needed. + */ + public SubRoutineDefinition(ProblemType type, String description) { + this(type.getId(), description); + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java b/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java index ecfa85c5..9cdfa142 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java @@ -3,6 +3,7 @@ import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.meta.MetaSolver; import edu.kit.provideq.toolbox.meta.Problem; +import edu.kit.provideq.toolbox.meta.ProblemType; import edu.kit.provideq.toolbox.meta.setting.MetaSolverSetting; import edu.kit.provideq.toolbox.sat.solvers.GamsSatSolver; import edu.kit.provideq.toolbox.sat.solvers.SatSolver; @@ -20,7 +21,7 @@ public class MetaSolverSat extends MetaSolver { - - private final MetaSolver metaSolver; - - public SatController(MetaSolver metaSolver) { - this.metaSolver = metaSolver; - } - - @Override - public ProblemType getProblemType() { - return ProblemType.SAT; - } - - @Override - public MetaSolver getMetaSolver() { - return metaSolver; - } - - @CrossOrigin - @PostMapping("/solve/sat") - public SolutionHandle solveSat(@RequestBody @Valid SolveSatRequest request) { - return super.solve(request).toStringSolution(DimacsCnfSolution::toHumanReadableString); - } - - @CrossOrigin - @GetMapping("/solve/sat") - public SolutionHandle getSolution(@RequestParam(name = "id") long id) { - return super.findSolution(id).toStringSolution(DimacsCnfSolution::toHumanReadableString); - } - - @CrossOrigin - @GetMapping("/sub-routines/sat") - public List getSubRoutines(@RequestParam(name = "id") String solverId) { - return super.getSubRoutines(solverId); - } - - @CrossOrigin - @GetMapping("/meta-solver/settings/sat") - public List getMetaSolverSettings() { - return metaSolver.getSettings(); - } - - @CrossOrigin - @GetMapping("/solvers/sat") - public Set getSolvers() { - return super.getSolvers(); - } -} diff --git a/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java index 5b2191b5..47b51689 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java @@ -1,52 +1,70 @@ package edu.kit.provideq.toolbox.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static edu.kit.provideq.toolbox.SolutionStatus.SOLVED; +import static org.hamcrest.Matchers.is; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.SolutionStatus; +import edu.kit.provideq.toolbox.SubRoutinePool; import edu.kit.provideq.toolbox.featuremodel.SolveFeatureModelRequest; -import edu.kit.provideq.toolbox.featuremodel.anomaly.solvers.FeatureModelAnomalySolver; +import edu.kit.provideq.toolbox.featuremodel.anomaly.dead.DeadFeatureMetaSolver; +import edu.kit.provideq.toolbox.featuremodel.anomaly.dead.SatBasedDeadFeatureSolver; +import edu.kit.provideq.toolbox.featuremodel.anomaly.voidmodel.SatBasedVoidFeatureSolver; +import edu.kit.provideq.toolbox.featuremodel.anomaly.voidmodel.VoidFeatureMetaSolver; +import edu.kit.provideq.toolbox.meta.ProblemSolver; +import edu.kit.provideq.toolbox.meta.ProblemType; +import edu.kit.provideq.toolbox.sat.MetaSolverSat; +import edu.kit.provideq.toolbox.sat.solvers.GamsSatSolver; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; 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.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.reactive.server.WebTestClient; -@SpringBootTest -@AutoConfigureMockMvc -public class FeatureModelAnomalySolverTest { - @Autowired - private MockMvc mvc; +@WebFluxTest +@Import(value = { + SolveRouter.class, + MetaSolverProvider.class, + DeadFeatureMetaSolver.class, + SatBasedDeadFeatureSolver.class, + VoidFeatureMetaSolver.class, + SatBasedVoidFeatureSolver.class, + SubRoutinePool.class, + MetaSolverSat.class, + GamsSatSolver.class, + GamsProcessRunner.class, + ResourceProvider.class, +}) +class FeatureModelAnomalySolverTest { @Autowired - private ObjectMapper mapper; + private WebTestClient client; - public static Stream provideAnomalySolverIds() { - String solverId = FeatureModelAnomalySolver.class.getName(); + static Stream provideAnomalySolverIds() { return Stream.of( - Arguments.of(solverId, "void", SolutionStatus.SOLVED), - Arguments.of(solverId, "dead", SolutionStatus.SOLVED), - - // not implemented yet, change to SOLVED when they have been implemented! - Arguments.of(solverId, "false-optional", SolutionStatus.INVALID), - Arguments.of(solverId, "redundant-constraints", SolutionStatus.INVALID) + Arguments.of(SatBasedVoidFeatureSolver.class, + ProblemType.FEATURE_MODEL_ANOMALY_VOID, SOLVED), + Arguments.of(SatBasedDeadFeatureSolver.class, + ProblemType.FEATURE_MODEL_ANOMALY_DEAD, SOLVED) ); } @ParameterizedTest @MethodSource("provideAnomalySolverIds") - void testFeatureModelAnomalySolver(String solverId, String anomalyType, - SolutionStatus expectedStatus) throws Exception { + void testFeatureModelAnomalySolver( + Class> solver, + ProblemType anomalyType, + SolutionStatus expectedStatus) { var req = new SolveFeatureModelRequest(); - req.requestedSolverId = solverId; + req.requestedSolverId = solver.getName(); req.requestContent = """ namespace Sandwich @@ -79,22 +97,15 @@ void testFeatureModelAnomalySolver(String solverId, String anomalyType, Lettuce """; - var requestBuilder = MockMvcRequestBuilders - .post("/solve/feature-model/anomaly/" + anomalyType) - .accept(MediaType.APPLICATION_JSON) + var response = client.post() + .uri("/solve/" + anomalyType.getId()) .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(req)); - - var result = mvc.perform(requestBuilder) - .andExpect(status().isOk()) - .andReturn() - .getResponse().getContentAsString(); - - JavaType solutionType = - mapper.getTypeFactory().constructParametricType(Solution.class, String.class); - Solution solution = mapper.readValue(result, solutionType); + .bodyValue(req) + .exchange(); - assertThat(solution.getStatus()) - .isSameAs(expectedStatus); + response.expectStatus().isOk(); + response.expectBody(new ParameterizedTypeReference>() { + }) + .value(Solution::getStatus, is(expectedStatus)); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java b/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java index 09c82ebe..b64da41a 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java @@ -1,35 +1,55 @@ package edu.kit.provideq.toolbox.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static edu.kit.provideq.toolbox.SolutionStatus.SOLVED; +import static org.hamcrest.Matchers.is; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.PythonProcessRunner; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.Solution; -import edu.kit.provideq.toolbox.SolutionStatus; +import edu.kit.provideq.toolbox.SubRoutinePool; +import edu.kit.provideq.toolbox.maxcut.MetaSolverMaxCut; import edu.kit.provideq.toolbox.maxcut.SolveMaxCutRequest; import edu.kit.provideq.toolbox.maxcut.solvers.GamsMaxCutSolver; import edu.kit.provideq.toolbox.maxcut.solvers.QiskitMaxCutSolver; +import java.time.Duration; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; 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.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.reactive.server.WebTestClient; -@SpringBootTest -@AutoConfigureMockMvc +@WebFluxTest +@Import(value = { + SolveRouter.class, + MetaSolverProvider.class, + MetaSolverMaxCut.class, + QiskitMaxCutSolver.class, + GamsMaxCutSolver.class, + QiskitMaxCutSolver.class, + SubRoutinePool.class, + GamsProcessRunner.class, + PythonProcessRunner.class, + ResourceProvider.class +}) class MaxCutSolversTest { @Autowired - private MockMvc mvc; + private WebTestClient client; - @Autowired - private ObjectMapper mapper; + @BeforeEach + void beforeEach() { + this.client = this.client.mutate() + .responseTimeout(Duration.ofSeconds(10)) + .build(); + } - public static Stream provideMaxCutSolverIds() { + static Stream provideMaxCutSolverIds() { return Stream.of( GamsMaxCutSolver.class.getName(), QiskitMaxCutSolver.class.getName() @@ -38,7 +58,7 @@ public static Stream provideMaxCutSolverIds() { @ParameterizedTest @MethodSource("provideMaxCutSolverIds") - void testMaxCutSolver(String solverId) throws Exception { + void testMaxCutSolver(String solverId) { var req = new SolveMaxCutRequest(); req.requestedSolverId = solverId; req.requestContent = """ @@ -70,22 +90,15 @@ void testMaxCutSolver(String solverId) throws Exception { ] ]"""; - var requestBuilder = MockMvcRequestBuilders - .post("/solve/max-cut") - .accept(MediaType.APPLICATION_JSON) + var response = client.post() + .uri("/solve/max-cut") .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(req)); - - var result = mvc.perform(requestBuilder) - .andExpect(status().isOk()) - .andReturn() - .getResponse().getContentAsString(); - - JavaType solutionType = - mapper.getTypeFactory().constructParametricType(Solution.class, String.class); - Solution solution = mapper.readValue(result, solutionType); + .bodyValue(req) + .exchange(); - assertThat(solution.getStatus()) - .isSameAs(SolutionStatus.SOLVED); + response.expectStatus().isOk(); + response.expectBody(new ParameterizedTypeReference>() { + }) + .value(Solution::getStatus, is(SOLVED)); } } diff --git a/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java b/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java index e849895d..38d1adb0 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java @@ -1,34 +1,41 @@ package edu.kit.provideq.toolbox.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static edu.kit.provideq.toolbox.SolutionStatus.SOLVED; +import static org.hamcrest.Matchers.is; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; +import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverProvider; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.Solution; -import edu.kit.provideq.toolbox.SolutionStatus; +import edu.kit.provideq.toolbox.SubRoutinePool; +import edu.kit.provideq.toolbox.sat.MetaSolverSat; import edu.kit.provideq.toolbox.sat.SolveSatRequest; import edu.kit.provideq.toolbox.sat.solvers.GamsSatSolver; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; 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.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -@SpringBootTest -@AutoConfigureMockMvc -public class SatSolverTest { - @Autowired - private MockMvc mvc; - +import org.springframework.test.web.reactive.server.WebTestClient; + +@WebFluxTest +@Import(value = { + SolveRouter.class, + MetaSolverProvider.class, + MetaSolverSat.class, + GamsSatSolver.class, + SubRoutinePool.class, + GamsProcessRunner.class, + ResourceProvider.class +}) +class SatSolverTest { @Autowired - private ObjectMapper mapper; + private WebTestClient client; - public static Stream provideSatSolverIds() { + static Stream provideSatSolverIds() { return Stream.of( GamsSatSolver.class.getName() ); @@ -36,27 +43,20 @@ public static Stream provideSatSolverIds() { @ParameterizedTest @MethodSource("provideSatSolverIds") - void testSatSolver(String solverId) throws Exception { + void testSatSolver(String solverId) { var req = new SolveSatRequest(); req.requestedSolverId = solverId; req.requestContent = "a and b"; - var requestBuilder = MockMvcRequestBuilders - .post("/solve/sat") - .accept(MediaType.APPLICATION_JSON) + var response = client.post() + .uri("/solve/sat") .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(req)); - - var result = mvc.perform(requestBuilder) - .andExpect(status().isOk()) - .andReturn() - .getResponse().getContentAsString(); - - JavaType solutionType = - mapper.getTypeFactory().constructParametricType(Solution.class, String.class); - Solution solution = mapper.readValue(result, solutionType); + .bodyValue(req) + .exchange(); - assertThat(solution.getStatus()) - .isSameAs(SolutionStatus.SOLVED); + response.expectStatus().isOk(); + response.expectBody(new ParameterizedTypeReference>() { + }) + .value(Solution::getStatus, is(SOLVED)); } }