diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1a3af92 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/fling-java-codestyle.xml b/fling-java-codestyle.xml new file mode 100644 index 0000000..a20c095 --- /dev/null +++ b/fling-java-codestyle.xml @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/service/fling/src/main/java/net/friedl/fling/FlingApplication.java b/service/fling/src/main/java/net/friedl/fling/FlingApplication.java index ae741be..983ad54 100644 --- a/service/fling/src/main/java/net/friedl/fling/FlingApplication.java +++ b/service/fling/src/main/java/net/friedl/fling/FlingApplication.java @@ -6,8 +6,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FlingApplication { - public static void main(String[] args) { - SpringApplication.run(FlingApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(FlingApplication.class, args); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java index 334a26c..6f24df1 100644 --- a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java @@ -2,14 +2,11 @@ package net.friedl.fling; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; diff --git a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java index a561605..6cf61e1 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java @@ -1,9 +1,7 @@ package net.friedl.fling.controller; import java.util.List; - import javax.servlet.http.HttpServletRequest; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -19,7 +17,6 @@ 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 net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.persistence.archive.ArchiveException; import net.friedl.fling.service.ArtifactService; @@ -28,55 +25,58 @@ import net.friedl.fling.service.ArtifactService; @RequestMapping("/api") public class ArtifactController { - private ArtifactService artifactService; + private ArtifactService artifactService; - @Autowired - public ArtifactController(ArtifactService artifactService) { - this.artifactService = artifactService; - } + @Autowired + public ArtifactController(ArtifactService artifactService) { + this.artifactService = artifactService; + } - @GetMapping(path = "/artifacts", params = "flingId") - public List getArtifacts(@RequestParam Long flingId) { - return artifactService.findAllArtifacts(flingId); - } + @GetMapping(path = "/artifacts", params = "flingId") + public List getArtifacts(@RequestParam Long flingId) { + return artifactService.findAllArtifacts(flingId); + } - @GetMapping(path = "/artifacts", params = "artifactId") - public ResponseEntity getArtifact(@RequestParam Long artifactId) { - return ResponseEntity.of(artifactService.findArtifact(artifactId)); - } + @GetMapping(path = "/artifacts", params = "artifactId") + public ResponseEntity getArtifact(@RequestParam Long artifactId) { + return ResponseEntity.of(artifactService.findArtifact(artifactId)); + } - @PostMapping("/artifacts/{flingId}") - public ArtifactDto postArtifact(@PathVariable Long flingId, HttpServletRequest request) throws Exception { - return artifactService.storeArtifact(flingId, request.getInputStream()); - } + @PostMapping("/artifacts/{flingId}") + public ArtifactDto postArtifact(@PathVariable Long flingId, HttpServletRequest request) + throws Exception { + return artifactService.storeArtifact(flingId, request.getInputStream()); + } - @PatchMapping(path = "/artifacts/{artifactId}", consumes = MediaType.APPLICATION_JSON_VALUE) - public ArtifactDto patchArtifact(@PathVariable Long artifactId, @RequestBody String body) { - return artifactService.mergeArtifact(artifactId, body); - } + @PatchMapping(path = "/artifacts/{artifactId}", consumes = MediaType.APPLICATION_JSON_VALUE) + public ArtifactDto patchArtifact(@PathVariable Long artifactId, @RequestBody String body) { + return artifactService.mergeArtifact(artifactId, body); + } - @DeleteMapping(path = "/artifacts/{artifactId}") - public void deleteArtifact(@PathVariable Long artifactId) throws ArchiveException { - artifactService.deleteArtifact(artifactId); - } + @DeleteMapping(path = "/artifacts/{artifactId}") + public void deleteArtifact(@PathVariable Long artifactId) throws ArchiveException { + artifactService.deleteArtifact(artifactId); + } - @GetMapping(path = "/artifacts/{artifactId}/downloadid") - public String getDownloadId(@PathVariable Long artifactId) { - return artifactService.generateDownloadId(artifactId); - } + @GetMapping(path = "/artifacts/{artifactId}/downloadid") + public String getDownloadId(@PathVariable Long artifactId) { + return artifactService.generateDownloadId(artifactId); + } - @GetMapping(path = "/artifacts/{artifactId}/{downloadId}/download") - public ResponseEntity downloadArtifact(@PathVariable Long artifactId, @PathVariable String downloadId) - throws ArchiveException { + @GetMapping(path = "/artifacts/{artifactId}/{downloadId}/download") + public ResponseEntity downloadArtifact(@PathVariable Long artifactId, + @PathVariable String downloadId) + throws ArchiveException { - var artifact = artifactService.findArtifact(artifactId).orElseThrow(); - var stream = new InputStreamResource(artifactService.downloadArtifact(downloadId)); + var artifact = artifactService.findArtifact(artifactId).orElseThrow(); + var stream = new InputStreamResource(artifactService.downloadArtifact(downloadId)); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + artifact.getName() + "\"") - .contentLength(artifact.getSize()) - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(stream); - } + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment;filename=\"" + artifact.getName() + "\"") + .contentLength(artifact.getSize()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(stream); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index cf5374f..de84211 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -2,7 +2,6 @@ package net.friedl.fling.controller; import java.io.IOException; import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -18,7 +17,6 @@ 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 net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.persistence.archive.ArchiveException; import net.friedl.fling.service.FlingService; @@ -27,64 +25,66 @@ import net.friedl.fling.service.FlingService; @RequestMapping("/api") public class FlingController { - private FlingService flingService; + private FlingService flingService; - @Autowired - public FlingController(FlingService flingService) { - this.flingService = flingService; - } + @Autowired + public FlingController(FlingService flingService) { + this.flingService = flingService; + } - @GetMapping("/fling") - public List getFlings() { - return flingService.findAll(); - } + @GetMapping("/fling") + public List getFlings() { + return flingService.findAll(); + } - @PostMapping("/fling") - public Long postFling(@RequestBody FlingDto flingDto) { - return flingService.createFling(flingDto); - } + @PostMapping("/fling") + public Long postFling(@RequestBody FlingDto flingDto) { + return flingService.createFling(flingDto); + } - @PutMapping("/fling/{flingId}") - public void putFling(@PathVariable Long flingId, @RequestBody FlingDto flingDto) { - flingService.mergeFling(flingId, flingDto); - } + @PutMapping("/fling/{flingId}") + public void putFling(@PathVariable Long flingId, @RequestBody FlingDto flingDto) { + flingService.mergeFling(flingId, flingDto); + } - @GetMapping(path = "/fling", params = "flingId") - public ResponseEntity getFling(@RequestParam Long flingId) { - return ResponseEntity.of(flingService.findFlingById(flingId)); - } + @GetMapping(path = "/fling", params = "flingId") + public ResponseEntity getFling(@RequestParam Long flingId) { + return ResponseEntity.of(flingService.findFlingById(flingId)); + } - @GetMapping(path = "/fling", params = "shareId") - public ResponseEntity getFlingByShareId(@RequestParam String shareId) { - return ResponseEntity.of(flingService.findFlingByShareId(shareId)); - } + @GetMapping(path = "/fling", params = "shareId") + public ResponseEntity getFlingByShareId(@RequestParam String shareId) { + return ResponseEntity.of(flingService.findFlingByShareId(shareId)); + } - @GetMapping(path = "/fling/shareExists/{shareId}") - public Boolean getShareExists(@PathVariable String shareId) { - return flingService.existsShareUrl(shareId); - } + @GetMapping(path = "/fling/shareExists/{shareId}") + public Boolean getShareExists(@PathVariable String shareId) { + return flingService.existsShareUrl(shareId); + } - @DeleteMapping("/fling/{flingId}") - public void deleteFling(@PathVariable Long flingId) { - flingService.deleteFlingById(flingId); - } + @DeleteMapping("/fling/{flingId}") + public void deleteFling(@PathVariable Long flingId) { + flingService.deleteFlingById(flingId); + } - @GetMapping(path = "/fling/{flingId}/package") - public String packageFling(@PathVariable Long flingId) throws IOException, ArchiveException { - return flingService.packageFling(flingId); - } + @GetMapping(path = "/fling/{flingId}/package") + public String packageFling(@PathVariable Long flingId) throws IOException, ArchiveException { + return flingService.packageFling(flingId); + } - @GetMapping(path = "/fling/{flingId}/download/{downloadId}") - public ResponseEntity downloadFling(@PathVariable Long flingId, @PathVariable String downloadId) throws ArchiveException, IOException { - var fling = flingService.findFlingById(flingId).orElseThrow(); - var flingPackage = flingService.downloadFling(downloadId); - var stream = new InputStreamResource(flingPackage.getFirst()); + @GetMapping(path = "/fling/{flingId}/download/{downloadId}") + public ResponseEntity downloadFling(@PathVariable Long flingId, + @PathVariable String downloadId) throws ArchiveException, IOException { + var fling = flingService.findFlingById(flingId).orElseThrow(); + var flingPackage = flingService.downloadFling(downloadId); + var stream = new InputStreamResource(flingPackage.getFirst()); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fling.getName() + ".zip" + "\"") - .contentLength(flingPackage.getSecond()) - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(stream); - } + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment;filename=\"" + fling.getName() + ".zip" + "\"") + .contentLength(flingPackage.getSecond()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(stream); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java index e0bd057..0bd96f6 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java @@ -1,26 +1,23 @@ package net.friedl.fling.model.dto; import java.time.Instant; - -import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.Data; @Data public class ArtifactDto { - private String name; + private String name; - private Long id; + private Long id; - private String path; + private String path; - private String doi; + private String doi; - private Long size; + private Long size; - private Integer version; + private Integer version; - private Instant uploadTime; + private Instant uploadTime; - private FlingDto fling; + private FlingDto fling; } diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java index 9b52077..46df0bb 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java @@ -3,94 +3,94 @@ package net.friedl.fling.model.dto; import java.time.Instant; import java.util.HashMap; import java.util.Map; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.Data; @Data public class FlingDto { - private String name; + private String name; - private Long id; + private Long id; - private Instant creationTime; + private Instant creationTime; - @JsonIgnore - private Boolean directDownload; + @JsonIgnore + private Boolean directDownload; - @JsonIgnore - private Boolean allowUpload; + @JsonIgnore + private Boolean allowUpload; - @JsonIgnore - private Boolean shared; + @JsonIgnore + private Boolean shared; - @JsonIgnore - private String shareUrl; + @JsonIgnore + private String shareUrl; - @JsonIgnore - private Integer expirationClicks; + @JsonIgnore + private Integer expirationClicks; - @JsonIgnore - private Instant expirationTime; + @JsonIgnore + private Instant expirationTime; - private String authCode; + private String authCode; - @JsonProperty("sharing") - private void unpackSharing(Map sharing) { - this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false); - this.allowUpload = (Boolean) sharing.getOrDefault("allowUpload", false); - this.shared = (Boolean) sharing.getOrDefault("shared", true); - this.shareUrl = (String) sharing.getOrDefault("shareUrl", null); + @JsonProperty("sharing") + private void unpackSharing(Map sharing) { + this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false); + this.allowUpload = (Boolean) sharing.getOrDefault("allowUpload", false); + this.shared = (Boolean) sharing.getOrDefault("shared", true); + this.shareUrl = (String) sharing.getOrDefault("shareUrl", null); + } + + @JsonProperty("sharing") + private Map packSharing() { + Map sharing = new HashMap<>(); + sharing.put("directDownload", this.directDownload); + sharing.put("allowUpload", this.allowUpload); + sharing.put("shared", this.shared); + sharing.put("shareUrl", this.shareUrl); + + return sharing; + } + + @JsonProperty("expiration") + private void unpackExpiration(Map expiration) { + String type = (String) expiration.getOrDefault("type", null); + if (type == null) + return; + + switch (type) { + case "time": + this.expirationClicks = null; + // json can only handle int, long must be given as string + // TODO: this back and forth conversion is a bit hack-ish + this.expirationTime = + Instant.ofEpochMilli(Long.valueOf(expiration.get("value").toString())); + break; + case "clicks": + this.expirationTime = null; + this.expirationClicks = Integer.valueOf(expiration.get("value").toString()); + break; + default: + throw new IllegalArgumentException("Unexpected value '" + type + "'"); + } + } + + @JsonProperty("expiration") + private Map packExpiration() { + Map expiration = new HashMap<>(); + + if (this.expirationClicks != null) { + expiration.put("type", "clicks"); + expiration.put("value", this.expirationClicks); } - @JsonProperty("sharing") - private Map packSharing() { - Map sharing = new HashMap<>(); - sharing.put("directDownload", this.directDownload); - sharing.put("allowUpload", this.allowUpload); - sharing.put("shared", this.shared); - sharing.put("shareUrl", this.shareUrl); - - return sharing; + if (this.expirationTime != null) { + expiration.put("type", "time"); + expiration.put("value", this.expirationTime.toEpochMilli()); } - @JsonProperty("expiration") - private void unpackExpiration(Map expiration) { - String type = (String) expiration.getOrDefault("type", null); - if(type == null) return; - - switch(type) { - case "time": - this.expirationClicks = null; - // json can only handle int, long must be given as string - // TODO: this back and forth conversion is a bit hack-ish - this.expirationTime = Instant.ofEpochMilli(Long.valueOf(expiration.get("value").toString())); - break; - case "clicks": - this.expirationTime = null; - this.expirationClicks = Integer.valueOf(expiration.get("value").toString()); - break; - default: - throw new IllegalArgumentException("Unexpected value '"+type+"'"); - } - } - - @JsonProperty("expiration") - private Map packExpiration() { - Map expiration = new HashMap<>(); - - if(this.expirationClicks != null) { - expiration.put("type", "clicks"); - expiration.put("value", this.expirationClicks); - } - - if(this.expirationTime != null) { - expiration.put("type", "time"); - expiration.put("value", this.expirationTime.toEpochMilli()); - } - - return expiration; - } + return expiration; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingSharingDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingSharingDto.java index ab65d22..fee9bb5 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingSharingDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingSharingDto.java @@ -4,9 +4,9 @@ import lombok.Data; @Data public class FlingSharingDto { - private Boolean allowUpload; + private Boolean allowUpload; - private Boolean directDownload; + private Boolean directDownload; - private String shareUrl; + private String shareUrl; } diff --git a/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java b/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java index 0295cf5..2a897eb 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java +++ b/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java @@ -4,9 +4,7 @@ import java.lang.reflect.Field; import java.util.List; import java.util.Map; import java.util.Optional; - import org.mapstruct.Mapper; - import lombok.extern.slf4j.Slf4j; import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.persistence.entities.ArtifactEntity; @@ -14,38 +12,38 @@ import net.friedl.fling.persistence.entities.ArtifactEntity; @Slf4j @Mapper(componentModel = "spring") public abstract class ArtifactMapper { - public abstract ArtifactDto map(ArtifactEntity artifactEntity); + public abstract ArtifactDto map(ArtifactEntity artifactEntity); - public abstract ArtifactEntity map(ArtifactDto artifactDto); + public abstract ArtifactEntity map(ArtifactDto artifactDto); - public abstract List map(List artifactEntities); + public abstract List map(List artifactEntities); - public Optional map(Optional artifactEntity) { - return artifactEntity.map(a -> map(a)); - } + public Optional map(Optional artifactEntity) { + return artifactEntity.map(a -> map(a)); + } - public ArtifactDto merge(ArtifactDto originalArtifactDto, Map patch) { - ArtifactDto mergedArtifactDto = new ArtifactDto(); + public ArtifactDto merge(ArtifactDto originalArtifactDto, Map patch) { + ArtifactDto mergedArtifactDto = new ArtifactDto(); - for (Field field : ArtifactDto.class.getDeclaredFields()) { - String fieldName = field.getName(); - field.setAccessible(true); - try { - if (patch.containsKey(fieldName)) { - if(field.getType().equals(Long.class)) { - field.set(mergedArtifactDto, ((Number) patch.get(fieldName)).longValue()); - } - field.set(mergedArtifactDto, patch.get(fieldName)); - } else { - field.set(mergedArtifactDto, field.get(originalArtifactDto)); - } - } - catch (IllegalArgumentException | IllegalAccessException e) { - log.error("Could not merge {} [value={}] with {}", fieldName, patch.get(fieldName), originalArtifactDto, - e); - } + for (Field field : ArtifactDto.class.getDeclaredFields()) { + String fieldName = field.getName(); + field.setAccessible(true); + try { + if (patch.containsKey(fieldName)) { + if (field.getType().equals(Long.class)) { + field.set(mergedArtifactDto, ((Number) patch.get(fieldName)).longValue()); + } + field.set(mergedArtifactDto, patch.get(fieldName)); + } else { + field.set(mergedArtifactDto, field.get(originalArtifactDto)); } - - return mergedArtifactDto; + } catch (IllegalArgumentException | IllegalAccessException e) { + log.error("Could not merge {} [value={}] with {}", fieldName, patch.get(fieldName), + originalArtifactDto, + e); + } } + + return mergedArtifactDto; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java b/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java index b1c208b..e7541b1 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java +++ b/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java @@ -2,21 +2,19 @@ package net.friedl.fling.model.mapper; import java.util.List; import java.util.Optional; - import org.mapstruct.Mapper; - import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.persistence.entities.FlingEntity; @Mapper(componentModel = "spring") public interface FlingMapper { - FlingDto map(FlingEntity flingEntity); + FlingDto map(FlingEntity flingEntity); - default Optional map(Optional flingEntity) { - return flingEntity.map(f -> map(f)); - } + default Optional map(Optional flingEntity) { + return flingEntity.map(f -> map(f)); + } - FlingEntity map(FlingDto flingDto); + FlingEntity map(FlingDto flingDto); - List map(List flingEntities); + List map(List flingEntities); } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/archive/Archive.java b/service/fling/src/main/java/net/friedl/fling/persistence/archive/Archive.java index fad55e1..e4790f8 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/archive/Archive.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/archive/Archive.java @@ -6,37 +6,35 @@ import java.io.IOException; import java.io.InputStream; public interface Archive { - /** - * Retrieve an artifact from the archive - * - * @param id The unique artifact id as returned by {@link Archive#store} - * @return An {@link InputStream} for reading the artifact - */ - InputStream get(String id) throws ArchiveException; + /** + * Retrieve an artifact from the archive + * + * @param id The unique artifact id as returned by {@link Archive#store} + * @return An {@link InputStream} for reading the artifact + */ + InputStream get(String id) throws ArchiveException; - /** - * Store an artifact - * - * @param is The artifact represented as {@link InputStream} - * @return A unique archive id for the artifact - * @throws IOException If anything goes wrong while storing the artifact in - * the archive - */ - String store(InputStream is) throws ArchiveException; + /** + * Store an artifact + * + * @param is The artifact represented as {@link InputStream} + * @return A unique archive id for the artifact + * @throws IOException If anything goes wrong while storing the artifact in the archive + */ + String store(InputStream is) throws ArchiveException; - default String store(File file) throws ArchiveException { - try { - return store(new FileInputStream(file)); - } - catch (IOException ex) { - throw new ArchiveException(ex); - } + default String store(File file) throws ArchiveException { + try { + return store(new FileInputStream(file)); + } catch (IOException ex) { + throw new ArchiveException(ex); } + } - /** - * Delete an artifact - * - * @param id The unique artifact id as returned by {@link Archive#store} - */ - void remove(String id) throws ArchiveException; + /** + * Delete an artifact + * + * @param id The unique artifact id as returned by {@link Archive#store} + */ + void remove(String id) throws ArchiveException; } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/archive/ArchiveException.java b/service/fling/src/main/java/net/friedl/fling/persistence/archive/ArchiveException.java index 7da9626..46db704 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/archive/ArchiveException.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/archive/ArchiveException.java @@ -1,78 +1,73 @@ package net.friedl.fling.persistence.archive; public class ArchiveException extends Exception { - private static final long serialVersionUID = 6216735865308056261L; + private static final long serialVersionUID = 6216735865308056261L; - /** - * Constructs a new exception with {@code null} as its detail message. The - * cause is not initialized, and may subsequently be initialized by a call - * to {@link #initCause}. - */ - public ArchiveException() { - super(); - } + /** + * Constructs a new exception with {@code null} as its detail message. The cause is not + * initialized, and may subsequently be initialized by a call to {@link #initCause}. + */ + public ArchiveException() { + super(); + } - /** - * Constructs a new exception with the specified detail message. The cause - * is not initialized, and may subsequently be initialized by a call to - * {@link #initCause}. - * - * @param message the detail message. The detail message is saved for later - * retrieval by the {@link #getMessage()} method. - */ - public ArchiveException(String message) { - super(message); - } + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and + * may subsequently be initialized by a call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for later retrieval by the + * {@link #getMessage()} method. + */ + public ArchiveException(String message) { + super(message); + } - /** - * Constructs a new exception with the specified detail message and cause. - *

- * Note that the detail message associated with {@code cause} is not - * automatically incorporated in this exception's detail message. - * - * @param message the detail message (which is saved for later retrieval by - * the {@link #getMessage()} method). - * @param cause the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A {@code null} value is permitted, - * and indicates that the cause is nonexistent or unknown.) - * @since 1.4 - */ - public ArchiveException(String message, Throwable cause) { - super(message, cause); - } + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with {@code cause} is not automatically + * incorporated in this exception's detail message. + * + * @param message the detail message (which is saved for later retrieval by the + * {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A {@code null} value is permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public ArchiveException(String message, Throwable cause) { + super(message, cause); + } - /** - * Constructs a new exception with the specified cause and a detail message - * of {@code (cause==null ? null : cause.toString())} (which typically - * contains the class and detail message of {@code cause}). This constructor - * is useful for exceptions that are little more than wrappers for other - * throwables (for example, - * {@link java.security.PrivilegedActionArchiveException}). - * - * @param cause the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A {@code null} value is permitted, - * and indicates that the cause is nonexistent or unknown.) - * @since 1.4 - */ - public ArchiveException(Throwable cause) { - super(cause); - } + /** + * Constructs a new exception with the specified cause and a detail message of + * {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail + * message of {@code cause}). This constructor is useful for exceptions that are little more than + * wrappers for other throwables (for example, + * {@link java.security.PrivilegedActionArchiveException}). + * + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * (A {@code null} value is permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public ArchiveException(Throwable cause) { + super(cause); + } - /** - * Constructs a new exception with the specified detail message, cause, - * suppression enabled or disabled, and writable stack trace enabled or - * disabled. - * - * @param message the detail message. - * @param cause the cause. (A {@code null} value is permitted, and indicates - * that the cause is nonexistent or unknown.) - * @param enableSuppression whether or not suppression is enabled or - * disabled - * @param writableStackTrace whether or not the stack trace should be - * writable - * @since 1.7 - */ - protected ArchiveException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } + /** + * Constructs a new exception with the specified detail message, cause, suppression enabled or + * disabled, and writable stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is + * nonexistent or unknown.) + * @param enableSuppression whether or not suppression is enabled or disabled + * @param writableStackTrace whether or not the stack trace should be writable + * @since 1.7 + */ + protected ArchiveException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchive.java b/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchive.java index 9ee3599..9630f09 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchive.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchive.java @@ -10,71 +10,71 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - import net.friedl.fling.persistence.archive.Archive; import net.friedl.fling.persistence.archive.ArchiveException; @Component("fileSystemArchive") public class FileSystemArchive implements Archive { - private MessageDigest fileStoreDigest; + private MessageDigest fileStoreDigest; - private FileSystemArchiveConfiguration configuration; + private FileSystemArchiveConfiguration configuration; - @Autowired - public FileSystemArchive(MessageDigest fileStoreDigest, FileSystemArchiveConfiguration configuration) { - this.fileStoreDigest = fileStoreDigest; - this.configuration = configuration; + @Autowired + public FileSystemArchive(MessageDigest fileStoreDigest, + FileSystemArchiveConfiguration configuration) { + this.fileStoreDigest = fileStoreDigest; + this.configuration = configuration; + } + + @Override + public InputStream get(String id) throws ArchiveException { + try { + var path = Paths.get(configuration.getDirectory(), id); + FileInputStream fis = new FileInputStream(path.toFile()); + return fis; + } catch (FileNotFoundException ex) { + throw new ArchiveException(ex); } + } - @Override - public InputStream get(String id) throws ArchiveException { - try { - var path = Paths.get(configuration.getDirectory(), id); - FileInputStream fis = new FileInputStream(path.toFile()); - return fis; - } catch (FileNotFoundException ex) { - throw new ArchiveException(ex); - } + @Override + public String store(InputStream is) throws ArchiveException { + try { + byte[] fileBytes = is.readAllBytes(); + is.close(); + + String fileStoreId = hexEncode(fileStoreDigest.digest(fileBytes)); + + FileChannel fc = FileChannel.open(Paths.get(configuration.getDirectory(), fileStoreId), + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, + StandardOpenOption.CREATE); + + fc.write(ByteBuffer.wrap(fileBytes)); + + fc.close(); + return fileStoreId; + + } catch (IOException ex) { + throw new ArchiveException(ex); } + } - @Override - public String store(InputStream is) throws ArchiveException { - try { - byte[] fileBytes = is.readAllBytes(); - is.close(); - - String fileStoreId = hexEncode(fileStoreDigest.digest(fileBytes)); - - FileChannel fc = FileChannel.open(Paths.get(configuration.getDirectory(), fileStoreId), - StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE); - - fc.write(ByteBuffer.wrap(fileBytes)); - - fc.close(); - return fileStoreId; - - } catch (IOException ex) { - throw new ArchiveException(ex); - } + @Override + public void remove(String id) throws ArchiveException { + var path = Paths.get(configuration.getDirectory(), id); + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new ArchiveException("Could not delete file at " + path.toString(), e); } + } - @Override - public void remove(String id) throws ArchiveException { - var path = Paths.get(configuration.getDirectory(), id); - try { - Files.deleteIfExists(path); - } catch (IOException e) { - throw new ArchiveException("Could not delete file at " + path.toString(), e); - } - } - - private String hexEncode(byte[] fileStoreId) { - StringBuilder sb = new StringBuilder(fileStoreId.length * 2); - for (byte b : fileStoreId) - sb.append(String.format("%02x", b)); - return sb.toString(); - } + private String hexEncode(byte[] fileStoreId) { + StringBuilder sb = new StringBuilder(fileStoreId.length * 2); + for (byte b : fileStoreId) + sb.append(String.format("%02x", b)); + return sb.toString(); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchiveConfiguration.java b/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchiveConfiguration.java index 96f82a2..7140857 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchiveConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/archive/impl/FileSystemArchiveConfiguration.java @@ -5,14 +5,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; - import javax.annotation.PostConstruct; - import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -21,23 +18,24 @@ import lombok.extern.slf4j.Slf4j; @Configuration @ConfigurationProperties("fling.archive.fileystem") @ConditionalOnBean(FileSystemArchive.class) -@Getter @Setter +@Getter +@Setter public class FileSystemArchiveConfiguration { - private String directory; + private String directory; - @Bean - public MessageDigest fileStoreDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance("SHA-512"); + @Bean + public MessageDigest fileStoreDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-512"); + } + + @PostConstruct + public void init() throws IOException { + if (directory == null) { + log.info("Directory not configured take temp path"); + Path tmpPath = Files.createTempDirectory("fling"); + this.directory = tmpPath.toAbsolutePath().toString(); } - @PostConstruct - public void init() throws IOException { - if (directory == null) { - log.info("Directory not configured take temp path"); - Path tmpPath = Files.createTempDirectory("fling"); - this.directory = tmpPath.toAbsolutePath().toString(); - } - - log.info("File store directory: {}", directory); - } + log.info("File store directory: {}", directory); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java b/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java index 3295c84..224edd2 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java @@ -1,7 +1,6 @@ package net.friedl.fling.persistence.entities; import java.time.Instant; - import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; @@ -9,38 +8,39 @@ import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.PrePersist; import javax.persistence.Table; - import lombok.Getter; import lombok.Setter; @Entity @Table(name = "Artifact") -@Getter @Setter +@Getter +@Setter public class ArtifactEntity { - @Id - @GeneratedValue - private Long id; + @Id + @GeneratedValue + private Long id; - private String name; + private String name; - private Integer version; + private Integer version; - private String path; + private String path; - @Column(unique = true) - private String doi; + @Column(unique = true) + private String doi; - private Instant uploadTime; + private Instant uploadTime; - private Long size; + private Long size; - @ManyToOne(optional = false) - private FlingEntity fling; + @ManyToOne(optional = false) + private FlingEntity fling; - @PrePersist - private void prePersist() { - this.uploadTime = Instant.now(); + @PrePersist + private void prePersist() { + this.uploadTime = Instant.now(); - if(this.version == null) this.version = -1; - } + if (this.version == null) + this.version = -1; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java b/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java index 123eac2..a8dfdf3 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java @@ -2,7 +2,6 @@ package net.friedl.fling.persistence.entities; import java.time.Instant; import java.util.Set; - import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; @@ -12,7 +11,6 @@ import javax.persistence.OneToMany; import javax.persistence.PostPersist; import javax.persistence.PrePersist; import javax.persistence.Table; - import lombok.Getter; import lombok.Setter; @@ -21,52 +19,52 @@ import lombok.Setter; @Getter @Setter public class FlingEntity { - @Id - @GeneratedValue - private Long id; + @Id + @GeneratedValue + private Long id; - private String name; + private String name; - private Instant creationTime; + private Instant creationTime; - private Instant expirationTime; + private Instant expirationTime; - private Integer expirationClicks; + private Integer expirationClicks; - @Column(nullable = false) - private Boolean directDownload; + @Column(nullable = false) + private Boolean directDownload; - @Column(nullable = false) - private Boolean allowUpload; + @Column(nullable = false) + private Boolean allowUpload; - @Column(nullable = false) - private Boolean shared; + @Column(nullable = false) + private Boolean shared; - @Column(unique = true, nullable = false) - private String shareUrl; + @Column(unique = true, nullable = false) + private String shareUrl; - private String authCode; + private String authCode; - @OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true) - private Set artifacts; + @OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true) + private Set artifacts; - @PrePersist - private void prePersist() { - if (this.directDownload == null) - this.directDownload = false; - if (this.allowUpload == null) - this.allowUpload = false; - if (this.shared == null) - this.shared = true; + @PrePersist + private void prePersist() { + if (this.directDownload == null) + this.directDownload = false; + if (this.allowUpload == null) + this.allowUpload = false; + if (this.shared == null) + this.shared = true; - this.creationTime = Instant.now(); - } + this.creationTime = Instant.now(); + } - @PostPersist - private void postPersist() { - System.out.println("ID: "+this.id); - System.out.println("Share Url: "+this.shareUrl); + @PostPersist + private void postPersist() { + System.out.println("ID: " + this.id); + System.out.println("Share Url: " + this.shareUrl); - this.shareUrl = this.id+this.shareUrl; - } + this.shareUrl = this.id + this.shareUrl; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java index 526683d..684e15e 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java @@ -2,13 +2,13 @@ package net.friedl.fling.persistence.repositories; import java.util.List; import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; - import net.friedl.fling.persistence.entities.ArtifactEntity; public interface ArtifactRepository extends JpaRepository { - Optional findByDoi(String doi); - List deleteByDoi(String doi); - List findAllByFlingId(Long flingId); + Optional findByDoi(String doi); + + List deleteByDoi(String doi); + + List findAllByFlingId(Long flingId); } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/FlingRepository.java b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/FlingRepository.java index 420b422..9e5c5ca 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/FlingRepository.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/FlingRepository.java @@ -1,17 +1,15 @@ package net.friedl.fling.persistence.repositories; import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; - import net.friedl.fling.persistence.entities.FlingEntity; public interface FlingRepository extends JpaRepository { - Optional findByName(String name); + Optional findByName(String name); - Optional findByShareUrl(String shareUrl); + Optional findByShareUrl(String shareUrl); - @Query("SELECT COUNT(*) FROM ArtifactEntity a, FlingEntity f where a.fling=f.id and f.id=:flingId") - Long countArtifactsById(Long flingId); + @Query("SELECT COUNT(*) FROM ArtifactEntity a, FlingEntity f where a.fling=f.id and f.id=:flingId") + Long countArtifactsById(Long flingId); } diff --git a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java index 29b6412..12396b4 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java +++ b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java @@ -1,13 +1,10 @@ package net.friedl.fling.security; import java.util.NoSuchElementException; - import javax.servlet.http.HttpServletRequest; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.stereotype.Service; - import lombok.extern.slf4j.Slf4j; import net.friedl.fling.security.authentication.FlingToken; import net.friedl.fling.security.authentication.dto.UserAuthDto; @@ -17,70 +14,76 @@ import net.friedl.fling.service.FlingService; @Slf4j @Service public class AuthorizationService { - private FlingService flingService; - private ArtifactService artifactService; + private FlingService flingService; + private ArtifactService artifactService; - @Autowired - public AuthorizationService(FlingService flingService, ArtifactService artifactService) { - this.flingService = flingService; - this.artifactService = artifactService; + @Autowired + public AuthorizationService(FlingService flingService, ArtifactService artifactService) { + this.flingService = flingService; + this.artifactService = artifactService; + } + + public boolean allowUpload(Long flingId, AbstractAuthenticationToken token) { + if (!(token instanceof FlingToken)) + return false; + + FlingToken flingToken = (FlingToken) token; + if (flingToken.getGrantedFlingAuthority().getAuthority() + .equals(FlingAuthority.FLING_OWNER.name())) { + return true; } - public boolean allowUpload(Long flingId, AbstractAuthenticationToken token) { - if (!(token instanceof FlingToken)) return false; + var uploadAllowed = flingService.findFlingById(flingId).orElseThrow().getAllowUpload(); - FlingToken flingToken = (FlingToken) token; - if (flingToken.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) { - return true; - } + return uploadAllowed && flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); + } - var uploadAllowed = flingService.findFlingById(flingId).orElseThrow().getAllowUpload(); + public boolean allowPatchingArtifact(Long artifactId, FlingToken authentication) { + var flingId = artifactService.findArtifact(artifactId).orElseThrow().getFling().getId(); + return allowUpload(flingId, authentication); + } - return uploadAllowed && flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); + public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) { + return userAuth.getShareId().equals(shareUrl); + } + + public boolean allowFlingAccess(Long flingId, AbstractAuthenticationToken token) { + if (!(token instanceof FlingToken)) + return false; + + FlingToken flingToken = (FlingToken) token; + if (flingToken.getGrantedFlingAuthority().getAuthority() + .equals(FlingAuthority.FLING_OWNER.name())) { + return true; } - public boolean allowPatchingArtifact(Long artifactId, FlingToken authentication) { - var flingId = artifactService.findArtifact(artifactId).orElseThrow().getFling().getId(); - return allowUpload(flingId, authentication); + return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); + } + + public boolean allowFlingAccess(AbstractAuthenticationToken token, HttpServletRequest request) { + if (!(token instanceof FlingToken)) + return false; + + FlingToken flingToken = (FlingToken) token; + if (flingToken.getGrantedFlingAuthority().getAuthority() + .equals(FlingAuthority.FLING_OWNER.name())) { + return true; } - public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) { - return userAuth.getShareId().equals(shareUrl); + var shareId = request.getParameter("shareId"); + + Long flingId; + + try { + flingId = shareId != null + ? flingService.findFlingByShareId(shareId).orElseThrow().getId() + : Long.parseLong(request.getParameter("flingId")); + } catch (NumberFormatException | NoSuchElementException e) { + log.warn("Invalid shareId [shareId=\"{}\"] or flingId [flingId=\"{}\"] found", + request.getParameter("shareId"), request.getParameter("flingId")); + flingId = null; } - public boolean allowFlingAccess(Long flingId, AbstractAuthenticationToken token) { - if (!(token instanceof FlingToken)) return false; - - FlingToken flingToken = (FlingToken) token; - if (flingToken.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) { - return true; - } - - return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); - } - - public boolean allowFlingAccess(AbstractAuthenticationToken token, HttpServletRequest request) { - if (!(token instanceof FlingToken)) return false; - - FlingToken flingToken = (FlingToken) token; - if (flingToken.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) { - return true; - } - - var shareId = request.getParameter("shareId"); - - Long flingId; - - try { - flingId = shareId != null - ? flingService.findFlingByShareId(shareId).orElseThrow().getId() - : Long.parseLong(request.getParameter("flingId")); - } catch (NumberFormatException | NoSuchElementException e) { - log.warn("Invalid shareId [shareId=\"{}\"] or flingId [flingId=\"{}\"] found", - request.getParameter("shareId"), request.getParameter("flingId")); - flingId = null; - } - - return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); - } + return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java index 412bd15..78b7f56 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java @@ -1,6 +1,5 @@ package net.friedl.fling.security; public enum FlingAuthority { - FLING_OWNER, - FLING_USER + FLING_OWNER, FLING_USER } diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java b/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java index 3fd065e..da2a866 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java @@ -3,11 +3,9 @@ package net.friedl.fling.security; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.List; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -17,26 +15,26 @@ import lombok.Data; @Data @ConfigurationProperties("fling.security") public class FlingSecurityConfiguration { - private List allowedOrigins; + private List allowedOrigins; - private String adminUser; + private String adminUser; - private String adminPassword; + private String adminPassword; - private String signingKey; + private String signingKey; - private Long jwtExpiration; + private Long jwtExpiration; - @Bean - public Key jwtSigningKey() { - byte[] key = signingKey.getBytes(StandardCharsets.UTF_8); - return Keys.hmacShaKeyFor(key); - } + @Bean + public Key jwtSigningKey() { + byte[] key = signingKey.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(key); + } - @Bean - public JwtParser jwtParser(Key jwtSignigKey) { - return Jwts.parserBuilder() - .setSigningKey(jwtSignigKey) - .build(); - } + @Bean + public JwtParser jwtParser(Key jwtSignigKey) { + return Jwts.parserBuilder() + .setSigningKey(jwtSignigKey) + .build(); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java index 158033c..76ee797 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java @@ -1,11 +1,9 @@ package net.friedl.fling.security; import static org.springframework.security.config.Customizer.withDefaults; - import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -20,7 +18,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -32,23 +29,23 @@ import net.friedl.fling.security.authentication.JwtAuthenticationFilter; @Getter @Setter public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { - private JwtAuthenticationFilter jwtAuthenticationFilter; - private AuthorizationService authorizationService; - private FlingSecurityConfiguration securityConfiguration; + private JwtAuthenticationFilter jwtAuthenticationFilter; + private AuthorizationService authorizationService; + private FlingSecurityConfiguration securityConfiguration; - @Autowired - public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter, - AuthorizationService authorizationService, - FlingSecurityConfiguration securityConfiguraiton) { + @Autowired + public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter, + AuthorizationService authorizationService, + FlingSecurityConfiguration securityConfiguraiton) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - this.authorizationService = authorizationService; - this.securityConfiguration = securityConfiguraiton; - } + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.authorizationService = authorizationService; + this.securityConfiguration = securityConfiguraiton; + } - @Override - protected void configure(HttpSecurity http) throws Exception { - //@formatter:off + @Override + protected void configure(HttpSecurity http) throws Exception { + //@formatter:off http .csrf().disable() .cors(withDefaults()) @@ -102,45 +99,46 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { .hasAuthority(FlingAuthority.FLING_OWNER.name()); //@formatter:on - } + } - private RequestMatcher modificationMethodsAntMatcher(String antPattern) { - return multiMethodAntMatcher(antPattern, - HttpMethod.PATCH, HttpMethod.PUT, - HttpMethod.POST, HttpMethod.DELETE); - } + private RequestMatcher modificationMethodsAntMatcher(String antPattern) { + return multiMethodAntMatcher(antPattern, + HttpMethod.PATCH, HttpMethod.PUT, + HttpMethod.POST, HttpMethod.DELETE); + } - private RequestMatcher multiMethodAntMatcher(String antPattern, HttpMethod... httpMethods) { - List antMatchers = Arrays.stream(httpMethods) - .map(m -> new AntPathRequestMatcher(antPattern, m.toString())) - .collect(Collectors.toList()); + private RequestMatcher multiMethodAntMatcher(String antPattern, HttpMethod... httpMethods) { + List antMatchers = Arrays.stream(httpMethods) + .map(m -> new AntPathRequestMatcher(antPattern, m.toString())) + .collect(Collectors.toList()); - return new OrRequestMatcher(antMatchers); - } + return new OrRequestMatcher(antMatchers); + } - @Bean - public CorsConfigurationSource corsConfigurationSource() { - // see https://stackoverflow.com/a/43559266 + @Bean + public CorsConfigurationSource corsConfigurationSource() { + // see https://stackoverflow.com/a/43559266 - log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins()); + log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins()); - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins()); - configuration.setAllowedMethods(List.of("*")); + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins()); + configuration.setAllowedMethods(List.of("*")); - // setAllowCredentials(true) is important, otherwise: - // The value of the 'Access-Control-Allow-Origin' header in the response - // must not be the wildcard '*' when the request's credentials mode is - // 'include'. - configuration.setAllowCredentials(true); + // setAllowCredentials(true) is important, otherwise: + // The value of the 'Access-Control-Allow-Origin' header in the response + // must not be the wildcard '*' when the request's credentials mode is + // 'include'. + configuration.setAllowCredentials(true); - // setAllowedHeaders is important! Without it, OPTIONS preflight request - // will fail with 403 Invalid CORS request - configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Origin")); + // setAllowedHeaders is important! Without it, OPTIONS preflight request + // will fail with 403 Invalid CORS request + configuration + .setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Origin")); - final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); - return source; - } + return source; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java index 5e73f36..42b82f5 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java @@ -5,7 +5,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - import net.friedl.fling.security.authentication.dto.OwnerAuthDto; import net.friedl.fling.security.authentication.dto.UserAuthDto; @@ -13,20 +12,20 @@ import net.friedl.fling.security.authentication.dto.UserAuthDto; @RequestMapping("/api") public class AuthenticationController { - private AuthenticationService authenticationService; + private AuthenticationService authenticationService; - @Autowired - public AuthenticationController(AuthenticationService authenticationService) { - this.authenticationService = authenticationService; - } + @Autowired + public AuthenticationController(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } - @PostMapping("/auth/owner") - public String authenticateOwner(@RequestBody OwnerAuthDto ownerAuthDto) { - return authenticationService.authenticate(ownerAuthDto); - } + @PostMapping("/auth/owner") + public String authenticateOwner(@RequestBody OwnerAuthDto ownerAuthDto) { + return authenticationService.authenticate(ownerAuthDto); + } - @PostMapping("/auth/user") - public String authenticateUser(@RequestBody UserAuthDto userAuthDto) { - return authenticationService.authenticate(userAuthDto); - } + @PostMapping("/auth/user") + public String authenticateUser(@RequestBody UserAuthDto userAuthDto) { + return authenticationService.authenticate(userAuthDto); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java index 5d71c8c..f3828c8 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java @@ -3,13 +3,11 @@ package net.friedl.fling.security.authentication; import java.security.Key; import java.time.Instant; import java.util.Date; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; - import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; @@ -22,81 +20,81 @@ import net.friedl.fling.service.FlingService; @Service public class AuthenticationService { - private FlingService flingService; - private JwtParser jwtParser; - private Key signingKey; - private FlingSecurityConfiguration securityConfig; + private FlingService flingService; + private JwtParser jwtParser; + private Key signingKey; + private FlingSecurityConfiguration securityConfig; - @Autowired - public AuthenticationService(JwtParser jwtParser, Key signingKey, FlingService flingService, - FlingSecurityConfiguration securityConfig) { - this.flingService = flingService; - this.jwtParser = jwtParser; - this.signingKey = signingKey; - this.securityConfig = securityConfig; + @Autowired + public AuthenticationService(JwtParser jwtParser, Key signingKey, FlingService flingService, + FlingSecurityConfiguration securityConfig) { + this.flingService = flingService; + this.jwtParser = jwtParser; + this.signingKey = signingKey; + this.securityConfig = securityConfig; + } + + public String authenticate(OwnerAuthDto ownerAuth) { + if (!securityConfig.getAdminUser().equals(ownerAuth.getUsername())) { + throw new AccessDeniedException("Wrong credentials"); } - public String authenticate(OwnerAuthDto ownerAuth) { - if (!securityConfig.getAdminUser().equals(ownerAuth.getUsername())) { - throw new AccessDeniedException("Wrong credentials"); - } - - if (!securityConfig.getAdminPassword().equals(ownerAuth.getPassword())) { - throw new AccessDeniedException("Wrong credentials"); - } - - return makeBaseBuilder() - .setSubject("owner") - .compact(); + if (!securityConfig.getAdminPassword().equals(ownerAuth.getPassword())) { + throw new AccessDeniedException("Wrong credentials"); } - public String authenticate(UserAuthDto userAuth) { - var fling = flingService.findFlingByShareId(userAuth.getShareId()) - .orElseThrow(); - String authCode = userAuth.getCode(); + return makeBaseBuilder() + .setSubject("owner") + .compact(); + } - if (!flingService.hasAuthCode(fling.getId(), authCode)) { - throw new AccessDeniedException("Wrong fling code"); - } - - return makeBaseBuilder() - .setSubject("user") - .claim("sid", fling.getShareUrl()) - .compact(); + public String authenticate(UserAuthDto userAuth) { + var fling = flingService.findFlingByShareId(userAuth.getShareId()) + .orElseThrow(); + String authCode = userAuth.getCode(); + if (!flingService.hasAuthCode(fling.getId(), authCode)) { + throw new AccessDeniedException("Wrong fling code"); } - public Authentication parseAuthentication(String token) { - Claims claims = parseClaims(token); + return makeBaseBuilder() + .setSubject("user") + .claim("sid", fling.getShareUrl()) + .compact(); - FlingAuthority authority; - Long flingId; + } - switch (claims.getSubject()) { - case "owner": - authority = FlingAuthority.FLING_OWNER; - flingId = null; - break; - case "user": - authority = FlingAuthority.FLING_USER; - var sid = claims.get("sid", String.class); - flingId = flingService.findFlingByShareId(sid).orElseThrow().getId(); - break; - default: - throw new BadCredentialsException("Invalid token"); - } + public Authentication parseAuthentication(String token) { + Claims claims = parseClaims(token); - return new FlingToken(new GrantedFlingAuthority(authority, flingId)); + FlingAuthority authority; + Long flingId; + + switch (claims.getSubject()) { + case "owner": + authority = FlingAuthority.FLING_OWNER; + flingId = null; + break; + case "user": + authority = FlingAuthority.FLING_USER; + var sid = claims.get("sid", String.class); + flingId = flingService.findFlingByShareId(sid).orElseThrow().getId(); + break; + default: + throw new BadCredentialsException("Invalid token"); } - private JwtBuilder makeBaseBuilder() { - return Jwts.builder() - .setIssuedAt(Date.from(Instant.now())) - .setExpiration(Date.from(Instant.now().plusSeconds(securityConfig.getJwtExpiration()))) - .signWith(signingKey); - } + return new FlingToken(new GrantedFlingAuthority(authority, flingId)); + } - private Claims parseClaims(String token) { - return jwtParser.parseClaimsJws(token).getBody(); - } + private JwtBuilder makeBaseBuilder() { + return Jwts.builder() + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plusSeconds(securityConfig.getJwtExpiration()))) + .signWith(signingKey); + } + + private Claims parseClaims(String token) { + return jwtParser.parseClaimsJws(token).getBody(); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java index 08aebf2..ccff7cd 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java @@ -1,35 +1,34 @@ package net.friedl.fling.security.authentication; import java.util.List; - import org.springframework.security.authentication.AbstractAuthenticationToken; public class FlingToken extends AbstractAuthenticationToken { - private static final long serialVersionUID = -1112423505610346583L; - private GrantedFlingAuthority grantedFlingAuthority; + private static final long serialVersionUID = -1112423505610346583L; + private GrantedFlingAuthority grantedFlingAuthority; - public FlingToken(GrantedFlingAuthority authority) { - super(List.of(authority)); - this.grantedFlingAuthority = authority; - } + public FlingToken(GrantedFlingAuthority authority) { + super(List.of(authority)); + this.grantedFlingAuthority = authority; + } - public GrantedFlingAuthority getGrantedFlingAuthority() { - return this.grantedFlingAuthority; - } + public GrantedFlingAuthority getGrantedFlingAuthority() { + return this.grantedFlingAuthority; + } - @Override - public Object getCredentials() { - return null; - } + @Override + public Object getCredentials() { + return null; + } - @Override - public Object getPrincipal() { - return null; - } + @Override + public Object getPrincipal() { + return null; + } - @Override - public boolean isAuthenticated() { - return true; - } + @Override + public boolean isAuthenticated() { + return true; + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java index d032a26..15d7c36 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java @@ -1,7 +1,6 @@ package net.friedl.fling.security.authentication; import org.springframework.security.core.GrantedAuthority; - import net.friedl.fling.security.FlingAuthority; /** @@ -11,23 +10,23 @@ import net.friedl.fling.security.FlingAuthority; */ public class GrantedFlingAuthority implements GrantedAuthority { - private static final long serialVersionUID = -1552301479158714777L; + private static final long serialVersionUID = -1552301479158714777L; - private FlingAuthority authority; - private Long flingId; + private FlingAuthority authority; + private Long flingId; - public GrantedFlingAuthority(FlingAuthority authority, Long flingId) { - this.authority = authority; - this.flingId = flingId; - } + public GrantedFlingAuthority(FlingAuthority authority, Long flingId) { + this.authority = authority; + this.flingId = flingId; + } - public Long getFlingId() { - return this.flingId; - } + public Long getFlingId() { + return this.flingId; + } - @Override - public String getAuthority() { - return authority.name(); - } + @Override + public String getAuthority() { + return authority.name(); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java index 0ca7569..71427d6 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java @@ -1,55 +1,53 @@ package net.friedl.fling.security.authentication; import java.io.IOException; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; - import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { - private static final String TOKEN_PREFIX = "Bearer "; - private static final String HEADER_STRING = "Authorization"; + private static final String TOKEN_PREFIX = "Bearer "; + private static final String HEADER_STRING = "Authorization"; - private AuthenticationService authenticationService; + private AuthenticationService authenticationService; - @Autowired - public JwtAuthenticationFilter(AuthenticationService authenticationService) { - this.authenticationService = authenticationService; + @Autowired + public JwtAuthenticationFilter(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String header = request.getHeader(HEADER_STRING); + + if (header == null || !header.startsWith(TOKEN_PREFIX)) { + log.warn("Could not find bearer token. No JWT authentication."); + filterChain.doFilter(request, response); + return; } - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { + String authToken = header.replace(TOKEN_PREFIX, ""); - String header = request.getHeader(HEADER_STRING); + SecurityContext securityContext = SecurityContextHolder.getContext(); - if(header == null || !header.startsWith(TOKEN_PREFIX)) { - log.warn("Could not find bearer token. No JWT authentication."); - filterChain.doFilter(request, response); - return; - } - - String authToken = header.replace(TOKEN_PREFIX, ""); - - SecurityContext securityContext = SecurityContextHolder.getContext(); - - if(securityContext.getAuthentication() == null) { - Authentication authentication = authenticationService.parseAuthentication(authToken); - securityContext.setAuthentication(authentication); - } - - filterChain.doFilter(request, response); + if (securityContext.getAuthentication() == null) { + Authentication authentication = authenticationService.parseAuthentication(authToken); + securityContext.setAuthentication(authentication); } + + filterChain.doFilter(request, response); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java index 5c6a340..c4ee495 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java @@ -4,6 +4,6 @@ import lombok.Data; @Data public class OwnerAuthDto { - private String username; - private String password; + private String username; + private String password; } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java index 0c714f3..51b08bc 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java @@ -4,6 +4,6 @@ import lombok.Data; @Data public class UserAuthDto { - String shareId; - String code; + String shareId; + String code; } diff --git a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java index dc35d56..412d5fa 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java @@ -4,14 +4,11 @@ import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Optional; - import javax.transaction.Transactional; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.json.JsonParser; import org.springframework.boot.json.JsonParserFactory; import org.springframework.stereotype.Service; - import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.mapper.ArtifactMapper; import net.friedl.fling.persistence.archive.Archive; @@ -24,69 +21,69 @@ import net.friedl.fling.persistence.repositories.FlingRepository; @Transactional public class ArtifactService { - private FlingRepository flingRepository; - private ArtifactRepository artifactRepository; - private ArtifactMapper artifactMapper; - private Archive archive; + private FlingRepository flingRepository; + private ArtifactRepository artifactRepository; + private ArtifactMapper artifactMapper; + private Archive archive; - @Autowired - public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository, - ArtifactMapper artifactMapper, Archive archive) { - this.artifactRepository = artifactRepository; - this.flingRepository = flingRepository; - this.artifactMapper = artifactMapper; - this.archive = archive; - } + @Autowired + public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository, + ArtifactMapper artifactMapper, Archive archive) { + this.artifactRepository = artifactRepository; + this.flingRepository = flingRepository; + this.artifactMapper = artifactMapper; + this.archive = archive; + } - public List findAllArtifacts(Long flingId) { - return artifactMapper.map(artifactRepository.findAllByFlingId(flingId)); - } + public List findAllArtifacts(Long flingId) { + return artifactMapper.map(artifactRepository.findAllByFlingId(flingId)); + } - public ArtifactDto storeArtifact(Long flingId, InputStream artifact) throws ArchiveException { - var flingEntity = flingRepository.findById(flingId).orElseThrow(); - var archiveId = archive.store(artifact); + public ArtifactDto storeArtifact(Long flingId, InputStream artifact) throws ArchiveException { + var flingEntity = flingRepository.findById(flingId).orElseThrow(); + var archiveId = archive.store(artifact); - ArtifactEntity artifactEntity = new ArtifactEntity(); - artifactEntity.setDoi(archiveId); - artifactEntity.setFling(flingEntity); + ArtifactEntity artifactEntity = new ArtifactEntity(); + artifactEntity.setDoi(archiveId); + artifactEntity.setFling(flingEntity); - artifactRepository.save(artifactEntity); + artifactRepository.save(artifactEntity); - return artifactMapper.map(artifactEntity); - } + return artifactMapper.map(artifactEntity); + } - public Optional findArtifact(Long artifactId) { - return artifactMapper.map(artifactRepository.findById(artifactId)); - } + public Optional findArtifact(Long artifactId) { + return artifactMapper.map(artifactRepository.findById(artifactId)); + } - public ArtifactDto mergeArtifact(Long artifactId, String body) { - JsonParser jsonParser = JsonParserFactory.getJsonParser(); - Map parsedBody = jsonParser.parseMap(body); + public ArtifactDto mergeArtifact(Long artifactId, String body) { + JsonParser jsonParser = JsonParserFactory.getJsonParser(); + Map parsedBody = jsonParser.parseMap(body); - artifactRepository.findById(artifactId) - // map entity to dto - .map(artifactMapper::map) - // merge parsedBody into dto - .map(a -> artifactMapper.merge(a, parsedBody)) - // map dto to entity - .map(artifactMapper::map) - .ifPresent(artifactRepository::save); + artifactRepository.findById(artifactId) + // map entity to dto + .map(artifactMapper::map) + // merge parsedBody into dto + .map(a -> artifactMapper.merge(a, parsedBody)) + // map dto to entity + .map(artifactMapper::map) + .ifPresent(artifactRepository::save); - return artifactMapper.map(artifactRepository.getOne(artifactId)); - } + return artifactMapper.map(artifactRepository.getOne(artifactId)); + } - public void deleteArtifact(Long artifactId) throws ArchiveException { - var doi = artifactRepository.getOne(artifactId).getDoi(); - artifactRepository.deleteById(artifactId); - archive.remove(doi); - } + public void deleteArtifact(Long artifactId) throws ArchiveException { + var doi = artifactRepository.getOne(artifactId).getDoi(); + artifactRepository.deleteById(artifactId); + archive.remove(doi); + } - public String generateDownloadId(Long artifactId) { - // TODO: This id is not secured! Generate temporary download id - return artifactRepository.getOne(artifactId).getDoi(); - } + public String generateDownloadId(Long artifactId) { + // TODO: This id is not secured! Generate temporary download id + return artifactRepository.getOne(artifactId).getDoi(); + } - public InputStream downloadArtifact(String downloadId) throws ArchiveException { - return archive.get(downloadId); - } + public InputStream downloadArtifact(String downloadId) throws ArchiveException { + return archive.get(downloadId); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java index 6f4dc84..f346bf3 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java @@ -16,16 +16,13 @@ import java.util.function.Supplier; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; - import javax.transaction.Transactional; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; - import lombok.extern.slf4j.Slf4j; import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.mapper.FlingMapper; @@ -40,157 +37,161 @@ import net.friedl.fling.persistence.repositories.FlingRepository; @Transactional public class FlingService { - private FlingRepository flingRepository; - private FlingMapper flingMapper; - private Archive archive; - private MessageDigest keyHashDigest; + private FlingRepository flingRepository; + private FlingMapper flingMapper; + private Archive archive; + private MessageDigest keyHashDigest; - @Autowired - public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive, MessageDigest keyHashDigest) { - this.flingRepository = flingRepository; - this.flingMapper = flingMapper; - this.archive = archive; - this.keyHashDigest = keyHashDigest; + @Autowired + public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive, + MessageDigest keyHashDigest) { + this.flingRepository = flingRepository; + this.flingMapper = flingMapper; + this.archive = archive; + this.keyHashDigest = keyHashDigest; + } + + public List findAll() { + return flingMapper.map(flingRepository.findAll()); + } + + public Long createFling(FlingDto flingDto) { + if (!StringUtils.hasText(flingDto.getShareUrl())) { + flingDto.setShareUrl(generateShareUrl()); } - public List findAll() { - return flingMapper.map(flingRepository.findAll()); - } + var flingEntity = flingMapper.map(flingDto); + flingEntity.setAuthCode(hashKey(flingEntity.getAuthCode())); + flingEntity = flingRepository.save(flingEntity); + return flingEntity.getId(); + } - public Long createFling(FlingDto flingDto) { - if (!StringUtils.hasText(flingDto.getShareUrl())) { - flingDto.setShareUrl(generateShareUrl()); + public Boolean existsShareUrl(String shareUrl) { + return !flingRepository.findByShareUrl(shareUrl).isEmpty(); + } + + public void mergeFling(Long flingId, FlingDto flingDto) { + var flingEntity = flingRepository.getOne(flingId); + + mergeNonEmpty(flingDto::getAllowUpload, flingEntity::setAllowUpload); + mergeNonEmpty(flingDto::getDirectDownload, flingEntity::setDirectDownload); + mergeWithEmpty(flingDto::getExpirationClicks, flingEntity::setExpirationClicks); + mergeWithEmpty(flingDto::getExpirationTime, flingEntity::setExpirationTime); + mergeNonEmpty(flingDto::getName, flingEntity::setName); + mergeNonEmpty(flingDto::getShared, flingEntity::setShared); + mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl); + mergeWithEmpty(() -> hashKey(flingDto.getAuthCode()), flingEntity::setAuthCode); + } + + public Optional findFlingById(Long flingId) { + return flingMapper.map(flingRepository.findById(flingId)); + } + + public Optional findFlingByShareId(String shareUrl) { + return flingMapper.map(flingRepository.findByShareUrl(shareUrl)); + } + + public void deleteFlingById(Long flingId) { + flingRepository.deleteById(flingId); + } + + public boolean hasAuthCode(Long flingId, String authCode) { + var fling = flingRepository.getOne(flingId); + + if (!StringUtils.hasText(fling.getAuthCode())) + return true; + + return fling.getAuthCode().equals(hashKey(authCode)); + } + + public String getShareName(String shareUrl) { + + FlingEntity flingEntity = flingRepository.findByShareUrl(shareUrl).orElseThrow(); + + if (flingEntity.getArtifacts().size() > 1) + return flingEntity.getName(); + else if (flingEntity.getArtifacts().size() == 1) + return flingEntity.getArtifacts().stream().findFirst().get().getName(); + + return null; + } + + public Long countArtifacts(Long flingId) { + return flingRepository.countArtifactsById(flingId); + } + + public Long getFlingSize(Long flingId) { + var fling = flingRepository.getOne(flingId); + + return fling.getArtifacts().stream() + .map(ae -> ae.getSize()) + .reduce(0L, (acc, as) -> acc + as); + } + + public String packageFling(Long flingId) throws IOException, ArchiveException { + var fling = flingRepository.getOne(flingId); + var tempFile = Files.createTempFile(Long.toString(flingId), ".zip"); + + try (var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))) { + zipStream.setLevel(Deflater.BEST_SPEED); + for (ArtifactEntity artifactEntity : fling.getArtifacts()) { + ZipEntry ze = new ZipEntry(artifactEntity.getName()); + zipStream.putNextEntry(ze); + + var artifactStream = archive.get(artifactEntity.getDoi()); + try (var archiveEntryStream = new BufferedInputStream(artifactStream)) { + int b; + while ((b = archiveEntryStream.read()) != -1) { + zipStream.write(b); + } + } finally { + zipStream.closeEntry(); } - - var flingEntity = flingMapper.map(flingDto); - flingEntity.setAuthCode(hashKey(flingEntity.getAuthCode())); - flingEntity = flingRepository.save(flingEntity); - return flingEntity.getId(); + } } - public Boolean existsShareUrl(String shareUrl) { - return !flingRepository.findByShareUrl(shareUrl).isEmpty(); - } + return tempFile.getFileName().toString(); + } - public void mergeFling(Long flingId, FlingDto flingDto) { - var flingEntity = flingRepository.getOne(flingId); + public Pair downloadFling(String fileId) throws IOException, ArchiveException { + var tempFile = Paths.get(System.getProperty("java.io.tmpdir"), fileId).toFile(); - mergeNonEmpty(flingDto::getAllowUpload, flingEntity::setAllowUpload); - mergeNonEmpty(flingDto::getDirectDownload, flingEntity::setDirectDownload); - mergeWithEmpty(flingDto::getExpirationClicks, flingEntity::setExpirationClicks); - mergeWithEmpty(flingDto::getExpirationTime, flingEntity::setExpirationTime); - mergeNonEmpty(flingDto::getName, flingEntity::setName); - mergeNonEmpty(flingDto::getShared, flingEntity::setShared); - mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl); - mergeWithEmpty(() -> hashKey(flingDto.getAuthCode()), flingEntity::setAuthCode); - } + var archiveLength = tempFile.length(); + var archiveStream = new FileInputStream(tempFile); - public Optional findFlingById(Long flingId) { - return flingMapper.map(flingRepository.findById(flingId)); - } + return Pair.of(archiveStream, archiveLength); + } - public Optional findFlingByShareId(String shareUrl) { - return flingMapper.map(flingRepository.findByShareUrl(shareUrl)); - } + public String generateShareUrl() { + var key = KeyGenerators + .secureRandom(16) + .generateKey(); - public void deleteFlingById(Long flingId) { - flingRepository.deleteById(flingId); - } + return Base64.getUrlEncoder().encodeToString(key) + // replace all special chars [=-_] in RFC 4648 + // "URL and Filename safe" table with characters from + // [A-Za-z0-9]. Hence, the generated share url will only consist + // of [A-Za-z0-9]. + .replace('=', 'q') + .replace('_', 'u') + .replace('-', 'd'); + } - public boolean hasAuthCode(Long flingId, String authCode) { - var fling = flingRepository.getOne(flingId); + public String hashKey(String key) { + if (!StringUtils.hasText(key)) + return null; - if(!StringUtils.hasText(fling.getAuthCode())) return true; + return new String(Hex.encode(keyHashDigest.digest(key.getBytes()))); + } - return fling.getAuthCode().equals(hashKey(authCode)); - } + private void mergeNonEmpty(Supplier sup, Consumer con) { + T r = sup.get(); + if (r != null) + con.accept(r); + } - public String getShareName(String shareUrl) { - - FlingEntity flingEntity = flingRepository.findByShareUrl(shareUrl).orElseThrow(); - - if (flingEntity.getArtifacts().size() > 1) - return flingEntity.getName(); - else if (flingEntity.getArtifacts().size() == 1) - return flingEntity.getArtifacts().stream().findFirst().get().getName(); - - return null; - } - - public Long countArtifacts(Long flingId) { - return flingRepository.countArtifactsById(flingId); - } - - public Long getFlingSize(Long flingId) { - var fling = flingRepository.getOne(flingId); - - return fling.getArtifacts().stream() - .map(ae -> ae.getSize()) - .reduce(0L, (acc, as) -> acc+as); - } - - public String packageFling(Long flingId) throws IOException, ArchiveException { - var fling = flingRepository.getOne(flingId); - var tempFile = Files.createTempFile(Long.toString(flingId), ".zip"); - - try(var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))){ - zipStream.setLevel(Deflater.BEST_SPEED); - for(ArtifactEntity artifactEntity: fling.getArtifacts()) { - ZipEntry ze = new ZipEntry(artifactEntity.getName()); - zipStream.putNextEntry(ze); - - var artifactStream = archive.get(artifactEntity.getDoi()); - try(var archiveEntryStream = new BufferedInputStream(artifactStream)) { - int b; - while( (b = archiveEntryStream.read()) != -1 ) { - zipStream.write(b); - } - } finally { - zipStream.closeEntry(); - } - } - } - - return tempFile.getFileName().toString(); - } - - public Pair downloadFling(String fileId) throws IOException, ArchiveException { - var tempFile = Paths.get(System.getProperty("java.io.tmpdir"), fileId).toFile(); - - var archiveLength = tempFile.length(); - var archiveStream = new FileInputStream(tempFile); - - return Pair.of(archiveStream, archiveLength); - } - - public String generateShareUrl() { - var key = KeyGenerators - .secureRandom(16) - .generateKey(); - - return Base64.getUrlEncoder().encodeToString(key) - // replace all special chars [=-_] in RFC 4648 - // "URL and Filename safe" table with characters from - // [A-Za-z0-9]. Hence, the generated share url will only consist - // of [A-Za-z0-9]. - .replace('=', 'q') - .replace('_', 'u') - .replace('-', 'd'); - } - - public String hashKey(String key) { - if(!StringUtils.hasText(key)) return null; - - return new String(Hex.encode(keyHashDigest.digest(key.getBytes()))); - } - - private void mergeNonEmpty(Supplier sup, Consumer con) { - T r = sup.get(); - if(r != null) con.accept(r); - } - - private void mergeWithEmpty(Supplier sup, Consumer con) { - T r = sup.get(); - con.accept(r); - } + private void mergeWithEmpty(Supplier sup, Consumer con) { + T r = sup.get(); + con.accept(r); + } } diff --git a/service/fling/src/main/resources/fling.yaml b/service/fling/src/main/resources/fling.yaml deleted file mode 100644 index 7ca4147..0000000 --- a/service/fling/src/main/resources/fling.yaml +++ /dev/null @@ -1,287 +0,0 @@ ---- -openapi: 3.0.2 -info: - title: Fling - version: 1.0.0 - description: A project based API for publishing and sharing digital artifacts - contact: - name: Armin Friedl - email: dev@friedl.net -paths: - /fling: - summary: A fling share - description: |- - A grouping of shared objects. Settings for sharing and expiration are - associated with a fling. - get: - responses: - "200": - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Fling' - description: List of metadata for all flings - summary: Get a list of metadata for all flings - post: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Fling' - required: true - responses: - "200": - description: Fling created successfuly - summary: Create a new fling - /fling/{flingId}: - get: - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Fling' - description: A fling object containing all metadata of the fling - summary: Get metadata for a fling - description: Expiration, sharing and general information of a fling. - delete: - responses: - "200": - description: Fling deleted successfully - patch: - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Fling' - required: true - parameters: - - name: id - description: The unique fling id - schema: - format: int64 - type: integer - in: query - required: true - responses: - "200": - description: Fling was successfully updated - parameters: - - name: flingId - description: Unique id of the fling - schema: - format: int64 - type: integer - in: path - required: true - /f: - summary: Endpoint for accessing flings by external users - /f/{shareId}: - get: - responses: - "302": - content: - text/html: - schema: - type: string - description: |- - If the fling is marked as direct download, the user will be directly redirected - to /f/{sharId}/download which starts the fling download. - "200": - content: - application/json: {} - description: sdf - parameters: - - name: shareId - description: |- - A share id with which a fling can be uniquely identified. The share id might be - the fling id also used in /fling endpoints, another artificially generated id or - a customURL given by the fling creator - schema: - type: string - in: path - required: true - /fling/{flingId}/artifact: - summary: Upload an object to the fling identified by id - get: - responses: - "200": - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Artifact' - description: A list of metadata of all objects within this fling - summary: Retrieve a list of metadata all objects within this fling - post: - requestBody: - description: "Content type is any media type from \nhttps://www.iana.org/assignments/media-types/media-types.xhtml.\n\ - The content can by arbitrary payload and will be stored as-is." - content: - application/octet-stream: - schema: - format: binary - type: string - required: true - parameters: - - name: name - description: The name of the object - schema: - type: string - in: query - required: true - - name: path - description: |- - A path under which the object should be stored. Nested paths must be delimited - by forward slash '/'. The path might or might not start with a '/', it will always - be interpreted as absolute starting from the fling root. - schema: - type: string - in: query - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Artifact' - description: Return the metadata of a fling object after successful request - parameters: - - name: flingId - description: Unique id for the fling - schema: - format: int64 - type: integer - in: path - required: true - /fling/{flingId}/artifact/{objectId}: - summary: Endpoint for interacting with individual objects within a fling - get: - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Artifact' - description: Return the metadata of a fling object after successful request - summary: Retrive the metadata of an object within a fling - put: - requestBody: - description: "Content type is any media type from \nhttps://www.iana.org/assignments/media-types/media-types.xhtml.\n\ - The content can by arbitrary payload and will be stored as-is." - content: - application/octet-stream: - schema: - format: binary - type: string - required: true - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Artifact' - description: Return the metadata of a fling object after successful request - summary: Create a new version of the object - parameters: - - name: flingId - description: The unique id of the fling - schema: - format: int64 - type: integer - in: path - required: true - - name: objectId - description: |- - The unique id of the object within its fling. The id is not necessarily globally - unique across all flings. - schema: - format: int64 - type: integer - in: path - required: true -components: - schemas: - Fling: - description: |- - Fling containing all metadata of a fling, including general information, - sharing settings and expiration settings. - required: - - name - type: object - properties: - name: - description: Human readable name of the fling - type: string - id: - format: int64 - description: Unique id of the fling - type: integer - expiration: - description: Expiration settings - type: object - properties: - expirationType: - description: Expiration type - enum: - - Time - - Clicks - type: string - value: - description: Parsable string representation for the expiration type - type: string - sharing: - description: Settings for sharing - type: object - properties: - directDownload: - description: Accessing the share will immediately prompt a download - type: boolean - upload: - description: Allow external users to upload to the share - type: boolean - customURL: - description: |- - Makes the fling available under a custom URL. The URL is only - allowed to have one path element. The allowed characters are - [A-Za-z0-9-._~], that is, the unreserved URL characters of - https://www.ietf.org/rfc/rfc3986 excluding the forward - slash. The customURL must be unique across all flings - of a Fling application deployment. - type: string - Artifact: - description: An object in a fling share - required: - - name - - doi - type: object - properties: - name: - description: The name of the object - type: string - id: - format: int64 - description: The unique id of the object within its fling - type: integer - path: - description: |- - The path of the object within its fling. Nested paths are separated by a forward - slash '/'. A path may or may not start with '/'. A path will always be interpreted - as absolute with the fling of the object as root. - type: string - doi: - description: "A unique and stable id for the fling object. This id is unique\ - \ across all flings\nand versions of an object within a Fling deployment.\ - \ A doi is never assigned twice\nby a Fling deployment, even if the object\ - \ it referred to (or a version thereof) \nwas delted. A doi might not\ - \ resolve to an object if the object was deleted." - type: string - version: - description: The version of the object - type: integer - fling: - format: int64 - description: The id of the fling this object belongs to - type: integer diff --git a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java index 36588bc..4e5c442 100644 --- a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java +++ b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java @@ -5,9 +5,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; - import java.util.List; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @@ -16,7 +14,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.test.web.servlet.MockMvc; - import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.service.ArtifactService; @@ -26,34 +23,34 @@ import net.friedl.fling.service.ArtifactService; // do not try to create beans in security excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*")) class ArtifactControllerTest { - @Autowired - private MockMvc mvc; + @Autowired + private MockMvc mvc; - @MockBean - private ArtifactService artifactService; + @MockBean + private ArtifactService artifactService; - @Test - public void testGetArtifacts_noArtifacts_empty() throws Exception { - Long flingId = 123L; + @Test + public void testGetArtifacts_noArtifacts_empty() throws Exception { + Long flingId = 123L; - when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of()); + when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of()); - mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) - .andExpect(jsonPath("$", hasSize(0))); - } + mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) + .andExpect(jsonPath("$", hasSize(0))); + } - @Test - public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception { - Long flingId = 123L; - String artifactName = "TEST"; + @Test + public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception { + Long flingId = 123L; + String artifactName = "TEST"; - ArtifactDto artifactDto = new ArtifactDto(); - artifactDto.setName(artifactName); + ArtifactDto artifactDto = new ArtifactDto(); + artifactDto.setName(artifactName); - when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto)); + when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto)); - mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name", equalTo(artifactName))); - } + mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].name", equalTo(artifactName))); + } }