diff --git a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/model/Solution.java b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/model/Solution.java new file mode 100644 index 00000000..aca5acdf --- /dev/null +++ b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/model/Solution.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2024 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.core.model; + +import java.util.UUID; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.OneToOne; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entity representing a solution. + */ +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Data +@Entity +public class Solution extends KnowledgeArtifact { + + private UUID patternId; + + @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true) + @EqualsAndHashCode.Exclude + @JoinTable( + name = "SolutionFile", + joinColumns = @JoinColumn(name = "solution_id"), + inverseJoinColumns = @JoinColumn(name = "file_id") + ) + private File file; + + private String solutionType; +} diff --git a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/repository/SolutionRepository.java b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/repository/SolutionRepository.java new file mode 100644 index 00000000..db15bf4c --- /dev/null +++ b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/repository/SolutionRepository.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2024 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.core.repository; + +import java.util.UUID; + +import org.planqk.atlas.core.model.Solution; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.stereotype.Repository; + +/** + * Repository to access {@link Solution}s available in the database. + */ +@Repository +@RepositoryRestResource(exported = false) +public interface SolutionRepository extends JpaRepository { + +} diff --git a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/FileServiceImpl.java b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/FileServiceImpl.java index 6958053f..4b6ef30d 100644 --- a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/FileServiceImpl.java +++ b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/FileServiceImpl.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URLConnection; import java.util.UUID; import org.planqk.atlas.core.model.File; @@ -54,7 +55,14 @@ public File create(MultipartFile file) { final File createdFile = new File(); createdFile.setName(file.getOriginalFilename()); - createdFile.setMimeType(file.getContentType()); + + var contentType = file.getContentType(); + + if (contentType == null) { + contentType = URLConnection.guessContentTypeFromName(file.getOriginalFilename()); + } + + createdFile.setMimeType(contentType); createdFile.setFileURL(file.getOriginalFilename()); final FileData fileData = new FileData(); @@ -80,9 +88,9 @@ public File findById(UUID fileId) { @Transactional public void delete(UUID id) { final File file = findById(id); - final FileData fileData = fileDataRepository.findByFile(file); - fileRepository.deleteById(id); + final var fileData = fileDataRepository.findByFile(file); fileDataRepository.delete(fileData); + fileRepository.delete(file); } @Override diff --git a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionService.java b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionService.java new file mode 100644 index 00000000..3159a38f --- /dev/null +++ b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionService.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2020 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.core.services; + +import java.util.UUID; + +import org.planqk.atlas.core.model.File; +import org.planqk.atlas.core.model.Implementation; +import org.planqk.atlas.core.model.Solution; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +/** + * Service class for operations related to interacting and modifying {@link Solution}s in the database. + */ +public interface SolutionService { + + /** + * Creates a new database entry for a given {@link Solution} and save it to the database. + *

+ * The ID of the {@link Solution} parameter should be null, since the ID will be generated by the database when + * creating the entry. The validation for this is done by the Controller layer, which will reject {@link Solution}s + * with a given ID in its create path. + * + * @param solution The {@link Solution} object describing the properties of a solution that should be saved to + * the database + * @return The {@link Solution} object that represents the saved status from the database + */ + @Transactional + Solution create(Solution solution); + + /** + * Retrieve multiple {@link Solution} entries from the database. + *

+ * The amount of entries is based on the given {@link Pageable} parameter. If the {@link Pageable} is unpaged a + * {@link Page} with all entries is queried. + *

+ * If no search should be executed the search parameter can be left null or empty. + * + * @param pageable The page information, namely page size and page number, of the page we want to retrieve + * @return The page of queried {@link Solution} entries + */ + Page findAll(Pageable pageable); + + /** + * Find a database entry of a {@link Solution} that is already saved in the database. This search is based on the + * ID the database has given the {@link Solution} object when it was created and first saved to the database. + *

+ * If there is no entry found in the database this method will throw a {@link java.util.NoSuchElementException}. + * + * @param solutionId The ID of the {@link Solution} we want to find + * @return The {@link Solution} with the given ID + */ + Solution findById(UUID solutionId); + + /** + * Update an existing {@link Solution} database entry by saving the updated {@link Solution} object to the + * database. + *

+ * The ID of the {@link Solution} parameter has to be set to the ID of the database entry we want to update. The + * validation for this ID to be set is done by the Controller layer, which will reject {@link Solution}s without a + * given ID in its update path. This ID will be used to query the existing {@link Solution} entry we want to + * update. If no {@link Solution} entry with the given ID is found this method will throw a {@link + * java.util.NoSuchElementException}. + * + * @param solution The {@link Solution} we want to update with its updated properties + * @return the updated {@link Solution} object that represents the updated status of the database + */ + @Transactional + Solution update(Solution solution); + + /** + * Delete an existing {@link Solution} entry from the database. This deletion is based on the ID the database has + * given the {@link Solution} when it was created and first saved to the database. + *

+ * When deleting an {@link Solution} the file referenced by the {@link Solution} will be deleted together + * with it. + * If no entry with the given ID is found this method will throw a {@link java.util.NoSuchElementException}. + * + * @param solutionId The ID of the {@link Solution} we want to delete + */ + @Transactional + void delete(UUID solutionId); + + /** + * Creates a {@link File} entry in the database from a multipartfile and links it to a given {@link + * Solution}. + * + * @param solutionId The ID of the {@link Solution} we want the {@link File} to be linked. + * @param multipartFile The multipart from which we want to create a File entity and link it to the {@link + * Solution} + * @return The created and linked {@link File} + */ + File addFileToSolution(UUID solutionId, MultipartFile multipartFile); +} diff --git a/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionServiceImpl.java b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionServiceImpl.java new file mode 100644 index 00000000..17486aab --- /dev/null +++ b/org.planqk.atlas.core/src/main/java/org/planqk/atlas/core/services/SolutionServiceImpl.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2020-2021 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.core.services; + +import java.util.UUID; + +import org.planqk.atlas.core.model.File; +import org.planqk.atlas.core.model.Solution; +import org.planqk.atlas.core.repository.SolutionRepository; +import org.planqk.atlas.core.util.ServiceUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@AllArgsConstructor +public class SolutionServiceImpl implements SolutionService { + + private final SolutionRepository solutionRepository; + + private final FileService fileService; + + @Override + @Transactional + public Solution create(Solution solution) { + return solutionRepository.save(solution); + } + + @Override + public Page findAll(@NonNull Pageable pageable) { + return solutionRepository.findAll(pageable); + } + + @Override + public Solution findById(@NonNull UUID solutionId) { + return ServiceUtils.findById(solutionId, Solution.class, solutionRepository); + } + + @Override + @Transactional + public Solution update(@NonNull Solution solution) { + final Solution persistedAlgorithm = findById(solution.getId()); + persistedAlgorithm.setPatternId(solution.getPatternId()); + + return solutionRepository.save(persistedAlgorithm); + } + + @Override + @Transactional + public void delete(@NonNull UUID solutionId) { + final Solution algorithm = findById(solutionId); + + if (algorithm.getFile() != null) { + fileService.delete(algorithm.getFile().getId()); + } + + solutionRepository.deleteById(solutionId); + } + + @Override + public File addFileToSolution(UUID solutionId, MultipartFile multipartFile) { + final Solution solution = + ServiceUtils.findById(solutionId, Solution.class, solutionRepository); + final File file = fileService.create(multipartFile); + solution.setFile(file); + solutionRepository.save(solution); + + return file; + } +} diff --git a/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/Constants.java b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/Constants.java index 3bbd530a..498325d4 100644 --- a/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/Constants.java +++ b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/Constants.java @@ -77,6 +77,8 @@ public final class Constants { public static final String REVISIONS = "revisions"; + public static final String SOLUTIONS = "solutions"; + // default Pagination params that are exposed in HATEOAS links public static final Integer DEFAULT_PAGE_NUMBER = 0; @@ -126,6 +128,8 @@ public final class Constants { public static final String TAG_TOSCA = "tosca-application-controller"; + public static final String TAG_SOLUTION = "solution"; + private Constants() { } } diff --git a/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/controller/SolutionController.java b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/controller/SolutionController.java new file mode 100644 index 00000000..f72a7407 --- /dev/null +++ b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/controller/SolutionController.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * Copyright (c) 2020-2021 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.web.controller; + +import java.util.UUID; + +import org.planqk.atlas.core.model.File; +import org.planqk.atlas.core.model.Solution; +import org.planqk.atlas.core.services.FileService; +import org.planqk.atlas.core.services.SolutionService; +import org.planqk.atlas.web.Constants; +import org.planqk.atlas.web.dtos.FileDto; +import org.planqk.atlas.web.dtos.SolutionDto; +import org.planqk.atlas.web.utils.ListParameters; +import org.planqk.atlas.web.utils.ListParametersDoc; +import org.planqk.atlas.web.utils.ModelMapperUtils; +import org.planqk.atlas.web.utils.ValidationGroups; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Controller to access and manipulate implementations of quantum algorithms. + */ +@io.swagger.v3.oas.annotations.tags.Tag(name = Constants.TAG_SOLUTION) +@RestController +@CrossOrigin(allowedHeaders = "*", origins = "*") +@RequestMapping("/" + Constants.SOLUTIONS) +@AllArgsConstructor +@Slf4j +public class SolutionController { + + private final SolutionService solutionService; + + private final FileService fileService; + + @Operation(responses = { + @ApiResponse(responseCode = "200") + }, description = "Retrieve all solutions.") + @ListParametersDoc + @GetMapping + public ResponseEntity> getSolutions( + @Parameter(hidden = true) ListParameters listParameters) { + return ResponseEntity.ok(ModelMapperUtils.convertPage(solutionService.findAll(listParameters.getPageable()), SolutionDto.class)); + } + + @Operation(responses = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "400", description = "Bad Request. Invalid request body."), + }, description = "Define the basic properties of a solution. ") + @PostMapping + public ResponseEntity createSolution( + @Validated(ValidationGroups.Create.class) @RequestBody SolutionDto solutionDto) { + final Solution savedSolution = solutionService.create(ModelMapperUtils.convert(solutionDto, Solution.class)); + + return new ResponseEntity<>(ModelMapperUtils.convert(savedSolution, SolutionDto.class), HttpStatus.CREATED); + } + + @Operation(responses = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", + description = "Bad Request. Invalid request body."), + @ApiResponse(responseCode = "404", + description = "Not Found. Solution with the given ID doesn't exist.") + }, description = "Update the basic properties of a solution. ") + @PutMapping("/{solutionId}") + public ResponseEntity updateSolution( + @PathVariable UUID solutionId, + @Validated(ValidationGroups.Update.class) @RequestBody SolutionDto solutionDto) { + solutionDto.setId(solutionId); + final Solution updatedSolution = solutionService.update(ModelMapperUtils.convert(solutionDto, Solution.class)); + + return ResponseEntity.ok(ModelMapperUtils.convert(updatedSolution, SolutionDto.class)); + } + + @Operation(responses = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "400", + description = "Bad Request. Invalid request body."), + @ApiResponse(responseCode = "404", + description = "Not Found. Solution with the given ID doesn't exist.") + }, description = "Delete a solution. ") + @DeleteMapping("/{solutionId}") + public ResponseEntity deleteSolution( + @PathVariable UUID solutionId) { + solutionService.delete(solutionId); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Operation(responses = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", + description = "Bad Request. Invalid request body."), + @ApiResponse(responseCode = "404", + description = "Not Found. Solution with the given ID doesn't exist.") + }, description = "Retrieve a specific solution.") + @GetMapping("/{solutionId}") + public ResponseEntity getSolution( + @PathVariable UUID solutionId) { + final var solution = solutionService.findById(solutionId); + + return ResponseEntity.ok(ModelMapperUtils.convert(solution, SolutionDto.class)); + } + + @Operation(responses = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "400", description = "Bad Request. Invalid request body."), + }, description = "Uploads and adds a file to a given solution") + @PostMapping(value = "/{solutionId}/" + Constants.FILE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity createFileForSolution( + @PathVariable UUID solutionId, + @RequestParam("file") MultipartFile multipartFile) { + final File file = solutionService.addFileToSolution(solutionId, multipartFile); + + return ResponseEntity.status(HttpStatus.CREATED).body(ModelMapperUtils.convert(file, FileDto.class)); + } + + @Operation(responses = { + @ApiResponse(responseCode = "200"), + }, description = "Retrieve the file of a solution") + @GetMapping("/{solutionId}/" + Constants.FILE) + public ResponseEntity getFileOfSolution( + @PathVariable UUID solutionId + ) { + final var solution = solutionService.findById(solutionId); + + return ResponseEntity.ok(ModelMapperUtils.convert(solution.getFile(), FileDto.class)); + } + + @Operation(responses = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", + description = "File of Solution with given ID doesn't exist") + }, description = "Downloads a specific file content of a Solution") + @GetMapping("/{solutionId}/" + Constants.FILE + "/content") + public ResponseEntity downloadFileContent( + @PathVariable UUID solutionId + ) { + final File file = solutionService.findById(solutionId).getFile(); + + if (file == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + return ResponseEntity + .ok() + .contentType(MediaType.parseMediaType(file.getMimeType())) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName()) + .body(fileService.getFileContent(file.getId())); + } + + @Operation(responses = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "400"), + @ApiResponse(responseCode = "404", description = "Not Found. Solution or File with given IDs doesn't exist") + }, description = "Delete a file of a solution.") + @DeleteMapping("/{solutionId}/" + Constants.FILE) + public ResponseEntity deleteFileOfSolution( + @PathVariable UUID solutionId) { + final File file = solutionService.findById(solutionId).getFile(); + + if (file == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + final var solution = solutionService.findById(solutionId); + solution.setFile(null); + fileService.delete(file.getId()); + + solutionService.update(solution); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/dtos/SolutionDto.java b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/dtos/SolutionDto.java new file mode 100644 index 00000000..ce6bda65 --- /dev/null +++ b/org.planqk.atlas.web/src/main/java/org/planqk/atlas/web/dtos/SolutionDto.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2020 the qc-atlas contributors. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ + +package org.planqk.atlas.web.dtos; + +import java.util.UUID; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; + +import org.planqk.atlas.web.utils.Identifyable; +import org.planqk.atlas.web.utils.ValidationGroups; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class SolutionDto implements Identifyable { + + @NotNull(groups = {ValidationGroups.IDOnly.class}, message = "An id is required to perform an update") + @Null(groups = {ValidationGroups.Create.class}, message = "The id must be null for creating a solution") + private UUID id; + + @NotNull(groups = {ValidationGroups.Create.class}, message = "A patternId is required for a solution") + private UUID patternId; + + @NotNull(groups = {ValidationGroups.Create.class}, message = "A solution type is required for a solution") + private String solutionType; +}