commit d075b14564a6eaf69a9f2e90c876be65e53def77 Author: droideparanoico Date: Sat Feb 13 11:47:33 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e495677 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.gradle/ +/.idea/ +/build/ +/gradle/ +/gradlew.bat +/gradlew diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0c489f6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 droideparanoico + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54a280e --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ + +

Code Sharing Platform

+ +Web service to store syntax highlighted code snippets generating unique identifiers with the added possibility of configure its self-deletion based on time and views limitations. + +There are two possible interfaces: API and web. The API interface returns data as `JSON`, while the web interface uses `HTML`, `JS`, and `CSS`. + +

Web interface endpoints

+ +* `GET /` should return the starting page: + + ![](src/main/resources/img/index.png) +* `GET /code/new` should return the page that allows to save a new snippet: + + ![](src/main/resources/img/new_snippet.png) +* `GET /code/UUID` should return the page with associated code snippet: + + ![](src/main/resources/img/snippet.png) +* `GET /code/latest` should return the page with 10 most recently uploaded unrestricted code snippets: + + ![](src/main/resources/img/latest.png) +

API interface endpoints

+ +* `GET /api/code/new` should take a JSON object with a field `code` and two other fields: + +     1. `time` field containing the time (in seconds) during which the snippet is accessible. + +     2. `views` field containing a number of views allowed for this snippet. + + And return JSON with a single field id: + + ![](src/main/resources/img/api_new_snippet.png) +* `GET /api/code/UUID` should return a JSON object with associated code snippet: + + ![](src/main/resources/img/api_snippet.png) +* `GET /api/code/latest` should return a JSON array the 10 most recently uploaded unrestricted code snippets: + + ![](src/main/resources/img/api_latest.png) \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..80c3ff9 --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'org.springframework.boot' version '2.3.3.RELEASE' + id 'java' +} + +apply plugin: 'io.spring.dependency-management' + +sourceCompatibility = 11 + +repositories { + mavenCentral() +} + +sourceSets.main.resources.srcDirs = ["src/main/resources"] + +dependencies { + runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-freemarker' + compile("org.springframework.boot:spring-boot-starter-web") + compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.12.1' + testImplementation('org.springframework.boot:spring-boot-starter-test') +} \ No newline at end of file diff --git a/src/main/java/com/droideparanoico/codesharingplatform/CodeSharingPlatform.java b/src/main/java/com/droideparanoico/codesharingplatform/CodeSharingPlatform.java new file mode 100644 index 0000000..e804351 --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/CodeSharingPlatform.java @@ -0,0 +1,13 @@ +package com.droideparanoico.codesharingplatform; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CodeSharingPlatform { + + public static void main(String[] args) { + SpringApplication.run(CodeSharingPlatform.class, args); + } + +} diff --git a/src/main/java/com/droideparanoico/codesharingplatform/controller/ApiController.java b/src/main/java/com/droideparanoico/codesharingplatform/controller/ApiController.java new file mode 100644 index 0000000..4c62f9c --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/controller/ApiController.java @@ -0,0 +1,39 @@ +package com.droideparanoico.codesharingplatform.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; +import com.droideparanoico.codesharingplatform.model.CodeSnippet; +import com.droideparanoico.codesharingplatform.service.CodeSnippetService; + +import java.util.Collection; +import java.util.Map; + +@RestController +@RequestMapping("/api/code") +public class ApiController { + + private CodeSnippetService codeSnippetService; + + @Qualifier("codeSnippetService") + @Autowired + public void setService(CodeSnippetService service) { + this.codeSnippetService = service; + } + + @GetMapping(path = "/{uuid}") + public CodeSnippet getCodeApi(@PathVariable String uuid) { + return codeSnippetService.getCodeSnippetById(uuid); + } + + @GetMapping(path = "/latest") + public Collection getLatestCodeApi() { + return codeSnippetService.findLatestUnrestricted(); + } + + @PostMapping(path = "/new", produces = "application/json;charset=UTF-8") + public Map createNewCodeApi(@RequestBody CodeSnippet codeSnippet) { + CodeSnippet responseCode = codeSnippetService.saveCodeSnippet(codeSnippet); + return Map.of("id", responseCode.getUuid()); + } +} diff --git a/src/main/java/com/droideparanoico/codesharingplatform/controller/WebController.java b/src/main/java/com/droideparanoico/codesharingplatform/controller/WebController.java new file mode 100644 index 0000000..c79cc40 --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/controller/WebController.java @@ -0,0 +1,50 @@ +package com.droideparanoico.codesharingplatform.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import com.droideparanoico.codesharingplatform.model.CodeSnippet; +import com.droideparanoico.codesharingplatform.service.CodeSnippetService; + +import java.util.*; + +@Controller +@RequestMapping("/") +public class WebController { + + private CodeSnippetService codeSnippetService; + + @Qualifier("codeSnippetService") + @Autowired + public void setService(CodeSnippetService service) { + this.codeSnippetService = service; + } + + @GetMapping(path = "/", produces = "text/html") + public String index() { + return "index"; + } + + @GetMapping(path = "/code/{uuid}", produces = "text/html") + public String getCodeWeb(Model model, @PathVariable String uuid) { + model.addAttribute("codeSnippet", codeSnippetService.getCodeSnippetById(uuid)); + return "snippet"; + } + + @GetMapping(path = "/code/latest", produces = "text/html") + public String getLatestCodeWeb(Model model) { + if (codeSnippetService.findLatestUnrestricted() != null) { + Collection snippetCollection = codeSnippetService.findLatestUnrestricted(); + List snippetList = new ArrayList<>(snippetCollection); + model.addAttribute("codeSnippets",snippetList); + } + return "latest"; + } + + @GetMapping(path = "/code/new", produces = "text/html") + public String createNewCodeWeb() { + return "new_snippet"; + } +} diff --git a/src/main/java/com/droideparanoico/codesharingplatform/model/CodeSnippet.java b/src/main/java/com/droideparanoico/codesharingplatform/model/CodeSnippet.java new file mode 100644 index 0000000..2775092 --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/model/CodeSnippet.java @@ -0,0 +1,96 @@ +package com.droideparanoico.codesharingplatform.model; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +@Entity +@Table(name="snippets") +public class CodeSnippet { + + @Id + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String uuid; + private LocalDateTime date; + @Lob + @Column + private String code; + private Long time = 0L; + private Integer views = 0; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private boolean timeRestricted; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private boolean viewRestricted; + + static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public LocalDateTime getDate() { + return date; + } + + @JsonProperty("date") + public String getFormattedDate() { + return date == null ? "" : formatter.format(date); + } + + public void setDate(LocalDateTime date) { + this.date = date; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Long getTime() { + return time; + } + + @JsonGetter("time") + public Long getTimeAvailable() { + return time == 0 ? 0 : time - date.until(LocalDateTime.now(), ChronoUnit.SECONDS); + } + + public void setTime(Long time) { + this.time = time; + } + + public Integer getViews() { + return views; + } + + public void setViews(Integer views) { + this.views = views; + } + + public boolean isTimeRestricted() { + return timeRestricted; + } + + public void setTimeRestricted(boolean timeRestricted) { + this.timeRestricted = timeRestricted; + } + + public boolean isViewRestricted() { + return viewRestricted; + } + + public void setViewRestricted(boolean viewRestricted) { + this.viewRestricted = viewRestricted; + } +} diff --git a/src/main/java/com/droideparanoico/codesharingplatform/repository/CodeSnippetRepository.java b/src/main/java/com/droideparanoico/codesharingplatform/repository/CodeSnippetRepository.java new file mode 100644 index 0000000..f1f6620 --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/repository/CodeSnippetRepository.java @@ -0,0 +1,11 @@ +package com.droideparanoico.codesharingplatform.repository; + +import org.springframework.data.repository.CrudRepository; +import com.droideparanoico.codesharingplatform.model.CodeSnippet; + +import java.util.Collection; + +public interface CodeSnippetRepository extends CrudRepository { + + Collection findTop10ByTimeEqualsAndViewsEqualsOrderByDateDesc(Long time, Integer views); +} \ No newline at end of file diff --git a/src/main/java/com/droideparanoico/codesharingplatform/service/CodeSnippetService.java b/src/main/java/com/droideparanoico/codesharingplatform/service/CodeSnippetService.java new file mode 100644 index 0000000..d7fa055 --- /dev/null +++ b/src/main/java/com/droideparanoico/codesharingplatform/service/CodeSnippetService.java @@ -0,0 +1,68 @@ +package com.droideparanoico.codesharingplatform.service; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import com.droideparanoico.codesharingplatform.model.CodeSnippet; +import com.droideparanoico.codesharingplatform.repository.CodeSnippetRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.UUID; + +@Service("codeSnippetService") +public class CodeSnippetService { + + private final CodeSnippetRepository codeSnippetRepository; + + public CodeSnippetService(CodeSnippetRepository codeSnippetRepository) { + this.codeSnippetRepository = codeSnippetRepository; + } + + public CodeSnippet getCodeSnippetById(final String uuid) { + CodeSnippet codeSnippet = codeSnippetRepository.findById(uuid).orElseThrow( + () -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + if (codeSnippet.isViewRestricted()) { + checkViewsRestriction(codeSnippet); + } + + if (codeSnippet.isTimeRestricted()) { + checkTimeRestriction(codeSnippet); + } + + return codeSnippetRepository.save(codeSnippet); + } + + public CodeSnippet saveCodeSnippet(final CodeSnippet codeSnippet) { + codeSnippet.setUuid(UUID.randomUUID().toString()); + codeSnippet.setDate(LocalDateTime.now()); + codeSnippet.setTimeRestricted(codeSnippet.getTime() > 0); + codeSnippet.setViewRestricted(codeSnippet.getViews() > 0); + + return codeSnippetRepository.save(codeSnippet); + } + + public Collection findLatestUnrestricted() { + return codeSnippetRepository.findTop10ByTimeEqualsAndViewsEqualsOrderByDateDesc(0L, 0); + } + + private void checkViewsRestriction(final CodeSnippet codeSnippet) { + if (codeSnippet.getViews() > 0) { + codeSnippet.setViews(codeSnippet.getViews() - 1); + } else { + deleteSnippet(codeSnippet); + } + } + + private void checkTimeRestriction(final CodeSnippet codeSnippet) { + if (codeSnippet.getTimeAvailable() < 0) { + deleteSnippet(codeSnippet); + } + } + + private void deleteSnippet(final CodeSnippet codeSnippet) { + codeSnippetRepository.delete(codeSnippet); + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..69896e3 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +server.port=8889 +management.endpoints.web.exposure.include=* +management.endpoint.shutdown.enabled=true + +spring.datasource.url=jdbc:h2:file:../snippets +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +spring.h2.console.enabled=true +spring.h2.console.settings.trace=false +spring.h2.console.settings.web-allow-others=false diff --git a/src/main/resources/img/api_latest.png b/src/main/resources/img/api_latest.png new file mode 100644 index 0000000..159e770 Binary files /dev/null and b/src/main/resources/img/api_latest.png differ diff --git a/src/main/resources/img/api_new_snippet.png b/src/main/resources/img/api_new_snippet.png new file mode 100644 index 0000000..8f965c2 Binary files /dev/null and b/src/main/resources/img/api_new_snippet.png differ diff --git a/src/main/resources/img/api_snippet.png b/src/main/resources/img/api_snippet.png new file mode 100644 index 0000000..0722ecc Binary files /dev/null and b/src/main/resources/img/api_snippet.png differ diff --git a/src/main/resources/img/index.png b/src/main/resources/img/index.png new file mode 100644 index 0000000..604de6d Binary files /dev/null and b/src/main/resources/img/index.png differ diff --git a/src/main/resources/img/latest.png b/src/main/resources/img/latest.png new file mode 100644 index 0000000..ce20752 Binary files /dev/null and b/src/main/resources/img/latest.png differ diff --git a/src/main/resources/img/new_snippet.png b/src/main/resources/img/new_snippet.png new file mode 100644 index 0000000..2f83f21 Binary files /dev/null and b/src/main/resources/img/new_snippet.png differ diff --git a/src/main/resources/img/snippet.png b/src/main/resources/img/snippet.png new file mode 100644 index 0000000..b80201b Binary files /dev/null and b/src/main/resources/img/snippet.png differ diff --git a/src/main/resources/templates/index.ftlh b/src/main/resources/templates/index.ftlh new file mode 100644 index 0000000..5937e82 --- /dev/null +++ b/src/main/resources/templates/index.ftlh @@ -0,0 +1,26 @@ + + + + Code sharing platform + + + + + + +
+

Code sharing platform

+

Web service to store syntax highlighted code snippets generating unique identifiers with the added possibility of configure its self-deletion based on time and views limitations.

+
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/latest.ftlh b/src/main/resources/templates/latest.ftlh new file mode 100644 index 0000000..fc043a4 --- /dev/null +++ b/src/main/resources/templates/latest.ftlh @@ -0,0 +1,27 @@ + + + + Latest + + + + + + + +

Latest snippets

+<#if codeSnippets??> + <#list codeSnippets as codeSnippet> +
+
+
+ ${codeSnippet.formattedDate} +
${codeSnippet.code}
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/new_snippet.ftlh b/src/main/resources/templates/new_snippet.ftlh new file mode 100644 index 0000000..c837258 --- /dev/null +++ b/src/main/resources/templates/new_snippet.ftlh @@ -0,0 +1,63 @@ + + + + + + Create + + +

New snippet

+
+
+
+
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ + +
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/snippet.ftlh b/src/main/resources/templates/snippet.ftlh new file mode 100644 index 0000000..1612974 --- /dev/null +++ b/src/main/resources/templates/snippet.ftlh @@ -0,0 +1,31 @@ + + + + Code + + + + + + + +
+
+
+ <#if codeSnippet??> + ${codeSnippet.formattedDate} +
${codeSnippet.code}
+ <#if (codeSnippet.isViewRestricted())> + ${codeSnippet.views} more views allowed +
+ + <#if (codeSnippet.isTimeRestricted())> + This code will be available for ${codeSnippet.timeAvailable} seconds + + +
+
+
+ + \ No newline at end of file diff --git a/src/test/java/com/droideparanoico/codesharingplatform/CodeSharingPlatformTest.java b/src/test/java/com/droideparanoico/codesharingplatform/CodeSharingPlatformTest.java new file mode 100644 index 0000000..d2eb135 --- /dev/null +++ b/src/test/java/com/droideparanoico/codesharingplatform/CodeSharingPlatformTest.java @@ -0,0 +1,12 @@ +package com.droideparanoico.codesharingplatform; + +import org.springframework.boot.test.context.SpringBootTest; +import org.junit.Test; + +@SpringBootTest +public class CodeSharingPlatformTest { + + @Test + public void contextLoads() { + } +}