diff --git a/pom.xml b/pom.xml index 6c34601c..8b415da1 100644 --- a/pom.xml +++ b/pom.xml @@ -2,8 +2,8 @@ 4.0.0 com.tpximpact - url_shortener_api - 1.0 + url_shortener + 1.0.0 org.springframework.boot @@ -16,17 +16,42 @@ - + org.springframework.boot - spring-boot-starter + spring-boot-starter-web - + org.springframework.boot - spring-boot-starter-web + spring-boot-starter-data-jpa - + + + com.h2database + h2 + runtime + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.projectlombok + lombok + true + + diff --git a/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java b/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java new file mode 100644 index 00000000..cdf3eb69 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java @@ -0,0 +1,13 @@ +package com.tpximpact.urlshortener; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +class UrlShortenerApplication { + + public static void main(String[] args) { + SpringApplication.run(UrlShortenerApplication.class, args); + } + +} diff --git a/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java b/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java new file mode 100644 index 00000000..a46cb266 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java @@ -0,0 +1,53 @@ +package com.tpximpact.urlshortener.controller; + +import com.tpximpact.urlshortener.dto.UrlListResponse; +import com.tpximpact.urlshortener.dto.UrlRequest; +import com.tpximpact.urlshortener.dto.UrlResponse; +import com.tpximpact.urlshortener.service.UrlService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import java.net.URI; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Slf4j +class UrlController { + private final UrlService urlService; + + @PostMapping("/shorten") + public ResponseEntity shorten(@RequestBody @Valid UrlRequest request) { + UrlResponse response = urlService.shorten(request); + return ResponseEntity.status(201).body(response); + } + + @GetMapping("/{alias}") + public ResponseEntity redirect(@PathVariable String alias) { + log.info("Contacting urlService to full url with: {}", alias); + String fullUrl = urlService.getFullUrl(alias); + log.info("Received Full url to redirect: {}", fullUrl); + return ResponseEntity.status(302) + .location(URI.create(fullUrl)) + .build(); + } + + @GetMapping("/urls") + public ResponseEntity> getAll() { + return ResponseEntity.ok(urlService.getAll()); + } + + @DeleteMapping("/{alias}") + public ResponseEntity delete(@PathVariable String alias) { + urlService.delete(alias); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java new file mode 100644 index 00000000..f61f25a6 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java @@ -0,0 +1,7 @@ +package com.tpximpact.urlshortener.dto; + +public record UrlListResponse( + String alias, + String fullUrl, + String shortUrl +) {} \ No newline at end of file diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java new file mode 100644 index 00000000..79bb75b4 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java @@ -0,0 +1,8 @@ +package com.tpximpact.urlshortener.dto; + +import jakarta.validation.constraints.NotBlank; + +public record UrlRequest ( + @NotBlank String fullUrl, + String customAlias +) {} diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java new file mode 100644 index 00000000..9c9d7d9e --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java @@ -0,0 +1,5 @@ +package com.tpximpact.urlshortener.dto; + +public record UrlResponse( + String shortUrl +) {} diff --git a/src/main/java/com/tpximpact/urlshortener/entity/Url.java b/src/main/java/com/tpximpact/urlshortener/entity/Url.java new file mode 100644 index 00000000..8dff4267 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/entity/Url.java @@ -0,0 +1,35 @@ +package com.tpximpact.urlshortener.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "urls", uniqueConstraints = { + @UniqueConstraint(columnNames = "alias") +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Url { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String fullUrl; + + @Column(nullable = false, unique = true) + private String alias; +} diff --git a/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java b/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java new file mode 100644 index 00000000..0490e90c --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java @@ -0,0 +1,5 @@ +package com.tpximpact.urlshortener.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { super(message); } +} diff --git a/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java b/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..01c41b00 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package com.tpximpact.urlshortener.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequest(BadRequestException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException ex) { + return ResponseEntity.status(404).body(ex.getMessage()); + } +} diff --git a/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java b/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java new file mode 100644 index 00000000..5042cfb3 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.tpximpact.urlshortener.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java b/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java new file mode 100644 index 00000000..877be95e --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java @@ -0,0 +1,15 @@ +package com.tpximpact.urlshortener.repository; + +import com.tpximpact.urlshortener.entity.Url; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UrlRepository extends JpaRepository { + + boolean existsByAlias(String alias); + + Optional findByAlias(String alias); +} diff --git a/src/main/java/com/tpximpact/urlshortener/service/UrlService.java b/src/main/java/com/tpximpact/urlshortener/service/UrlService.java new file mode 100644 index 00000000..7b948ed4 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/service/UrlService.java @@ -0,0 +1,18 @@ +package com.tpximpact.urlshortener.service; + +import com.tpximpact.urlshortener.dto.UrlListResponse; +import com.tpximpact.urlshortener.dto.UrlRequest; +import com.tpximpact.urlshortener.dto.UrlResponse; + +import java.util.List; + +public interface UrlService { + + UrlResponse shorten(UrlRequest request); + + String getFullUrl(String alias); + + List getAll(); + + void delete(String alias); +} diff --git a/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java b/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java new file mode 100644 index 00000000..c9efd817 --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java @@ -0,0 +1,71 @@ +package com.tpximpact.urlshortener.service; + +import com.tpximpact.urlshortener.dto.UrlListResponse; +import com.tpximpact.urlshortener.dto.UrlRequest; +import com.tpximpact.urlshortener.dto.UrlResponse; +import com.tpximpact.urlshortener.entity.Url; +import com.tpximpact.urlshortener.exception.BadRequestException; +import com.tpximpact.urlshortener.exception.NotFoundException; +import com.tpximpact.urlshortener.repository.UrlRepository; +import com.tpximpact.urlshortener.util.UrlUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UrlServiceImpl implements UrlService { + private static final String BASE_URL = "http://localhost:8080/"; + private final UrlRepository urlRepository; + + @Override + public UrlResponse shorten(UrlRequest request) { + + String alias = request.customAlias() != null + ? request.customAlias() : UrlUtils.generateAlias(); + + if (urlRepository.existsByAlias(alias)) { + throw new BadRequestException("Invalid input or alias already taken"); + } + + Url url = Url + .builder() + .fullUrl(request.fullUrl()) + .alias(alias) + .build(); + + urlRepository.save(url); + + return new UrlResponse(BASE_URL + alias); + } + + @Override + public String getFullUrl(String alias) { + log.info("Getting full url for alias ' {} ' from the DB", alias); + return urlRepository.findByAlias(alias) + .map(Url::getFullUrl) + .orElseThrow(() -> new NotFoundException("Alias not found")); + } + + @Override + public List getAll() { + return urlRepository.findAll().stream() + .map(url -> new UrlListResponse( + url.getAlias(), + url.getFullUrl(), + BASE_URL + url.getAlias() + )) + .toList(); + } + + public void delete(String alias) { + + Url url = urlRepository.findByAlias(alias) + .orElseThrow(() -> new NotFoundException("Alias not found")); + log.info("Deleting shorten url for alias '{}' from the DB", url.getAlias()); + urlRepository.delete(url); + } +} diff --git a/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java b/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java new file mode 100644 index 00000000..46e50cfe --- /dev/null +++ b/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java @@ -0,0 +1,13 @@ +package com.tpximpact.urlshortener.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; + +@Slf4j +public class UrlUtils { + + public static String generateAlias() { + return UUID.randomUUID().toString().substring(0, 6); + } +} diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 00000000..819ffce0 --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,16 @@ +spring: + datasource: + url: jdbc:h2:file:./data/mydb + driver-class-name: org.h2.Driver + username: sa + password: + + h2: + console: + enabled: true + path: /h2-console + + jpa: + show_sql: true + hibernate: + ddl-auto: update