diff --git a/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java b/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java index 7709c47f..b7c26b4e 100644 --- a/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java +++ b/src/main/java/edu/kit/provideq/toolbox/ProcessRunner.java @@ -178,7 +178,8 @@ public ProcessResult run(ProblemType problemType, long solutionId, String proble try { Process process = processBuilder.start(); - processOutput = readStream(process.inputReader()) + readStream(process.errorReader()); + processOutput = resourceProvider.readStream(process.inputReader()) + + resourceProvider.readStream(process.errorReader()); processExitCode = process.waitFor(); } catch (IOException | InterruptedException e) { @@ -221,18 +222,6 @@ private void addCommand(String command) { processBuilder.command(existingCommands); } - private String readStream(BufferedReader reader) throws IOException { - var inputBuilder = new StringBuilder(); - var line = reader.readLine(); - while (line != null) { - inputBuilder.append(line).append('\n'); - line = reader.readLine(); - } - reader.close(); - - return inputBuilder.toString(); - } - protected static ProcessBuilder createGenericProcessBuilder( String directory, String executableName, diff --git a/src/main/java/edu/kit/provideq/toolbox/ResourceProvider.java b/src/main/java/edu/kit/provideq/toolbox/ResourceProvider.java index 0f06ab47..acc71158 100644 --- a/src/main/java/edu/kit/provideq/toolbox/ResourceProvider.java +++ b/src/main/java/edu/kit/provideq/toolbox/ResourceProvider.java @@ -1,13 +1,18 @@ package edu.kit.provideq.toolbox; import edu.kit.provideq.toolbox.meta.ProblemType; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; @@ -60,6 +65,72 @@ public File getProblemDirectory(ProblemType problemType, long solutionId) throws return dir.toFile(); } + public List getExampleProblems(String examplesDirectoryPath) throws IOException { + // Reading the directory yields all names of the files in the directory, one per line + return readResourceString(examplesDirectoryPath) + .lines() + .map(file -> { + try { + return readResourceString(examplesDirectoryPath + "/" + file); + } catch (Exception e) { + return null; + } + }) + .toList(); + } + + /** + * Reads the input stream of a {@link BufferedReader} into a string. + * + * @param reader reader to read from + * @return full string of the input stream + * @throws IOException when the input stream couldn't be read + */ + public String readStream(BufferedReader reader) throws IOException { + var inputBuilder = new StringBuilder(); + + // Process first line manually to avoid adding a newline at the beginning + var line = reader.readLine(); + if (line != null) { + inputBuilder.append(line); + } + + line = reader.readLine(); + while (line != null) { + inputBuilder.append('\n').append(line); + line = reader.readLine(); + } + reader.close(); + + return inputBuilder.toString(); + } + + /** + * Reads the input stream of an {@link InputStream} into a string. + * + * @param stream stream to read from + * @return full string of the input stream + * @throws IOException when the input stream couldn't be read + */ + public String readStream(InputStream stream) throws IOException { + return readStream(new BufferedReader(new InputStreamReader(stream))); + } + + /** + * Reads the resource at the specified resource path into a string. + * + * @param resourcePath path that points to the requested resource + * @return full string of the data inside the resource + * @throws IOException when the resource is not available or couldn't be read + */ + public String readResourceString(String resourcePath) throws IOException { + Resource resource = resourceLoader.getResource("classpath:" + resourcePath); + + try (var reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + return readStream(reader); + } + } + /** * Returns the resource at the specified resource path. * @@ -68,11 +139,8 @@ public File getProblemDirectory(ProblemType problemType, long solutionId) throws * @throws IOException when the resource is not available */ public File getResource(String resourcePath) throws IOException { - return getRootFile(resourcePath); - - /* removed as long as we're not using resources directly Resource resource = resourceLoader.getResource("classpath:" + resourcePath); - return resource.getFile();*/ + return resource.getFile(); } /** diff --git a/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java index db4304a7..65d1c237 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/MetaSolverSettingsRouter.java @@ -8,10 +8,13 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.reactive.function.server.ServerResponse.ok; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; 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.springdoc.core.fn.builders.operation.Builder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,18 +54,43 @@ private RouterFunction defineMetaSolverSettingsRouteForMetaSolve return route().GET( getRouteForProblemType(problemType), req -> handleMetaSolverSettingsRouteForMetaSolver(metaSolver), - ops -> ops + ops -> handleMetaSolverSettingsRouteDocumentation(ops, metaSolver) + ).build(); + } + + private void handleMetaSolverSettingsRouteDocumentation( + Builder ops, MetaSolver metaSolver) { + var problemType = metaSolver.getProblemType(); + ops .operationId(getRouteForProblemType(problemType)) .tag(problemType.getId()) - .response(responseBuilder() - .responseCode(String.valueOf(HttpStatus.OK.value())) - .content(contentBuilder() + .description(("Returns the selection of settings available for of the " + + problemType.getId() + " meta-solver. Settings can be used to configure" + + " what the meat solver considers to choose the best solver.")) + .response(getResponseOk(metaSolver)); + } + + private static org.springdoc.core.fn.builders.apiresponse.Builder getResponseOk( + MetaSolver metaSolver) { + String example; + try { + example = new ObjectMapper().writeValueAsString(metaSolver.getSettings()); + } catch (JsonProcessingException e) { + throw new RuntimeException("example could not be parsed", e); + } + + return responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() .mediaType(APPLICATION_JSON_VALUE) + .example(org.springdoc.core.fn.builders.exampleobject.Builder + .exampleOjectBuilder() + .name(metaSolver.getProblemType().getId()) + .value(example)) .array(arraySchemaBuilder().schema( - schemaBuilder().implementation(MetaSolverSetting.class))) - ) - ) - ).build(); + schemaBuilder().implementation(MetaSolverSetting.class)) + ) + ); } private Mono handleMetaSolverSettingsRouteForMetaSolver( diff --git a/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java index 54b7c370..18fded04 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/SolveRouter.java @@ -1,6 +1,7 @@ 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.requestbody.Builder.requestBodyBuilder; @@ -11,12 +12,19 @@ import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.ServerResponse.ok; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.provideq.toolbox.MetaSolverProvider; import edu.kit.provideq.toolbox.Solution; import edu.kit.provideq.toolbox.SolveRequest; 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 java.util.function.Consumer; +import java.util.stream.Collectors; +import org.springdoc.core.fn.builders.operation.Builder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; @@ -57,26 +65,11 @@ RouterFunction getSolveRoutes() { } private RouterFunction defineRouteForMetaSolver(MetaSolver metaSolver) { - var problemType = metaSolver.getProblemType(); return route().POST( - getSolveRouteForProblemType(problemType), + getSolveRouteForProblemType(metaSolver.getProblemType()), 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(Solution.class) - ) + ops -> handleRouteDocumentation(metaSolver, ops) ).build(); } @@ -141,6 +134,131 @@ private Mono handleSolutionRouteForMetaSolver(MetaSolver metaSolver, Builder ops) { + var problemType = metaSolver.getProblemType(); + ops + .operationId(getSolveRouteForProblemType(problemType)) + .tag(problemType.getId()) + .description("Solves a " + problemType.getId() + " problem. To solve the problem, " + + "either the meta-solver will choose the best available solver," + + "or a specific solver selected in the request will be used.") + .requestBody(requestBodyBuilder() + .content(getRequestContent(metaSolver)) + .required(true)) + .response(getResponseOk(metaSolver)); + } + + private static org.springdoc.core.fn.builders.apiresponse.Builder getResponseOk( + MetaSolver metaSolver) { + return responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .example(getExampleSolved(metaSolver)) + .example(getExampleInvalid(metaSolver)) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(Solution.class)))); + } + + private static org.springdoc.core.fn.builders.exampleobject.Builder getExampleSolved( + MetaSolver metaSolver) { + return getExampleOk( + "Solved", + metaSolver, + solution -> { + solution.setSolutionData("Solution data to solve the problem"); + solution.complete(); + }); + } + + private static org.springdoc.core.fn.builders.exampleobject.Builder getExampleInvalid( + MetaSolver metaSolver) { + return getExampleOk( + "Error", + metaSolver, + solution -> { + solution.setDebugData("Some error occurred"); + solution.abort(); + }); + } + + private static org.springdoc.core.fn.builders.exampleobject.Builder getExampleOk( + String exampleName, + MetaSolver metaSolver, + Consumer> solutionModifier) { + // Prepare a solved solution with some example data + var solvedSolution = new Solution(42); + solvedSolution.setExecutionMilliseconds(42); + metaSolver.getAllSolvers().stream() + .findFirst() + .ifPresent(solver -> solvedSolution.setSolverName(solver.getName())); + solutionModifier.accept(solvedSolution); + + // Convert the solution to a string + String solvedSolutionString; + try { + solvedSolutionString = new ObjectMapper().writeValueAsString(solvedSolution); + } catch (JsonProcessingException e) { + throw new RuntimeException("example could not be parsed", e); + } + + // Build the example + return org.springdoc.core.fn.builders.exampleobject.Builder + .exampleOjectBuilder() + .name(exampleName) + .description("The problem was solved successfully.") + .value(solvedSolutionString); + } + + private org.springdoc.core.fn.builders.content.Builder getRequestContent( + MetaSolver metaSolver) { + Object content = metaSolver.getExampleProblems().stream() + .findFirst() + .orElseThrow(() -> new RuntimeException("no example available")); + + var request = new SolveRequest<>(); + request.requestContent = content; + request.requestedMetaSolverSettings = metaSolver.getSettings(); + + metaSolver.getAllSolvers().stream() + .findFirst() + .ifPresentOrElse(solver -> { + request.requestedSolverId = solver.getId(); + request.requestedSubSolveRequests = solver.getSubRoutines().stream() + .collect(Collectors.toMap( + SubRoutineDefinition::type, + subRoutine -> { + var subSolveRequest = new SolveRequest<>(); + + subSolveRequest.requestedSolverId = metaSolverProvider + .getMetaSolver(subRoutine.type()) + .getAllSolvers().stream() + .findFirst() + .map(ProblemSolver::getId) + .orElse(""); + return subSolveRequest; + })); + }, () -> { + throw new RuntimeException("no solver found"); + }); + + String requestString; + try { + requestString = new ObjectMapper().writeValueAsString(request); + } catch (JsonProcessingException exception) { + throw new RuntimeException("no example available", exception); + } + + var problemType = metaSolver.getProblemType(); + return contentBuilder() + .example(org.springdoc.core.fn.builders.exampleobject.Builder.exampleOjectBuilder() + .name(problemType.getId()) + .value(requestString)) + .schema(schemaBuilder().implementation( + problemType.getRequestType())) + .mediaType(APPLICATION_JSON_VALUE); + } + private String getSolveRouteForProblemType(ProblemType type) { return "/solve/" + 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 index a9bc93a4..3530e6f4 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/SolversRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/SolversRouter.java @@ -8,10 +8,14 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.reactive.function.server.ServerResponse.ok; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; 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 java.util.List; +import org.springdoc.core.fn.builders.operation.Builder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,29 +56,54 @@ private RouterFunction defineSolversRouteForMetaSolver( 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))) - ) - ) + ops -> handleSolversRouteDocumentation(ops, metaSolver) ).build(); } private Mono handleSolversRouteForMetaSolver(MetaSolver metaSolver) { - var solvers = metaSolver.getAllSolvers().stream() - .map(solver -> new ProblemSolverInfo(solver.getId(), solver.getName())) - .toList(); + var solvers = getAllSolverInfos(metaSolver); return ok().body(Mono.just(solvers), new ParameterizedTypeReference<>() { }); } + private static List getAllSolverInfos(MetaSolver metaSolver) { + return metaSolver.getAllSolvers().stream() + .map(solver -> new ProblemSolverInfo(solver.getId(), solver.getName())) + .toList(); + } + + private void handleSolversRouteDocumentation(Builder ops, MetaSolver metaSolver) { + ops + .operationId(getSolversRouteForProblemType(metaSolver.getProblemType())) + .tag(metaSolver.getProblemType().getId()) + .description("Returns a list of solvers available to solve the " + + metaSolver.getProblemType().getId() + " problem type.") + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(getOkResponseContent(metaSolver)) + ); + } + + private static org.springdoc.core.fn.builders.content.Builder getOkResponseContent( + MetaSolver metaSolver) { + var allSolvers = getAllSolverInfos(metaSolver); + String example; + try { + example = new ObjectMapper().writeValueAsString(allSolvers); + } catch (JsonProcessingException e) { + throw new RuntimeException("solvers could not be parsed", e); + } + + return contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .example(org.springdoc.core.fn.builders.exampleobject.Builder.exampleOjectBuilder() + .name(metaSolver.getProblemType().getId()) + .value(example)) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(ProblemSolverInfo.class))); + } + 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 index 8e56cc4b..817420ed 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineRouter.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/SubRoutineRouter.java @@ -9,12 +9,15 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.reactive.function.server.ServerResponse.ok; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; 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.springdoc.core.fn.builders.content.Builder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,18 +60,7 @@ private RouterFunction defineSubRoutineRouteForMetaSolver( 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))) - ) - ) + ops -> handleSubRoutineRouteDocumentation(metaSolver, ops) ).build(); } @@ -84,6 +76,63 @@ private Mono handleSubRoutineRouteForMetaSolver(MetaSolver metaSolver, org.springdoc.core.fn.builders.operation.Builder ops) { + ProblemType problemType = metaSolver.getProblemType(); + ops.operationId(getSubRoutinesRouteForProblemType(problemType)) + .parameter(getParameterBuilder(metaSolver)) + .tag(problemType.getId()) + .description("Returns the sub-routines available for the given solver id of type " + + problemType.getId() + ". " + + "Sub-routines are used in some solvers to solve sub-problems. " + + "Passing a sub-routine in a solve request will ensure that" + + " the desired sub-routine is used in the calculation.") + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.OK.value())) + .content(getOkResponseContent(metaSolver))) + .response(responseBuilder() + .responseCode(String.valueOf(HttpStatus.NOT_FOUND.value()))); + } + + private static org.springdoc.core.fn.builders.parameter.Builder + getParameterBuilder(MetaSolver metaSolver) { + return parameterBuilder() + .in(ParameterIn.QUERY) + .name("id") + .description("The id of the solver to get the sub-routines from." + + " Use the endpoint GET /solvers/" + metaSolver.getProblemType().getId() + + " to get a list of available solver ids.") + .required(true) + .example(metaSolver + .getAllSolvers().stream() + .findFirst() + .map(ProblemSolver::getId) + .orElseThrow(() -> new RuntimeException("No solver found"))); + } + + private static Builder getOkResponseContent(MetaSolver metaSolver) { + String example = metaSolver + .getAllSolvers().stream() + .findFirst() + .map(solver -> { + var subRoutines = solver.getSubRoutines(); + try { + return new ObjectMapper().writeValueAsString(subRoutines); + } catch (JsonProcessingException e) { + throw new RuntimeException("example could not be parsed", e); + } + }) + .orElseThrow(() -> new RuntimeException("no solver found")); + + return contentBuilder() + .mediaType(APPLICATION_JSON_VALUE) + .example(org.springdoc.core.fn.builders.exampleobject.Builder.exampleOjectBuilder() + .name(metaSolver.getProblemType().getId()) + .value(example)) + .array(arraySchemaBuilder().schema( + schemaBuilder().implementation(SubRoutineDefinition.class))); + } + private String getSubRoutinesRouteForProblemType(ProblemType type) { return "/sub-routines/" + type.getId(); } 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 index e100718a..2e6cad78 100644 --- 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 @@ -1,11 +1,14 @@ package edu.kit.provideq.toolbox.featuremodel.anomaly.dead; +import edu.kit.provideq.toolbox.ResourceProvider; 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.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** @@ -14,8 +17,17 @@ @Component public class DeadFeatureMetaSolver extends MetaSolver> { - public DeadFeatureMetaSolver(SatBasedDeadFeatureSolver solver) { + private final String examplesDirectoryPath; + private final ResourceProvider resourceProvider; + + @Autowired + public DeadFeatureMetaSolver( + @Value("${examples.directory.feature-model}") String examplesDirectoryPath, + ResourceProvider resourceProvider, + SatBasedDeadFeatureSolver solver) { super(ProblemType.FEATURE_MODEL_ANOMALY_DEAD, solver); + this.examplesDirectoryPath = examplesDirectoryPath; + this.resourceProvider = resourceProvider; } @Override @@ -24,4 +36,13 @@ public ProblemSolver findSolver(Problem problem, // we only have one solver at this point return getAllSolvers().stream().findAny().orElseThrow(); } + + @Override + public List getExampleProblems() { + try { + return resourceProvider.getExampleProblems(examplesDirectoryPath); + } catch (Exception e) { + throw new RuntimeException("Could not load example problems", e); + } + } } 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 index bdac824c..f24deff8 100644 --- 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 @@ -1,11 +1,14 @@ package edu.kit.provideq.toolbox.featuremodel.anomaly.voidmodel; +import edu.kit.provideq.toolbox.ResourceProvider; 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.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** @@ -14,8 +17,17 @@ @Component public class VoidFeatureMetaSolver extends MetaSolver> { - public VoidFeatureMetaSolver(SatBasedVoidFeatureSolver solver) { + private final String examplesDirectoryPath; + private final ResourceProvider resourceProvider; + + @Autowired + public VoidFeatureMetaSolver( + @Value("${examples.directory.feature-model}") String examplesDirectoryPath, + ResourceProvider resourceProvider, + SatBasedVoidFeatureSolver solver) { super(ProblemType.FEATURE_MODEL_ANOMALY_VOID, solver); + this.examplesDirectoryPath = examplesDirectoryPath; + this.resourceProvider = resourceProvider; } @Override @@ -24,4 +36,13 @@ public ProblemSolver findSolver(Problem problem, // we only have one solver at this point return getAllSolvers().stream().findAny().orElseThrow(); } + + @Override + public List getExampleProblems() { + try { + return resourceProvider.getExampleProblems(examplesDirectoryPath); + } catch (Exception e) { + throw new RuntimeException("Could not load example problems", e); + } + } } 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 2b8a921f..934a564d 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/MetaSolverMaxCut.java @@ -1,5 +1,6 @@ package edu.kit.provideq.toolbox.maxcut; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.maxcut.solvers.CirqMaxCutSolver; import edu.kit.provideq.toolbox.maxcut.solvers.GamsMaxCutSolver; import edu.kit.provideq.toolbox.maxcut.solvers.MaxCutSolver; @@ -12,6 +13,7 @@ import java.util.List; import java.util.Random; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** @@ -19,12 +21,19 @@ */ @Component public class MetaSolverMaxCut extends MetaSolver { + private final String examplesDirectoryPath; + private final ResourceProvider resourceProvider; @Autowired - public MetaSolverMaxCut(QiskitMaxCutSolver qiskitSolver, - GamsMaxCutSolver gamsSolver, - CirqMaxCutSolver cirqSolver) { - super(ProblemType.MAX_CUT, qiskitSolver, gamsSolver, cirqSolver); + public MetaSolverMaxCut( + @Value("${examples.directory.max-cut}") String examplesDirectoryPath, + ResourceProvider resourceProvider, + QiskitMaxCutSolver qiskitMaxCutSolver, + GamsMaxCutSolver gamsMaxCutSolver, + CirqMaxCutSolver cirqMaxCutSolver) { + super(ProblemType.MAX_CUT, qiskitMaxCutSolver, gamsMaxCutSolver, cirqMaxCutSolver); + this.examplesDirectoryPath = examplesDirectoryPath; + this.resourceProvider = resourceProvider; } @Override @@ -33,4 +42,13 @@ public MaxCutSolver findSolver( List metaSolverSettings) { return (new ArrayList<>(this.solvers)).get((new Random()).nextInt(this.solvers.size())); } + + @Override + public List getExampleProblems() { + try { + return resourceProvider.getExampleProblems(examplesDirectoryPath); + } catch (Exception e) { + throw new RuntimeException("Could not load example problems", e); + } + } } 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 ab6be8b9..83c2ff85 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/MetaSolver.java @@ -125,4 +125,6 @@ public Solution solve(SolveRequest request) { return solution; } + + public abstract List getExampleProblems(); } 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 00ca4498..49d081b4 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,6 @@ package edu.kit.provideq.toolbox.meta; +import com.fasterxml.jackson.annotation.JsonValue; import edu.kit.provideq.toolbox.SolveRequest; import edu.kit.provideq.toolbox.featuremodel.SolveFeatureModelRequest; import edu.kit.provideq.toolbox.maxcut.SolveMaxCutRequest; @@ -50,6 +51,7 @@ public enum ProblemType { /** * Returns a unique identifier for this problem type. */ + @JsonValue public String getId() { return id; } 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 7597252f..f1350958 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/SubRoutineDefinition.java @@ -4,21 +4,10 @@ * A sub-routine definition describes which problem type needs to be solved by a sub-routine and why * it needs to be solved. * - * @param problemTypeId {@link ProblemType#getId() id} of the problem type that needs to be solved + * @param type {@link ProblemType} 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(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); - } +public record SubRoutineDefinition(ProblemType type, String 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 9cdfa142..34b199c5 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/MetaSolverSat.java @@ -1,5 +1,6 @@ package edu.kit.provideq.toolbox.sat; +import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.format.cnf.dimacs.DimacsCnfSolution; import edu.kit.provideq.toolbox.meta.MetaSolver; import edu.kit.provideq.toolbox.meta.Problem; @@ -11,6 +12,7 @@ import java.util.List; import java.util.Random; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** @@ -18,11 +20,18 @@ */ @Component public class MetaSolverSat extends MetaSolver { + private final String examplesDirectoryPath; + private final ResourceProvider resourceProvider; @Autowired - public MetaSolverSat(GamsSatSolver gamsSatSolver) { + public MetaSolverSat( + @Value("${examples.directory.sat}") String examplesDirectoryPath, + ResourceProvider resourceProvider, + GamsSatSolver gamsSatSolver) { super(ProblemType.SAT, gamsSatSolver); //TODO: register more SAT Solvers + this.examplesDirectoryPath = examplesDirectoryPath; + this.resourceProvider = resourceProvider; } @Override @@ -30,4 +39,13 @@ public SatSolver findSolver(Problem problem, List met // todo add decision return (new ArrayList<>(this.solvers)).get((new Random()).nextInt(this.solvers.size())); } + + @Override + public List getExampleProblems() { + try { + return resourceProvider.getExampleProblems(examplesDirectoryPath); + } catch (Exception e) { + throw new RuntimeException("Could not load example problems", e); + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 15543c50..a179b8b9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,4 +10,9 @@ qiskit.directory.max-cut=${qiskit.directory}/max-cut cirq.directory=cirq cirq.directory.max-cut=${cirq.directory}/max-cut +examples.directory=examples +examples.directory.max-cut=${examples.directory}/max-cut +examples.directory.sat=${examples.directory}/sat +examples.directory.feature-model=${examples.directory}/feature-model + springdoc.swagger-ui.path=/ diff --git a/src/main/resources/examples/feature-model/sandwich.txt b/src/main/resources/examples/feature-model/sandwich.txt new file mode 100644 index 00000000..cdd25827 --- /dev/null +++ b/src/main/resources/examples/feature-model/sandwich.txt @@ -0,0 +1,29 @@ +namespace Sandwich + +features + Sandwich {extended__} + mandatory + Bread + alternative + "Full Grain" {Calories 203, Price 1.99, Organic true} + Flatbread {Calories 90, Price 0.79, Organic true} + Toast {Calories 250, Price 0.99, Organic false} + optional + Cheese + optional + Gouda + alternative + Sprinkled {Fat {value 35, unit "g"}} + Slice {Fat {value 35, unit "g"}} + Cheddar + "Cream Cheese" + Meat + or + "Salami" {Producer "Farmer Bob"} + Ham {Producer "Farmer Sam"} + "Chicken Breast" {Producer "Farmer Sam"} + Vegetables + optional + "Cucumber" + Tomatoes + Lettuce \ No newline at end of file diff --git a/src/main/resources/examples/max-cut/3-nodes-3-edges.txt b/src/main/resources/examples/max-cut/3-nodes-3-edges.txt new file mode 100644 index 00000000..f1db0b36 --- /dev/null +++ b/src/main/resources/examples/max-cut/3-nodes-3-edges.txt @@ -0,0 +1,27 @@ +graph [ + id 42 + node [ + id 1 + label "1" + ] + node [ + id 2 + label "2" + ] + node [ + id 3 + label "3" + ] + edge [ + source 1 + target 2 + ] + edge [ + source 2 + target 3 + ] + edge [ + source 3 + target 1 + ] +] \ No newline at end of file diff --git a/src/main/resources/examples/sat/simple-and-or.txt b/src/main/resources/examples/sat/simple-and-or.txt new file mode 100644 index 00000000..983819f9 --- /dev/null +++ b/src/main/resources/examples/sat/simple-and-or.txt @@ -0,0 +1 @@ +a and b or c \ No newline at end of file diff --git a/src/main/resources/examples/sat/simple-and.txt b/src/main/resources/examples/sat/simple-and.txt new file mode 100644 index 00000000..9832407e --- /dev/null +++ b/src/main/resources/examples/sat/simple-and.txt @@ -0,0 +1 @@ +a and b \ No newline at end of file diff --git a/src/test/java/edu/kit/provideq/toolbox/MetaSolverHelper.java b/src/test/java/edu/kit/provideq/toolbox/MetaSolverHelper.java new file mode 100644 index 00000000..046bdbd3 --- /dev/null +++ b/src/test/java/edu/kit/provideq/toolbox/MetaSolverHelper.java @@ -0,0 +1,21 @@ +package edu.kit.provideq.toolbox; + +import com.google.common.collect.Lists; +import edu.kit.provideq.toolbox.meta.MetaSolver; +import java.util.List; +import java.util.stream.Stream; + +public class MetaSolverHelper { + public static Stream> getAllArgumentCombinations(MetaSolver metaSolver) { + // Convert all solvers to their solver id + var solvers = metaSolver.getAllSolvers().stream() + .map(x -> x.getClass().getName()) + .toList(); + + // Get all example problems + var problems = metaSolver.getExampleProblems(); + + // Return all combinations + return Lists.cartesianProduct(solvers, problems).stream(); + } +} 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 47b51689..8983702e 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/FeatureModelAnomalySolverTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.is; import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverHelper; import edu.kit.provideq.toolbox.MetaSolverProvider; import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.Solution; @@ -14,11 +15,13 @@ 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.MetaSolver; 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.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -30,6 +33,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebFluxTest @Import(value = { SolveRouter.class, @@ -48,54 +52,38 @@ class FeatureModelAnomalySolverTest { @Autowired private WebTestClient client; - static Stream provideAnomalySolverIds() { - return Stream.of( - Arguments.of(SatBasedVoidFeatureSolver.class, - ProblemType.FEATURE_MODEL_ANOMALY_VOID, SOLVED), - Arguments.of(SatBasedDeadFeatureSolver.class, - ProblemType.FEATURE_MODEL_ANOMALY_DEAD, SOLVED) - ); + @Autowired + private VoidFeatureMetaSolver voidMetaSolver; + + @Autowired + private DeadFeatureMetaSolver deadFeatureMetaSolver; + + Stream provideArguments() { + // Return combined stream + return Stream.concat( + getArguments(voidMetaSolver, ProblemType.FEATURE_MODEL_ANOMALY_VOID), + getArguments(deadFeatureMetaSolver, ProblemType.FEATURE_MODEL_ANOMALY_DEAD)); + } + + static Stream getArguments(MetaSolver metaSolver, ProblemType problemType) { + return MetaSolverHelper.getAllArgumentCombinations(metaSolver) + .map(list -> Arguments.of( + list.get(0), + problemType, + SOLVED, + list.get(1))); } @ParameterizedTest - @MethodSource("provideAnomalySolverIds") + @MethodSource("provideArguments") void testFeatureModelAnomalySolver( Class> solver, ProblemType anomalyType, - SolutionStatus expectedStatus) { + SolutionStatus expectedStatus, + String content) { var req = new SolveFeatureModelRequest(); req.requestedSolverId = solver.getName(); - req.requestContent = """ - namespace Sandwich - - features - Sandwich {extended__} \s - mandatory - Bread \s - alternative - "Full Grain" {Calories 203, Price 1.99, Organic true} - Flatbread {Calories 90, Price 0.79, Organic true} - Toast {Calories 250, Price 0.99, Organic false} - optional - Cheese \s - optional - Gouda \s - alternative - Sprinkled {Fat {value 35, unit "g"}} - Slice {Fat {value 35, unit "g"}} - Cheddar - "Cream Cheese" - Meat \s - or - "Salami" {Producer "Farmer Bob"} - Ham {Producer "Farmer Sam"} - "Chicken Breast" {Producer "Farmer Sam"} - Vegetables \s - optional - "Cucumber" - Tomatoes - Lettuce - """; + req.requestContent = content; var response = client.post() .uri("/solve/" + anomalyType.getId()) 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 25e73607..45b2a269 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/MaxCutSolversTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.is; import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverHelper; import edu.kit.provideq.toolbox.MetaSolverProvider; import edu.kit.provideq.toolbox.PythonProcessRunner; import edu.kit.provideq.toolbox.ResourceProvider; @@ -17,7 +18,9 @@ import java.time.Duration; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; 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.reactive.WebFluxTest; @@ -26,6 +29,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebFluxTest @Import(value = { SolveRouter.class, @@ -44,6 +48,9 @@ class MaxCutSolversTest { @Autowired private WebTestClient client; + @Autowired + private MetaSolverMaxCut metaSolverMaxCut; + @BeforeEach void beforeEach() { this.client = this.client.mutate() @@ -51,47 +58,17 @@ void beforeEach() { .build(); } - static Stream provideMaxCutSolverIds() { - return Stream.of( - GamsMaxCutSolver.class.getName(), - QiskitMaxCutSolver.class.getName(), - CirqMaxCutSolver.class.getName() - ); + Stream provideArguments() { + return MetaSolverHelper.getAllArgumentCombinations(metaSolverMaxCut) + .map(list -> Arguments.of(list.get(0), list.get(1))); } @ParameterizedTest - @MethodSource("provideMaxCutSolverIds") - void testMaxCutSolver(String solverId) { + @MethodSource("provideArguments") + void testMaxCutSolver(String solverId, String content) { var req = new SolveMaxCutRequest(); req.requestedSolverId = solverId; - req.requestContent = """ - graph [ - id 42 - node [ - id 1 - label "1" - ] - node [ - id 2 - label "2" - ] - node [ - id 3 - label "3" - ] - edge [ - source 1 - target 2 - ] - edge [ - source 2 - target 3 - ] - edge [ - source 3 - target 1 - ] - ]"""; + req.requestContent = content; var response = client.post() .uri("/solve/max-cut") 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 38d1adb0..9af6e499 100644 --- a/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java +++ b/src/test/java/edu/kit/provideq/toolbox/api/SatSolverTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.is; import edu.kit.provideq.toolbox.GamsProcessRunner; +import edu.kit.provideq.toolbox.MetaSolverHelper; import edu.kit.provideq.toolbox.MetaSolverProvider; import edu.kit.provideq.toolbox.ResourceProvider; import edu.kit.provideq.toolbox.Solution; @@ -12,7 +13,9 @@ import edu.kit.provideq.toolbox.sat.SolveSatRequest; import edu.kit.provideq.toolbox.sat.solvers.GamsSatSolver; import java.util.stream.Stream; +import org.junit.jupiter.api.TestInstance; 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.reactive.WebFluxTest; @@ -21,6 +24,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebFluxTest @Import(value = { SolveRouter.class, @@ -35,18 +39,20 @@ class SatSolverTest { @Autowired private WebTestClient client; - static Stream provideSatSolverIds() { - return Stream.of( - GamsSatSolver.class.getName() - ); + @Autowired + private MetaSolverSat metaSolverSat; + + Stream provideArguments() { + return MetaSolverHelper.getAllArgumentCombinations(metaSolverSat) + .map(list -> Arguments.of(list.get(0), list.get(1))); } @ParameterizedTest - @MethodSource("provideSatSolverIds") - void testSatSolver(String solverId) { + @MethodSource("provideArguments") + void testSatSolver(String solverId, String content) { var req = new SolveSatRequest(); req.requestedSolverId = solverId; - req.requestContent = "a and b"; + req.requestContent = content; var response = client.post() .uri("/solve/sat")