diff --git a/.github/workflows/spotless.yaml b/.github/workflows/spotless.yaml new file mode 100644 index 0000000..b6d7aef --- /dev/null +++ b/.github/workflows/spotless.yaml @@ -0,0 +1,40 @@ +name: Code Formatting & Style Check + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + check-formatting: + name: Run spotless and checkstyle + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache gradle dependencies + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Change execute permission + run: chmod +x gradlew + + - name: Run spotless + run: ./gradlew spotlessCheck + + - name: Run checkstyle + run: ./gradlew checkstyleMain checkstyleTest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6992575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +.gradle +.vscode +*.iml +.editorconfig +build \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 650bdbb..e31ffa8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,10 +2,12 @@ plugins { java id("org.springframework.boot") version "3.4.3" id("io.spring.dependency-management") version "1.1.7" + id("checkstyle") + id("com.diffplug.spotless") version "6.25.0" } group = "com.m" -version = "0.0.1-SNAPSHOT" +version = "0.0.2" java { toolchain { @@ -19,6 +21,21 @@ configurations { } } +spotless { + java { + indentWithTabs(4) + indentWithSpaces(4) + endWithNewline() + trimTrailingWhitespace() + removeUnusedImports() + } +} + +checkstyle { + toolVersion = "10.15.0" + configFile = rootProject.file("config/checkstyle/checkstyle.xml") +} + repositories { mavenCentral() } @@ -34,6 +51,10 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.withType { - useJUnitPlatform() -} + +tasks.withType().configureEach { + reports { + html.required.set(true) + xml.required.set(false) + } +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..c1cc338 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..e18bc25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index faf9300..f3b75f3 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/src/main/java/com/m/linshor/LinShorApplication.java b/src/main/java/com/m/linshor/LinShorApplication.java index 7089c84..b18d48e 100644 --- a/src/main/java/com/m/linshor/LinShorApplication.java +++ b/src/main/java/com/m/linshor/LinShorApplication.java @@ -5,9 +5,7 @@ @SpringBootApplication public class LinShorApplication { - public static void main(String[] args) { SpringApplication.run(LinShorApplication.class, args); } - } diff --git a/src/main/java/com/m/linshor/controllers/LinShorController.java b/src/main/java/com/m/linshor/controllers/LinShorController.java new file mode 100644 index 0000000..c63e254 --- /dev/null +++ b/src/main/java/com/m/linshor/controllers/LinShorController.java @@ -0,0 +1,58 @@ +package com.m.linshor.controllers; + +import com.m.linshor.entities.Mapping; +import com.m.linshor.services.LinShorService; + +import java.net.URI; +import java.util.Optional; + +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/linshor/v1") +@AllArgsConstructor +public class LinShorController { + private final LinShorService linShorService; + + @GetMapping("/find/{shortUrl}") + public Optional findByShortUrl(@PathVariable String shortUrl) { + return linShorService.findByShortUrl(shortUrl); + } + + @PostMapping("post") + public Mapping saveLink(String longUrl) { + return linShorService.saveLink(longUrl); + } + + @GetMapping("/{id}") + public Optional findById(@PathVariable int id) { + return linShorService.findById(id); + } + + @PutMapping("update") + public Mapping updateLink(String longUrl) { + return linShorService.updateLink(longUrl); + } + + @DeleteMapping("delete/{id}") + public int deleteById(@PathVariable int id) { + linShorService.deleteById(id); + return 200; + } + + @PostMapping("/shortener") + public ResponseEntity linshorUrl(@RequestBody String longUrl) { + Mapping mapping = linShorService.saveLink(longUrl); + return ResponseEntity.ok(mapping.getShortUrl()); + } + + @GetMapping("/{shortUrl}") + public ResponseEntity redirectToLongUrl(@PathVariable String shortUrl) { + Optional mapping = linShorService.findByShortUrl(shortUrl); + return mapping + .map(m -> ResponseEntity.status(302).location(URI.create(m.getLongUrl())).build()) + .orElseGet(() -> ResponseEntity.notFound().build()); + } +} diff --git a/src/main/java/com/m/linshor/entities/Mapping.java b/src/main/java/com/m/linshor/entities/Mapping.java new file mode 100644 index 0000000..c9eeb3f --- /dev/null +++ b/src/main/java/com/m/linshor/entities/Mapping.java @@ -0,0 +1,21 @@ +package com.m.linshor.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +public class Mapping { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + int id; + + String longUrl; + String shortUrl; +} diff --git a/src/main/java/com/m/linshor/repositories/MappingDao.java b/src/main/java/com/m/linshor/repositories/MappingDao.java new file mode 100644 index 0000000..5d83eca --- /dev/null +++ b/src/main/java/com/m/linshor/repositories/MappingDao.java @@ -0,0 +1,61 @@ +package com.m.linshor.repositories; + +import com.m.linshor.entities.Mapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import org.springframework.stereotype.Repository; + +@Repository +public class MappingDao { + private final List LINKS = new ArrayList<>(); + + public Mapping saveLink(Mapping mapping) { + LINKS.add(mapping); + return null; + } + + public Optional findByShortUrl(String shortUrl) { + return LINKS.stream().filter(link -> link.getShortUrl().equals(shortUrl)).findFirst(); + } + + public Optional findById(int id) { + return LINKS.stream().filter(element -> element.getId() == id).findFirst(); + } + + public Mapping updateLink(Mapping mapping) { + var linkIndex = + IntStream.range(0, LINKS.size()) + .filter(index -> LINKS.get(index).getId() == mapping.getId()) + .findFirst() + .orElse(-1); + if (linkIndex > -1) { + LINKS.set(linkIndex, mapping); + return mapping; + } + return null; + } + + public void deleteById(int id) { + var link = findById(id); + if (link.isPresent()) { + LINKS.remove(link); + } + } + + public Mapping findByLongUrl(String longUrl) { + var linkIndex = + IntStream.range(0, LINKS.size()) + .filter(index -> LINKS.get(index).getLongUrl().equals(longUrl)) + .findFirst() + .orElse(-1); + if (linkIndex > -1) { + return LINKS.get(linkIndex); + } else { + return null; + } + } +} diff --git a/src/main/java/com/m/linshor/repositories/MappingRepository.java b/src/main/java/com/m/linshor/repositories/MappingRepository.java new file mode 100644 index 0000000..da1adf5 --- /dev/null +++ b/src/main/java/com/m/linshor/repositories/MappingRepository.java @@ -0,0 +1,17 @@ +package com.m.linshor.repositories; + +import com.m.linshor.entities.Mapping; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MappingRepository extends JpaRepository { + Optional findByShortUrl(String shortUrl); + + Optional findById(int id); + + Mapping findByLongUrl(String longUrl); +} diff --git a/src/main/java/com/m/linshor/services/InMemoryLinShorService.java b/src/main/java/com/m/linshor/services/InMemoryLinShorService.java new file mode 100644 index 0000000..a5b49fd --- /dev/null +++ b/src/main/java/com/m/linshor/services/InMemoryLinShorService.java @@ -0,0 +1,70 @@ +package com.m.linshor.services; + +import com.m.linshor.entities.Mapping; +import com.m.linshor.repositories.MappingDao; + +import java.util.Optional; +import java.util.Random; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class InMemoryLinShorService implements LinShorService { + private MappingDao repository; + private static final String BASE62 = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int SHORT_URL_LENGTH = 10; + + @Override + public Optional findByShortUrl(String shortUrl) { + return repository.findByShortUrl(shortUrl); + } + + @Override + public Mapping saveLink(String longUrl) { + String shortUrl; + do { + shortUrl = generateShor(); + } while (repository.findByShortUrl(shortUrl).isPresent()); + + Mapping urlMapping = new Mapping(); + urlMapping.setShortUrl(shortUrl); + urlMapping.setLongUrl(longUrl); + return repository.saveLink(urlMapping); + } + + @Override + public Optional findById(int id) { + return repository.findById(id); + } + + @Override + public Mapping updateLink(String longUrl) { + Mapping mapping = findByLongUrl(longUrl); + String shortUrl = generateShor(); + + mapping.setShortUrl(shortUrl); + return repository.updateLink(mapping); + } + + @Override + public void deleteById(int id) { + repository.deleteById(id); + } + + private String generateShor() { + Random random = new Random(); + StringBuilder sb = new StringBuilder(SHORT_URL_LENGTH); + for (int i = 0; i < SHORT_URL_LENGTH; i++) { + sb.append(BASE62.charAt(random.nextInt(BASE62.length()))); + } + return sb.toString(); + } + + @Override + public Mapping findByLongUrl(String longUrl) { + return repository.findByLongUrl(longUrl); + } +} diff --git a/src/main/java/com/m/linshor/services/LinShorService.java b/src/main/java/com/m/linshor/services/LinShorService.java new file mode 100644 index 0000000..c36f527 --- /dev/null +++ b/src/main/java/com/m/linshor/services/LinShorService.java @@ -0,0 +1,23 @@ +package com.m.linshor.services; + +import com.m.linshor.entities.Mapping; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +@Service +public interface LinShorService { + + Optional findByShortUrl(String shortUrl); + + Mapping saveLink(String longUrl); + + Optional findById(int id); + + Mapping updateLink(String longUrl); + + void deleteById(int id); + + Mapping findByLongUrl(String longUrl); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c8bc057..9871d24 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ spring.application.name=LinShor +spring.datasource.url=jdbc:postgresql://localhost:5432/template1 +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.datasource.driver-class-name=org.postgresql.Driver + diff --git a/src/test/java/com/m/linshor/LinShorApplicationTests.java b/src/test/java/com/m/linshor/LinShorApplicationTests.java index 56f0680..5b1f647 100644 --- a/src/test/java/com/m/linshor/LinShorApplicationTests.java +++ b/src/test/java/com/m/linshor/LinShorApplicationTests.java @@ -5,9 +5,8 @@ @SpringBootTest class LinShorApplicationTests { - @Test void contextLoads() { - } + } }