From aab6640281aaa3f83c4621117825da9e75915850 Mon Sep 17 00:00:00 2001 From: Shubham Varshney Date: Tue, 6 Feb 2024 13:26:47 +0530 Subject: [PATCH] af --- .dockerignore | 5 +- infra/{ => app}/.terraform.lock.hcl | 0 infra/{ => app}/LICENSE | 0 infra/{ => app}/README.md | 0 infra/app/main.tf | 16 ++ .../distribution/documentssstore.tf | 0 .../aws/cloudfront/distribution/main.tf | 0 .../aws/cloudfront/distribution/outputs.tf | 0 .../aws/cloudfront/distribution/variables.tf | 0 .../modules/aws/s3/buckets/documentssstore.tf | 0 .../{ => app}/modules/aws/s3/buckets/main.tf | 0 .../modules/aws/s3/buckets/outputs.tf | 0 .../modules/aws/s3/buckets/variables.tf | 0 infra/{ => app}/outputs.tf | 0 infra/{ => app}/provider.tf | 0 infra/app/variables.tf | 24 +++ infra/{terraform.tf => app/versions.tf} | 0 infra/main.tf | 7 - infra/pipeline/.terraform.lock.hcl | 65 ++++++++ infra/pipeline/LICENSE | 0 infra/pipeline/README.md | 0 infra/pipeline/main.tf | 12 ++ infra/pipeline/modules/aws/s3/buckets/main.tf | 0 .../modules/aws/s3/buckets/outputs.tf | 4 + .../aws/s3/buckets/private-code-artifacts.tf | 10 ++ .../modules/aws/s3/buckets}/variables.tf | 3 - infra/pipeline/outputs.tf | 4 + infra/pipeline/pipeline.tf | 77 +++++++++ infra/pipeline/provider.tf | 7 + infra/pipeline/role.tf | 45 ++++++ infra/pipeline/terraform.tfstate.backup | 146 ++++++++++++++++++ infra/pipeline/variables.tf | 131 ++++++++++++++++ infra/pipeline/versions.tf | 28 ++++ .../contexts/AccountIDContextHolder.java | 4 + .../BaseAbstractDistributedIdEntity.java | 1 + .../exceptions/InvalidRequestException.java | 20 +-- .../commons/exceptions/NotFoundException.java | 30 ++++ .../exceptions/builders/ErrorMessages.java | 87 +++++++++++ .../RestResponseEntityExceptionHandler.java | 7 + .../shubham/commons/utils/ResponseUtils.java | 16 +- .../dao/entities/AvailableKey.java | 26 ++++ .../keygeneration/dao/entities/UsedKey.java | 24 +++ .../repositories/AvailableKeyRepository.java | 17 ++ .../dao/repositories/UsedKeyRepository.java | 10 ++ .../services/KeyGenerationService.java | 46 ++++++ .../services/facades/KeyGenerationFacade.java | 67 ++++++++ .../KeyGenerationClient.java | 46 ++++++ .../configurations/DependencyBeans.java | 26 ++++ .../service/IKeyGenerationService.java | 7 + .../strategies/HashedKeyGenerateStrategy.java | 37 +++++ .../strategies/IKeyGenerateStrategy.java | 7 + .../RandomCharacterKeyGenerateStrategy.java | 20 +++ .../core/tinyurl/dao/entities/ShortURL.java | 44 ++++++ .../dao/repositories/ShortUrlRepository.java | 17 ++ .../core/tinyurl/services/TinyURLService.java | 68 ++++++++ .../strategies/KGSKeyGenerationStrategy.java | 21 +++ .../web/v1/controllers/TinyURLController.java | 58 +++++++ ...3_create_procedure_update_avaibale_key.sql | 8 + 58 files changed, 1261 insertions(+), 37 deletions(-) rename infra/{ => app}/.terraform.lock.hcl (100%) rename infra/{ => app}/LICENSE (100%) rename infra/{ => app}/README.md (100%) create mode 100644 infra/app/main.tf rename infra/{ => app}/modules/aws/cloudfront/distribution/documentssstore.tf (100%) rename infra/{ => app}/modules/aws/cloudfront/distribution/main.tf (100%) rename infra/{ => app}/modules/aws/cloudfront/distribution/outputs.tf (100%) rename infra/{ => app}/modules/aws/cloudfront/distribution/variables.tf (100%) rename infra/{ => app}/modules/aws/s3/buckets/documentssstore.tf (100%) rename infra/{ => app}/modules/aws/s3/buckets/main.tf (100%) rename infra/{ => app}/modules/aws/s3/buckets/outputs.tf (100%) rename infra/{ => app}/modules/aws/s3/buckets/variables.tf (100%) rename infra/{ => app}/outputs.tf (100%) rename infra/{ => app}/provider.tf (100%) create mode 100644 infra/app/variables.tf rename infra/{terraform.tf => app/versions.tf} (100%) delete mode 100644 infra/main.tf create mode 100644 infra/pipeline/.terraform.lock.hcl create mode 100644 infra/pipeline/LICENSE create mode 100644 infra/pipeline/README.md create mode 100644 infra/pipeline/main.tf create mode 100644 infra/pipeline/modules/aws/s3/buckets/main.tf create mode 100644 infra/pipeline/modules/aws/s3/buckets/outputs.tf create mode 100644 infra/pipeline/modules/aws/s3/buckets/private-code-artifacts.tf rename infra/{ => pipeline/modules/aws/s3/buckets}/variables.tf (99%) create mode 100644 infra/pipeline/outputs.tf create mode 100644 infra/pipeline/pipeline.tf create mode 100644 infra/pipeline/provider.tf create mode 100644 infra/pipeline/role.tf create mode 100644 infra/pipeline/terraform.tfstate.backup create mode 100644 infra/pipeline/variables.tf create mode 100644 infra/pipeline/versions.tf create mode 100644 src/main/java/code/shubham/commons/exceptions/NotFoundException.java create mode 100644 src/main/java/code/shubham/commons/exceptions/builders/ErrorMessages.java create mode 100644 src/main/java/code/shubham/core/keygeneration/dao/entities/AvailableKey.java create mode 100644 src/main/java/code/shubham/core/keygeneration/dao/entities/UsedKey.java create mode 100644 src/main/java/code/shubham/core/keygeneration/dao/repositories/AvailableKeyRepository.java create mode 100644 src/main/java/code/shubham/core/keygeneration/dao/repositories/UsedKeyRepository.java create mode 100644 src/main/java/code/shubham/core/keygeneration/services/KeyGenerationService.java create mode 100644 src/main/java/code/shubham/core/keygeneration/services/facades/KeyGenerationFacade.java create mode 100644 src/main/java/code/shubham/core/keygenerationclient/KeyGenerationClient.java create mode 100644 src/main/java/code/shubham/core/keygenerationcommons/configurations/DependencyBeans.java create mode 100644 src/main/java/code/shubham/core/keygenerationcommons/service/IKeyGenerationService.java create mode 100644 src/main/java/code/shubham/core/keygenerationcommons/strategies/HashedKeyGenerateStrategy.java create mode 100644 src/main/java/code/shubham/core/keygenerationcommons/strategies/IKeyGenerateStrategy.java create mode 100644 src/main/java/code/shubham/core/keygenerationcommons/strategies/RandomCharacterKeyGenerateStrategy.java create mode 100644 src/main/java/code/shubham/core/tinyurl/dao/entities/ShortURL.java create mode 100644 src/main/java/code/shubham/core/tinyurl/dao/repositories/ShortUrlRepository.java create mode 100644 src/main/java/code/shubham/core/tinyurl/services/TinyURLService.java create mode 100644 src/main/java/code/shubham/core/tinyurl/strategies/KGSKeyGenerationStrategy.java create mode 100644 src/main/java/code/shubham/core/tinyurl/web/v1/controllers/TinyURLController.java create mode 100644 src/main/resources/db/migration/V3__00003_create_procedure_update_avaibale_key.sql diff --git a/.dockerignore b/.dockerignore index 46fe836..d263c76 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,17 +2,14 @@ *.iml *.iws .target/ -target/ .sonar/ .sonarlint/ .github/ .gradle/ -.build/ -build/ fluentbit/ grafana/ -infra/ +infra/app/ k8s/ log/ logs/ diff --git a/infra/.terraform.lock.hcl b/infra/app/.terraform.lock.hcl similarity index 100% rename from infra/.terraform.lock.hcl rename to infra/app/.terraform.lock.hcl diff --git a/infra/LICENSE b/infra/app/LICENSE similarity index 100% rename from infra/LICENSE rename to infra/app/LICENSE diff --git a/infra/README.md b/infra/app/README.md similarity index 100% rename from infra/README.md rename to infra/app/README.md diff --git a/infra/app/main.tf b/infra/app/main.tf new file mode 100644 index 0000000..255c85e --- /dev/null +++ b/infra/app/main.tf @@ -0,0 +1,16 @@ +locals { + tags = { + Project = var.project_name + CreatedBy = var.createdBy + CreatedOn = timestamp() + Environment = terraform.workspace + } +} + +module "app_aws_s3_buckets" { + source = "./modules/aws/s3/buckets" +} + +#module "project_aws_cloudfront_distribution" { +# source = "./modules/aws/cloudfront/distribution" +#} diff --git a/infra/modules/aws/cloudfront/distribution/documentssstore.tf b/infra/app/modules/aws/cloudfront/distribution/documentssstore.tf similarity index 100% rename from infra/modules/aws/cloudfront/distribution/documentssstore.tf rename to infra/app/modules/aws/cloudfront/distribution/documentssstore.tf diff --git a/infra/modules/aws/cloudfront/distribution/main.tf b/infra/app/modules/aws/cloudfront/distribution/main.tf similarity index 100% rename from infra/modules/aws/cloudfront/distribution/main.tf rename to infra/app/modules/aws/cloudfront/distribution/main.tf diff --git a/infra/modules/aws/cloudfront/distribution/outputs.tf b/infra/app/modules/aws/cloudfront/distribution/outputs.tf similarity index 100% rename from infra/modules/aws/cloudfront/distribution/outputs.tf rename to infra/app/modules/aws/cloudfront/distribution/outputs.tf diff --git a/infra/modules/aws/cloudfront/distribution/variables.tf b/infra/app/modules/aws/cloudfront/distribution/variables.tf similarity index 100% rename from infra/modules/aws/cloudfront/distribution/variables.tf rename to infra/app/modules/aws/cloudfront/distribution/variables.tf diff --git a/infra/modules/aws/s3/buckets/documentssstore.tf b/infra/app/modules/aws/s3/buckets/documentssstore.tf similarity index 100% rename from infra/modules/aws/s3/buckets/documentssstore.tf rename to infra/app/modules/aws/s3/buckets/documentssstore.tf diff --git a/infra/modules/aws/s3/buckets/main.tf b/infra/app/modules/aws/s3/buckets/main.tf similarity index 100% rename from infra/modules/aws/s3/buckets/main.tf rename to infra/app/modules/aws/s3/buckets/main.tf diff --git a/infra/modules/aws/s3/buckets/outputs.tf b/infra/app/modules/aws/s3/buckets/outputs.tf similarity index 100% rename from infra/modules/aws/s3/buckets/outputs.tf rename to infra/app/modules/aws/s3/buckets/outputs.tf diff --git a/infra/modules/aws/s3/buckets/variables.tf b/infra/app/modules/aws/s3/buckets/variables.tf similarity index 100% rename from infra/modules/aws/s3/buckets/variables.tf rename to infra/app/modules/aws/s3/buckets/variables.tf diff --git a/infra/outputs.tf b/infra/app/outputs.tf similarity index 100% rename from infra/outputs.tf rename to infra/app/outputs.tf diff --git a/infra/provider.tf b/infra/app/provider.tf similarity index 100% rename from infra/provider.tf rename to infra/app/provider.tf diff --git a/infra/app/variables.tf b/infra/app/variables.tf new file mode 100644 index 0000000..7c3a7bb --- /dev/null +++ b/infra/app/variables.tf @@ -0,0 +1,24 @@ +variable "region" { + type = string + description = "AWS region for all resources." + + default = "ap-south-1" +} + +variable "project_name" { + type = string + description = "Template service in java, spring-boot project." + default = "template-service-java-spring-boot" +} + +variable "environment" { + type = string + description = "environment" + default = "test" +} + +variable "createdBy" { + type = string + description = "createdBy" + default = "terraform" +} diff --git a/infra/terraform.tf b/infra/app/versions.tf similarity index 100% rename from infra/terraform.tf rename to infra/app/versions.tf diff --git a/infra/main.tf b/infra/main.tf deleted file mode 100644 index 0ecc837..0000000 --- a/infra/main.tf +++ /dev/null @@ -1,7 +0,0 @@ -module "project_aws_s3_buckets" { - source = "./modules/aws/s3/buckets" -} - -#module "project_aws_cloudfront_distribution" { -# source = "./modules/aws/cloudfront/distribution" -#} diff --git a/infra/pipeline/.terraform.lock.hcl b/infra/pipeline/.terraform.lock.hcl new file mode 100644 index 0000000..1ce1f6a --- /dev/null +++ b/infra/pipeline/.terraform.lock.hcl @@ -0,0 +1,65 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.7.0" + constraints = "5.7.0" + hashes = [ + "h1:A7p0npQ+UHlnapVuikOzhmgchAq8agtfZGkYiiEOnp0=", + "zh:03240d7fc041d5331db7fd5f2ca4fe031321d07d2a6ca27085c5020dae13f211", + "zh:0b5252b14c354636fe0348823195dd901b457de1a033015f4a7d11cfe998c766", + "zh:2bfb62325b0487be8d1850a964f09cca0d45148faec577459c2a24334ec9977b", + "zh:2f9e317ffc57d2b5117cfe8dc266f88aa139b760bc93d8adeed7ad533a78b5a3", + "zh:36512725c9d7c559927b98fead04be58494a3a997e5270b905a75a468e307427", + "zh:5483e696d3ea764f746d3fe439f7dcc49001c3c774122d7baa51ce01011f0075", + "zh:5967635cc14f969ea26622863a2e3f9d6a7ddd3e7d35a29a7275c5e10579ac8c", + "zh:7e63c94a64af5b7aeb36ea6e3719962f65a7c28074532c02549a67212d410bb8", + "zh:8a7d5f33b11a3f5c7281413b431fa85de149ed8493ec1eea73d50d2d80a475e6", + "zh:8e2ed2d986aaf590975a79a2f6b5e60e0dc7d804ab01a8c03ab181e41cfe9b0f", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c7b8ca1b17489f16a6d0f1fc2aa9c130978ea74c9c861d8435410567a0a888f", + "zh:a54385896a70524063f0c5420be26ff6f88909bd8e6902dd3e922577b21fd546", + "zh:aecd3a8fb70b938b58d93459bfb311540fd6aaf981924bf34abd48f953b4be0d", + "zh:f3de076fa3402768d27af0187c6a677777b47691d1f0f84c9b259ff66e65953e", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + constraints = "3.5.1" + hashes = [ + "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", + "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", + "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", + "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", + "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", + "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", + "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", + "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", + "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", + "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", + "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.9.1" + constraints = "0.9.1" + hashes = [ + "h1:NUv/YtEytDQncBQ2mTxnUZEy/rmDlPYmE9h2iokR0vk=", + "zh:00a1476ecf18c735cc08e27bfa835c33f8ac8fa6fa746b01cd3bcbad8ca84f7f", + "zh:3007f8fc4a4f8614c43e8ef1d4b0c773a5de1dcac50e701d8abc9fdc8fcb6bf5", + "zh:5f79d0730fdec8cb148b277de3f00485eff3e9cf1ff47fb715b1c969e5bbd9d4", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8c8094689a2bed4bb597d24a418bbbf846e15507f08be447d0a5acea67c2265a", + "zh:a6d9206e95d5681229429b406bc7a9ba4b2d9b67470bda7df88fa161508ace57", + "zh:aa299ec058f23ebe68976c7581017de50da6204883950de228ed9246f309e7f1", + "zh:b129f00f45fba1991db0aa954a6ba48d90f64a738629119bfb8e9a844b66e80b", + "zh:ef6cecf5f50cda971c1b215847938ced4cb4a30a18095509c068643b14030b00", + "zh:f1f46a4f6c65886d2dd27b66d92632232adc64f92145bf8403fe64d5ffa5caea", + "zh:f79d6155cda7d559c60d74883a24879a01c4d5f6fd7e8d1e3250f3cd215fb904", + "zh:fd59fa73074805c3575f08cd627eef7acda14ab6dac2c135a66e7a38d262201c", + ] +} diff --git a/infra/pipeline/LICENSE b/infra/pipeline/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/infra/pipeline/README.md b/infra/pipeline/README.md new file mode 100644 index 0000000..e69de29 diff --git a/infra/pipeline/main.tf b/infra/pipeline/main.tf new file mode 100644 index 0000000..47daef9 --- /dev/null +++ b/infra/pipeline/main.tf @@ -0,0 +1,12 @@ +locals { + tags = { + Project = var.project_name + CreatedBy = var.createdBy + CreatedOn = timestamp() + Environment = terraform.workspace + } +} + +module "pipeline_aws_s3_buckets" { + source = "./modules/aws/s3/buckets" +} \ No newline at end of file diff --git a/infra/pipeline/modules/aws/s3/buckets/main.tf b/infra/pipeline/modules/aws/s3/buckets/main.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/pipeline/modules/aws/s3/buckets/outputs.tf b/infra/pipeline/modules/aws/s3/buckets/outputs.tf new file mode 100644 index 0000000..d5751da --- /dev/null +++ b/infra/pipeline/modules/aws/s3/buckets/outputs.tf @@ -0,0 +1,4 @@ +output "aws_code_artifact_s3_bucket_id" { + value = aws_s3_bucket.private-code-artifacts.id + description = "aws_code_artifact_s3_bucket_id" +} \ No newline at end of file diff --git a/infra/pipeline/modules/aws/s3/buckets/private-code-artifacts.tf b/infra/pipeline/modules/aws/s3/buckets/private-code-artifacts.tf new file mode 100644 index 0000000..efe10ee --- /dev/null +++ b/infra/pipeline/modules/aws/s3/buckets/private-code-artifacts.tf @@ -0,0 +1,10 @@ +resource "aws_s3_bucket" "private-code-artifacts" { + bucket = "private-code-artifacts" + + tags = { + Name = "private-code-artifacts" + Owner = "${var.project_name}" + Environment = "${var.environment}" + Region = "${var.region}" + } +} \ No newline at end of file diff --git a/infra/variables.tf b/infra/pipeline/modules/aws/s3/buckets/variables.tf similarity index 99% rename from infra/variables.tf rename to infra/pipeline/modules/aws/s3/buckets/variables.tf index 00f14d7..57268e1 100644 --- a/infra/variables.tf +++ b/infra/pipeline/modules/aws/s3/buckets/variables.tf @@ -1,20 +1,17 @@ variable "region" { type = string description = "AWS region for all resources." - default = "ap-south-1" } variable "project_name" { type = string description = "Template service in java, spring-boot project." - default = "template-service-java-spring-boot" } variable "environment" { type = string description = "Template service in java, spring-boot project." - default = "test" } diff --git a/infra/pipeline/outputs.tf b/infra/pipeline/outputs.tf new file mode 100644 index 0000000..e043529 --- /dev/null +++ b/infra/pipeline/outputs.tf @@ -0,0 +1,4 @@ +output "aws_codepipeline_arn" { + value = aws_codepipeline.this.arn + description = "aws codepipeline project arn" +} \ No newline at end of file diff --git a/infra/pipeline/pipeline.tf b/infra/pipeline/pipeline.tf new file mode 100644 index 0000000..dd588c6 --- /dev/null +++ b/infra/pipeline/pipeline.tf @@ -0,0 +1,77 @@ +resource "aws_codepipeline" "this" { + + name = var.project_name + role_arn = aws_iam_role.this.arn + + artifact_store { + type = var.artifacts_store_type + location = module.pipeline_aws_s3_buckets.aws_code_artifact_s3_bucket_id + } + + stage { + name = "Source" + action { + name = "Source" + category = "Source" + owner = "AWS" + provider = var.source_provider + version = "1" + output_artifacts = [var.output_artifacts] + configuration = { + FullRepositoryId = var.full_repository_id + BranchName = var.branch_name + ConnectionArn = var.codestar_connector_credentials + OutputArtifactFormat = var.output_artifact_format + } + } + } + + stage { + name = "Apply" #"Plan" + action { + name = "Build" + category = "Build" + provider = "CodeBuild" + version = "1" + owner = "AWS" + input_artifacts = [var.input_artifacts] + configuration = { + ProjectName = var.project_name + } + } + } + + # stage { + # name = "Approve" + + # action { + # name = "Approval" + # category = "Approval" + # owner = "AWS" + # provider = "Manual" + # version = "1" + # input_artifacts = [var.input_artifacts] + # configuration = { + # #NotificationArn = var.approve_sns_arn + # CustomData = var.approve_comment + # #ExternalEntityLink = var.approve_url + # } + # } + # } + + # stage { + # name = "Deploy" + # action { + # name = "Deploy" + # category = "Build" + # provider = "CodeBuild" + # version = "1" + # owner = "AWS" + # input_artifacts = [var.input_artifacts] + # configuration = { + # ProjectName = var.project_name + # } + # } + # } + +} \ No newline at end of file diff --git a/infra/pipeline/provider.tf b/infra/pipeline/provider.tf new file mode 100644 index 0000000..daa3464 --- /dev/null +++ b/infra/pipeline/provider.tf @@ -0,0 +1,7 @@ +provider "aws" { + region = var.region +} + +provider "random" {} + +provider "time" {} \ No newline at end of file diff --git a/infra/pipeline/role.tf b/infra/pipeline/role.tf new file mode 100644 index 0000000..26ed452 --- /dev/null +++ b/infra/pipeline/role.tf @@ -0,0 +1,45 @@ +resource "aws_iam_role" "this" { + name = var.role_name + + assume_role_policy = <> getErrorMessages() { @Override public String toString() { - StringBuilder builder = new StringBuilder("[\n"); - this.errorMessagesList.stream().map(errorMessagesMap -> { - StringBuilder mapBBuilder = new StringBuilder("{\n"); - errorMessagesMap.forEach((k, v) -> { - mapBBuilder.append("\t").append('"').append(k).append('"').append(": ").append("[\n"); - v.forEach(e -> mapBBuilder.append("\t\t").append('"').append(e).append('"').append(",\n")); - mapBBuilder.replace(mapBBuilder.lastIndexOf(","), mapBBuilder.lastIndexOf(",") + 1, ""); - mapBBuilder.append('\t').append("]\n"); - if ("]".equals(mapBBuilder.charAt(mapBBuilder.length() - 1))) { - mapBBuilder.append(','); - } - mapBBuilder.append('\n'); - }); - mapBBuilder.append("},"); - return mapBBuilder; - }).forEach(builder::append); - builder.append("]\n"); - return builder.toString(); + return ErrorMessages.formulate(this.errorMessagesList); } public void tryThrow() { diff --git a/src/main/java/code/shubham/commons/exceptions/NotFoundException.java b/src/main/java/code/shubham/commons/exceptions/NotFoundException.java new file mode 100644 index 0000000..5bcaffe --- /dev/null +++ b/src/main/java/code/shubham/commons/exceptions/NotFoundException.java @@ -0,0 +1,30 @@ +package code.shubham.commons.exceptions; + +import code.shubham.commons.exceptions.builders.ErrorMessages; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.Optional; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ResponseStatus(HttpStatus.NOT_FOUND) +public class NotFoundException extends RuntimeException { + + private ErrorMessages errorMessages; + + public NotFoundException(final String key, final String value, final String... placeholderValues) { + this.errorMessages = ErrorMessages.builder().key(key).value(value, placeholderValues); + } + + @Override + public String getMessage() { + return Optional.ofNullable(this.errorMessages).map(ErrorMessages::toString).orElse(""); + } + +} diff --git a/src/main/java/code/shubham/commons/exceptions/builders/ErrorMessages.java b/src/main/java/code/shubham/commons/exceptions/builders/ErrorMessages.java new file mode 100644 index 0000000..aec4e39 --- /dev/null +++ b/src/main/java/code/shubham/commons/exceptions/builders/ErrorMessages.java @@ -0,0 +1,87 @@ +package code.shubham.commons.exceptions.builders; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class ErrorMessages { + + private String key; + + private Collection values; + + private Map> errorMessages; + + private final Collection>> errorMessagesList; + + private ErrorMessages() { + this.key = null; + this.values = new ArrayList<>(); + this.errorMessages = new HashMap<>(); + this.errorMessagesList = new ArrayList<>(); + this.errorMessagesList.add(this.errorMessages); + } + + public static ErrorMessages builder() { + return new ErrorMessages(); + } + + public ErrorMessages key(final String key) { + this.errorMessages.remove(this.key); + + this.key = key; + this.errorMessages.put(this.key, values); + return this; + } + + public ErrorMessages value(final String value, final String... placeholderValues) { + this.values.add(String.format(value, placeholderValues)); + return this; + } + + public ErrorMessages and() { + this.key = null; + this.values = new ArrayList<>(); + return this; + } + + public ErrorMessages next() { + this.errorMessages = new HashMap<>(); + this.errorMessagesList.add(this.errorMessages); + return this; + } + + public Collection>> build() { + return this.errorMessagesList; + } + + @Override + public String toString() { + return formulate(this.errorMessagesList); + } + + public static String formulate(final Collection>> messages) { + final StringBuilder builder = new StringBuilder("[\n"); + messages.stream().map(errorMessagesMap -> { + StringBuilder mapBBuilder = new StringBuilder("{\n"); + errorMessagesMap.forEach((k, v) -> { + mapBBuilder.append("\t").append('"').append(k).append('"').append(": ").append("[\n"); + v.forEach(e -> mapBBuilder.append("\t\t").append('"').append(e).append('"').append(",\n")); + mapBBuilder.replace(mapBBuilder.lastIndexOf(","), mapBBuilder.lastIndexOf(",") + 1, ""); + mapBBuilder.append('\t').append("]\n"); + + if ("]".equals(mapBBuilder.charAt(mapBBuilder.length() - 1))) + mapBBuilder.append(','); + + mapBBuilder.append('\n'); + }); + mapBBuilder.append("},"); + return mapBBuilder; + }).forEach(builder::append); + builder.append("]\n"); + + return builder.toString(); + } + +} diff --git a/src/main/java/code/shubham/commons/exceptions/handlers/RestResponseEntityExceptionHandler.java b/src/main/java/code/shubham/commons/exceptions/handlers/RestResponseEntityExceptionHandler.java index 528e8bf..149009b 100644 --- a/src/main/java/code/shubham/commons/exceptions/handlers/RestResponseEntityExceptionHandler.java +++ b/src/main/java/code/shubham/commons/exceptions/handlers/RestResponseEntityExceptionHandler.java @@ -3,6 +3,7 @@ import code.shubham.commons.exceptions.BlobStoreException; import code.shubham.commons.exceptions.InternalServerException; import code.shubham.commons.exceptions.InvalidRequestException; +import code.shubham.commons.exceptions.NotFoundException; import code.shubham.commons.exceptions.ServiceInvocationClientException; import code.shubham.commons.exceptions.UnauthorizedException; import code.shubham.commons.utils.ResponseUtils; @@ -42,6 +43,12 @@ public ResponseEntity handleInvalidRequestException(final InvalidRequestExcep return ResponseUtils.getErrorResponseEntity(HttpStatus.BAD_REQUEST.value(), exception.getOriginalErrors()); } + @ExceptionHandler({ NotFoundException.class }) + public ResponseEntity handleNotFoundException(final NotFoundException exception, final WebRequest request) { + log.debug(exception.getMessage(), exception); + return ResponseUtils.getErrorResponseEntity(HttpStatus.NOT_FOUND.value(), exception.getErrorMessages()); + } + @ExceptionHandler({ ServiceInvocationClientException.class }) public ResponseEntity handleServiceInvocationClientException(final InvalidRequestException exception, final WebRequest request) { diff --git a/src/main/java/code/shubham/commons/utils/ResponseUtils.java b/src/main/java/code/shubham/commons/utils/ResponseUtils.java index 8ee7769..14b96c9 100644 --- a/src/main/java/code/shubham/commons/utils/ResponseUtils.java +++ b/src/main/java/code/shubham/commons/utils/ResponseUtils.java @@ -1,6 +1,7 @@ package code.shubham.commons.utils; import code.shubham.commons.models.ServiceResponse; +import lombok.SneakyThrows; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,14 +16,14 @@ public class ResponseUtils { public static ResponseEntity getOK() { - return ResponseEntity.status(org.springframework.http.HttpStatus.OK).build(); + return ResponseEntity.status(HttpStatus.OK).build(); } public static ResponseEntity getErrorResponseEntity(int statusCode, Object errors) { return getResponseEntity(statusCode, null, errors); } - public static ResponseEntity getResponseEntity(org.springframework.http.HttpStatus status) { + public static ResponseEntity getResponseEntity(HttpStatus status) { return getResponseEntity(status.value(), null, null); } @@ -30,7 +31,7 @@ public static ResponseEntity getResponseEntity(int statusCode) { return getResponseEntity(statusCode, null, null); } - public static ResponseEntity getDataResponseEntity(org.springframework.http.HttpStatus status, Object data) { + public static ResponseEntity getDataResponseEntity(HttpStatus status, Object data) { return getResponseEntity(status.value(), data, null); } @@ -75,9 +76,14 @@ public static ResponseEntity getResponseEntity(final int statusCode, final Ob return ResponseEntity.status(statusCode).headers(httpHeaders).body(response); } - public static ResponseEntity redirect(final String redirectURI) throws URISyntaxException { + public static ResponseEntity redirect(final String redirectURI) { final HttpHeaders headers = new HttpHeaders(); - headers.setLocation(new URI(redirectURI)); + try { + headers.setLocation(new URI(redirectURI)); + } + catch (URISyntaxException e) { + throw new RuntimeException(e); + } return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).headers(headers).build(); } diff --git a/src/main/java/code/shubham/core/keygeneration/dao/entities/AvailableKey.java b/src/main/java/code/shubham/core/keygeneration/dao/entities/AvailableKey.java new file mode 100644 index 0000000..f5d2b88 --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/dao/entities/AvailableKey.java @@ -0,0 +1,26 @@ +package code.shubham.core.keygeneration.dao.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "available_keys") +public class AvailableKey { + + @Id + @Column(unique = true, nullable = false, length = 8) + private String name; + + private Boolean isSelected; + +} diff --git a/src/main/java/code/shubham/core/keygeneration/dao/entities/UsedKey.java b/src/main/java/code/shubham/core/keygeneration/dao/entities/UsedKey.java new file mode 100644 index 0000000..ac6df41 --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/dao/entities/UsedKey.java @@ -0,0 +1,24 @@ +package code.shubham.core.keygeneration.dao.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "used_keys") +public class UsedKey { + + @Id + @Column(unique = true, nullable = false, length = 8) + private String name; + +} diff --git a/src/main/java/code/shubham/core/keygeneration/dao/repositories/AvailableKeyRepository.java b/src/main/java/code/shubham/core/keygeneration/dao/repositories/AvailableKeyRepository.java new file mode 100644 index 0000000..491dca0 --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/dao/repositories/AvailableKeyRepository.java @@ -0,0 +1,17 @@ +package code.shubham.core.keygeneration.dao.repositories; + +import code.shubham.core.keygeneration.dao.entities.AvailableKey; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AvailableKeyRepository extends JpaRepository { + + @Procedure("POLL_ANY_AVAILABLE_KEY") + Optional findAny(); + +} diff --git a/src/main/java/code/shubham/core/keygeneration/dao/repositories/UsedKeyRepository.java b/src/main/java/code/shubham/core/keygeneration/dao/repositories/UsedKeyRepository.java new file mode 100644 index 0000000..e4bcdc5 --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/dao/repositories/UsedKeyRepository.java @@ -0,0 +1,10 @@ +package code.shubham.core.keygeneration.dao.repositories; + +import code.shubham.core.keygeneration.dao.entities.UsedKey; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UsedKeyRepository extends JpaRepository { + +} diff --git a/src/main/java/code/shubham/core/keygeneration/services/KeyGenerationService.java b/src/main/java/code/shubham/core/keygeneration/services/KeyGenerationService.java new file mode 100644 index 0000000..2c7d64a --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/services/KeyGenerationService.java @@ -0,0 +1,46 @@ +package code.shubham.core.keygeneration.services; + +import code.shubham.core.keygeneration.dao.repositories.AvailableKeyRepository; +import code.shubham.core.keygeneration.services.facades.KeyGenerationFacade; +import code.shubham.core.keygenerationcommons.service.IKeyGenerationService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KeyGenerationService implements IKeyGenerationService { + + private final KeyGenerationFacade facade; + + private final AvailableKeyRepository availableKeyRepository; + + @PostConstruct + public void init() { + this.scheduleKeyGenerationTask(); + } + + public String poll() { + return this.facade.poll(); + } + + private void scheduleKeyGenerationTask() { + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + executor.scheduleAtFixedRate(() -> this.addKeys(100), 1l, 1l, TimeUnit.HOURS); + } + + private void addKeys(int count) { + if (this.availableKeyRepository.count() > 50) + return; + + while (count-- > 0) + this.facade.addRandomKey(); + } + +} diff --git a/src/main/java/code/shubham/core/keygeneration/services/facades/KeyGenerationFacade.java b/src/main/java/code/shubham/core/keygeneration/services/facades/KeyGenerationFacade.java new file mode 100644 index 0000000..15f675a --- /dev/null +++ b/src/main/java/code/shubham/core/keygeneration/services/facades/KeyGenerationFacade.java @@ -0,0 +1,67 @@ +package code.shubham.core.keygeneration.services.facades; + +import code.shubham.commons.utils.StringUtils; +import code.shubham.core.keygeneration.dao.entities.AvailableKey; +import code.shubham.core.keygeneration.dao.entities.UsedKey; +import code.shubham.core.keygeneration.dao.repositories.AvailableKeyRepository; +import code.shubham.core.keygeneration.dao.repositories.UsedKeyRepository; +import code.shubham.core.keygenerationcommons.strategies.IKeyGenerateStrategy; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KeyGenerationFacade { + + private final AvailableKeyRepository availableKeyRepository; + + private final UsedKeyRepository usedKeyRepository; + + @Autowired + @Qualifier("RandomCharacterKeyGenerateStrategy") + private IKeyGenerateStrategy randomCharacterKeyGenerateStrategy; + + @Transactional + public String poll() { + final String name = this.getAvailable() + .filter(StringUtils::isNotEmpty) + .orElse(this.randomCharacterKeyGenerateStrategy.generate(null, 7)); + this.usedKeyRepository.save(UsedKey.builder().name(name).build()); + this.availableKeyRepository.deleteById(name); + return name; + } + + public boolean addRandomKey() { + boolean result = false; + int retry = 0; + while (!result && retry++ < 3) { + final String name = this.randomCharacterKeyGenerateStrategy.generate(null, 7); + result = this.addKey(name); + } + return result; + } + + private boolean addKey(final String name) { + final Optional usedKey = this.usedKeyRepository.findById(name); + if (usedKey.isEmpty()) { + this.availableKeyRepository.save(AvailableKey.builder().name(name).build()); + return true; + } + return false; + } + + private Optional getAvailable() { + // return this.availableKeyRepository.findAny(); + return Optional.empty(); + } + +} diff --git a/src/main/java/code/shubham/core/keygenerationclient/KeyGenerationClient.java b/src/main/java/code/shubham/core/keygenerationclient/KeyGenerationClient.java new file mode 100644 index 0000000..d324627 --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationclient/KeyGenerationClient.java @@ -0,0 +1,46 @@ +package code.shubham.core.keygenerationclient; + +import code.shubham.core.keygenerationcommons.service.IKeyGenerationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class KeyGenerationClient { + + private final IKeyGenerationService keyGenerationService; + + public KeyGenerationClient(final IKeyGenerationService keyGenerationService) { + this.keyGenerationService = keyGenerationService; + } + + // @Value("${host.keygeneration:127.0.0.1}") + // private String host = "127.0.0.1"; + // + // @Value("${port.authorization: #{8091}}") + // private Integer port = 8084; + // private final String baseUrl; + // private static final String CONTEXT_PATH = "/v1/keygeneration"; + // + // private final HttpClientUtils httpClientUtils; + // + // @Autowired + // public KeyGenerationClient(final HttpClientUtils httpClientUtils) { + // this.httpClientUtils = httpClientUtils; + // this.baseUrl = "http://" + this.host + ":" + this.port + CONTEXT_PATH; + // } + // + // public String poll() { + // HttpRequest request = this.httpClientUtils.createRequest( + // "GET", + // this.baseUrl + "/", + // null, + // null); + // return this.httpClientUtils.sendRequest(request, String.class, "KeyGeneration"); + // } + + public String poll() { + return this.keyGenerationService.poll(); + } + +} diff --git a/src/main/java/code/shubham/core/keygenerationcommons/configurations/DependencyBeans.java b/src/main/java/code/shubham/core/keygenerationcommons/configurations/DependencyBeans.java new file mode 100644 index 0000000..8e32cc6 --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationcommons/configurations/DependencyBeans.java @@ -0,0 +1,26 @@ +package code.shubham.core.keygenerationcommons.configurations; + +import code.shubham.core.keygenerationclient.KeyGenerationClient; +import code.shubham.core.keygenerationcommons.strategies.IKeyGenerateStrategy; +import code.shubham.core.tinyurl.strategies.KGSKeyGenerationStrategy; +import code.shubham.core.keygenerationcommons.strategies.RandomCharacterKeyGenerateStrategy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DependencyBeans { + + @Bean("KeyGenerateStrategy") + @Autowired + public IKeyGenerateStrategy keyGenerateStrategy(final KeyGenerationClient keyGenerationClient) { + return new KGSKeyGenerationStrategy(keyGenerationClient); + } + + @Bean("RandomCharacterKeyGenerateStrategy") + @Autowired + public IKeyGenerateStrategy randomKeyGenerateStrategy() { + return new RandomCharacterKeyGenerateStrategy(); + } + +} \ No newline at end of file diff --git a/src/main/java/code/shubham/core/keygenerationcommons/service/IKeyGenerationService.java b/src/main/java/code/shubham/core/keygenerationcommons/service/IKeyGenerationService.java new file mode 100644 index 0000000..2935da4 --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationcommons/service/IKeyGenerationService.java @@ -0,0 +1,7 @@ +package code.shubham.core.keygenerationcommons.service; + +public interface IKeyGenerationService { + + String poll(); + +} diff --git a/src/main/java/code/shubham/core/keygenerationcommons/strategies/HashedKeyGenerateStrategy.java b/src/main/java/code/shubham/core/keygenerationcommons/strategies/HashedKeyGenerateStrategy.java new file mode 100644 index 0000000..57d7530 --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationcommons/strategies/HashedKeyGenerateStrategy.java @@ -0,0 +1,37 @@ +package code.shubham.core.keygenerationcommons.strategies; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Random; +import java.util.stream.IntStream; + +public class HashedKeyGenerateStrategy implements IKeyGenerateStrategy { + + private final String algorithm; + + public HashedKeyGenerateStrategy(String algorithm) { + this.algorithm = algorithm; + } + + @Override + public String generate(String input, Integer keyLength) { + MessageDigest md5MessageDigest = null; + try { + md5MessageDigest = MessageDigest.getInstance(this.algorithm); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + final var digest = md5MessageDigest.digest(input.getBytes()); + final var encoded = Base64.getEncoder().encodeToString(digest); + final var chrs = encoded.toCharArray(); + + final var random = new Random(); + return IntStream.range(0, keyLength) + .map(i -> chrs[random.nextInt(22) - 1]) + .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) + .toString(); + } + +} diff --git a/src/main/java/code/shubham/core/keygenerationcommons/strategies/IKeyGenerateStrategy.java b/src/main/java/code/shubham/core/keygenerationcommons/strategies/IKeyGenerateStrategy.java new file mode 100644 index 0000000..554563a --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationcommons/strategies/IKeyGenerateStrategy.java @@ -0,0 +1,7 @@ +package code.shubham.core.keygenerationcommons.strategies; + +public interface IKeyGenerateStrategy { + + String generate(String input, Integer keyLength); + +} diff --git a/src/main/java/code/shubham/core/keygenerationcommons/strategies/RandomCharacterKeyGenerateStrategy.java b/src/main/java/code/shubham/core/keygenerationcommons/strategies/RandomCharacterKeyGenerateStrategy.java new file mode 100644 index 0000000..3a0593c --- /dev/null +++ b/src/main/java/code/shubham/core/keygenerationcommons/strategies/RandomCharacterKeyGenerateStrategy.java @@ -0,0 +1,20 @@ +package code.shubham.core.keygenerationcommons.strategies; + +import java.util.Random; +import java.util.stream.IntStream; + +public class RandomCharacterKeyGenerateStrategy implements IKeyGenerateStrategy { + + private static final String CHARACTERS_STRING = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"; + + private static final char[] CHARACTERS = CHARACTERS_STRING.toCharArray(); + + @Override + public String generate(final String input, final Integer keyLength) { + final char[] result = new char[keyLength]; + final Random random = new Random(); + IntStream.range(0, keyLength).forEach(i -> result[i] = CHARACTERS[random.nextInt(61 - 1) + 1]); + return new String(result); + } + +} diff --git a/src/main/java/code/shubham/core/tinyurl/dao/entities/ShortURL.java b/src/main/java/code/shubham/core/tinyurl/dao/entities/ShortURL.java new file mode 100644 index 0000000..0c3b387 --- /dev/null +++ b/src/main/java/code/shubham/core/tinyurl/dao/entities/ShortURL.java @@ -0,0 +1,44 @@ +package code.shubham.core.tinyurl.dao.entities; + +import code.shubham.commons.dao.base.entities.BaseAbstractAuditableEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Date; + +@SuperBuilder +@Builder +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "short_urls") +public class ShortURL extends BaseAbstractAuditableEntity { + + @Column(unique = true, length = 8, nullable = false, updatable = false) + private String keyName; + + @Lob + private String url; + + @Temporal(TemporalType.TIMESTAMP) + private Date expiryAt; + + private Long accountId; + + public String getShortURL() { + return keyName; + } + +} diff --git a/src/main/java/code/shubham/core/tinyurl/dao/repositories/ShortUrlRepository.java b/src/main/java/code/shubham/core/tinyurl/dao/repositories/ShortUrlRepository.java new file mode 100644 index 0000000..562ef9b --- /dev/null +++ b/src/main/java/code/shubham/core/tinyurl/dao/repositories/ShortUrlRepository.java @@ -0,0 +1,17 @@ +package code.shubham.core.tinyurl.dao.repositories; + +import code.shubham.core.tinyurl.dao.entities.ShortURL; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ShortUrlRepository extends JpaRepository { + + @Query(nativeQuery = true, + value = "SELECT url FROM short_urls WHERE key_name = ? AND (account_id IS NULL OR account_id = ?)") + Optional findURL(String shortUrl, Long accountId); + +} diff --git a/src/main/java/code/shubham/core/tinyurl/services/TinyURLService.java b/src/main/java/code/shubham/core/tinyurl/services/TinyURLService.java new file mode 100644 index 0000000..c84827a --- /dev/null +++ b/src/main/java/code/shubham/core/tinyurl/services/TinyURLService.java @@ -0,0 +1,68 @@ +package code.shubham.core.tinyurl.services; + +import code.shubham.commons.utils.StringUtils; +import code.shubham.core.tinyurl.dao.entities.ShortURL; +import code.shubham.core.tinyurl.dao.repositories.ShortUrlRepository; +import code.shubham.core.keygenerationcommons.strategies.IKeyGenerateStrategy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Slf4j +@Component +public class TinyURLService { + + private final ShortUrlRepository repository; + + private IKeyGenerateStrategy keyGenerateStrategy; + + private IKeyGenerateStrategy randomCharacterKeyGenerateStrategy; + + private static final int DEFAULT_SHORT_URL_LENGTH = 7; + + @Autowired + public TinyURLService(final ShortUrlRepository repository, + @Qualifier("KeyGenerateStrategy") final IKeyGenerateStrategy keyGenerateStrategy, + @Qualifier("RandomCharacterKeyGenerateStrategy") final IKeyGenerateStrategy randomCharacterKeyGenerateStrategy) { + this.repository = repository; + this.keyGenerateStrategy = keyGenerateStrategy; + this.randomCharacterKeyGenerateStrategy = randomCharacterKeyGenerateStrategy; + } + + public String create(final ShortURL shortURL) { + ShortURL generatedShortURL = null; + if (StringUtils.isNotEmpty(shortURL.getKeyName())) + generatedShortURL = this.repository.save(shortURL); + else + generatedShortURL = this.generateShortUrl(shortURL); + return generatedShortURL.getKeyName(); + } + + private ShortURL generateShortUrl(ShortURL shortURL) { + String shortUrl = null; + try { + shortUrl = this.keyGenerateStrategy.generate(shortURL.getUrl(), DEFAULT_SHORT_URL_LENGTH); + } + catch (final Exception exception) { + shortUrl = this.randomCharacterKeyGenerateStrategy.generate(shortURL.getUrl(), DEFAULT_SHORT_URL_LENGTH); + } + + shortURL.setKeyName(shortUrl); + try { + shortURL = this.repository.save(shortURL); + } + catch (final DuplicateKeyException duplicateKeyException) { + this.generateShortUrl(shortURL); + } + return shortURL; + } + + public Optional resolve(final String shortUrl, final Long accountId) { + return this.repository.findURL(shortUrl, accountId); + } + +} diff --git a/src/main/java/code/shubham/core/tinyurl/strategies/KGSKeyGenerationStrategy.java b/src/main/java/code/shubham/core/tinyurl/strategies/KGSKeyGenerationStrategy.java new file mode 100644 index 0000000..6872a5a --- /dev/null +++ b/src/main/java/code/shubham/core/tinyurl/strategies/KGSKeyGenerationStrategy.java @@ -0,0 +1,21 @@ +package code.shubham.core.tinyurl.strategies; + +import code.shubham.core.keygenerationclient.KeyGenerationClient; +import code.shubham.core.keygenerationcommons.strategies.IKeyGenerateStrategy; +import org.springframework.beans.factory.annotation.Autowired; + +public class KGSKeyGenerationStrategy implements IKeyGenerateStrategy { + + private final KeyGenerationClient keyGenerationClient; + + @Autowired + public KGSKeyGenerationStrategy(final KeyGenerationClient keyGenerationClient) { + this.keyGenerationClient = keyGenerationClient; + } + + @Override + public String generate(String input, Integer keyLength) { + return this.keyGenerationClient.poll(); + } + +} diff --git a/src/main/java/code/shubham/core/tinyurl/web/v1/controllers/TinyURLController.java b/src/main/java/code/shubham/core/tinyurl/web/v1/controllers/TinyURLController.java new file mode 100644 index 0000000..4b1a24e --- /dev/null +++ b/src/main/java/code/shubham/core/tinyurl/web/v1/controllers/TinyURLController.java @@ -0,0 +1,58 @@ +package code.shubham.core.tinyurl.web.v1.controllers; + +import code.shubham.commons.contexts.AccountIDContextHolder; +import code.shubham.commons.exceptions.InvalidRequestException; +import code.shubham.commons.exceptions.NotFoundException; +import code.shubham.commons.utils.ResponseUtils; +import code.shubham.commons.utils.StringUtils; +import code.shubham.core.tinyurl.dao.entities.ShortURL; +import code.shubham.core.tinyurl.services.TinyURLService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; + +@Slf4j +@RestController +@RequestMapping("/v1/tinyurl") +@RequiredArgsConstructor +public class TinyURLController { + + private final TinyURLService service; + + @GetMapping + public ResponseEntity get(@RequestParam("url") final String url) { + + if (StringUtils.isEmpty(url)) + throw new InvalidRequestException("url", "url must not be null or empty"); + + final ShortURL shortURL = ShortURL.builder().url(url).accountId(AccountIDContextHolder.get()).build(); + final String tinyURL = this.service.create(shortURL); + return ResponseUtils.getDataResponseEntity(HttpStatus.CREATED.value(), new HashMap<>() { + { + put("url", tinyURL); + } + }); + } + + @GetMapping("/{tinyURL}") + public ResponseEntity redirect(@PathVariable("tinyURL") final String tinyURL) { + + if (StringUtils.isEmpty(tinyURL)) + throw new InvalidRequestException("tinyURL", "tinyURL must not be null or empty"); + + return this.service.resolve(tinyURL, AccountIDContextHolder.get()) + .map(ResponseUtils::redirect) + .orElseThrow(() -> new NotFoundException("tinURL", "tinyURL %s not found", tinyURL)); + } + +} diff --git a/src/main/resources/db/migration/V3__00003_create_procedure_update_avaibale_key.sql b/src/main/resources/db/migration/V3__00003_create_procedure_update_avaibale_key.sql new file mode 100644 index 0000000..3cf25bd --- /dev/null +++ b/src/main/resources/db/migration/V3__00003_create_procedure_update_avaibale_key.sql @@ -0,0 +1,8 @@ +--CREATE PROCEDURE POLL_ANY_AVAILABLE_KEY() +--BEGIN +-- SELECT `name` FROM `available_keys` WHERE `is_selected` = 0 LIMIT 1); +-- UPDATE available_keys SET is_selected = 1 WHERE name = (SELECT @keyName); +-- INSERT INTO used_keys VALUES ((SELECT @keyName)); +-- DELETE FROM available_keys WHERE name = (SELECT @keyName); +-- SELECT @keyName as name; +--END;