Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Models and endpoints for user accounts #46

Merged
merged 12 commits into from
Nov 21, 2022
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,6 @@ gradle test
- `dto/` - Data Transfer Objects for creating and modifying entities
- `repository/` - Data access layer methods (Spring Data repositories)
- `service/` - Business logic for the controllers
- `annotations/` - Custom annotations used in the project
- `validation/` - Custom validations used across the different models
- `src/test/` - Self explanatory: unit tests, functional (end-to-end) tests, etc.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pt.up.fe.ni.website.backend.annotations.validation

import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.reflect.KClass

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
@MustBeDocumented
annotation class NullOrNotBlank(
val message: String = "must be null or not blank",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<Payload>> = []
)

class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String?> {
override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {
return value == null || value.isNotBlank()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pt.up.fe.ni.website.backend.controller

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.RestController
import pt.up.fe.ni.website.backend.model.dto.AccountDto
import pt.up.fe.ni.website.backend.service.AccountService

@RestController
@RequestMapping("/accounts")
class AccountController(private val service: AccountService) {
@GetMapping
fun getAllAccounts() = service.getAllAccounts()

@GetMapping("/{id}")
fun getAccountById(@PathVariable id: Long) = service.getAccountById(id)

@PostMapping("/new")
fun createAccount(@RequestBody dto: AccountDto) = service.createAccount(dto)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pt.up.fe.ni.website.backend.controller

import com.fasterxml.jackson.databind.exc.InvalidFormatException
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.http.HttpStatus
Expand Down Expand Up @@ -62,6 +63,13 @@ class ErrorController : ErrorController {
param = cause.parameter.name
)
}

is MismatchedInputException -> {
return wrapSimpleError(
"must be ${cause.targetType.simpleName.lowercase()}",
param = cause.path.joinToString(".") { it.fieldName }
)
}
}

return wrapSimpleError(e.message ?: "invalid request body")
Expand All @@ -73,6 +81,12 @@ class ErrorController : ErrorController {
return wrapSimpleError(e.message ?: "element not found")
}

@ExceptionHandler(IllegalArgumentException::class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
fun illegalArgument(e: IllegalArgumentException): CustomError {
return wrapSimpleError(e.message ?: "invalid argument")
}

@ExceptionHandler(Exception::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun unexpectedError(e: Exception): CustomError {
Expand Down
60 changes: 60 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Account.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import pt.up.fe.ni.website.backend.annotations.validation.NullOrNotBlank
import java.util.Date
import javax.persistence.CascadeType
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.OneToMany
import javax.persistence.Temporal
import javax.persistence.TemporalType
import javax.validation.constraints.Email
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.Past
import javax.validation.constraints.Size
import pt.up.fe.ni.website.backend.model.constants.AccountConstants as Constants

@Entity
class Account(
@JsonProperty(required = true)
@field:Size(min = Constants.Name.minSize, max = Constants.Name.maxSize)
var name: String,

@JsonProperty(required = true)
@Column(unique = true)
@field:NotEmpty
@field:Email
var email: String,

@field:Size(min = Constants.Name.minSize, max = Constants.Name.maxSize)
var bio: String?,

@Temporal(TemporalType.DATE)
@field:Past
var birthDate: Date?,

@field:NullOrNotBlank
@field:URL
var photoPath: String?,

@field:NullOrNotBlank
@field:URL
var linkedin: String?,

@field:NullOrNotBlank
@field:URL
var github: String?,

@JoinColumn
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
Comment on lines +52 to +53
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val websites: List<CustomWebsite>,

@Id @GeneratedValue
val id: Long? = null
)
22 changes: 22 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/CustomWebsite.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import pt.up.fe.ni.website.backend.annotations.validation.NullOrNotBlank
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
class CustomWebsite(
@JsonProperty(required = true)
@field:URL
val url: String,

@field:NullOrNotBlank
@field:URL
val iconPath: String?,

@Id @GeneratedValue
val id: Long? = null
)
4 changes: 4 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
Expand All @@ -26,12 +28,14 @@ class Post(

@JsonProperty(required = true)
@field:NotEmpty
@field:URL
var thumbnailPath: String,

@CreatedDate
var publishDate: Date? = null,

@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
var lastUpdatedAt: Date? = null,

@Id @GeneratedValue
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pt.up.fe.ni.website.backend.model.constants

object AccountConstants {
object Name {
const val minSize = 2
const val maxSize = 100
}

object Bio {
const val minSize = 5
const val maxSize = 500
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pt.up.fe.ni.website.backend.model.dto

import pt.up.fe.ni.website.backend.model.Account
import java.util.Date

class AccountDto(
val name: String,
val email: String,
val bio: String?,
val birthDate: Date?,
val photoPath: String?,
val linkedin: String?,
val github: String?,
val websites: List<CustomWebsiteDto>
) : Dto<Account>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.model.dto

import pt.up.fe.ni.website.backend.model.CustomWebsite

class CustomWebsiteDto(
val url: String,
val iconPath: String?
) : Dto<CustomWebsite>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.repository

import org.springframework.data.repository.CrudRepository
import pt.up.fe.ni.website.backend.model.Account

interface AccountRepository : CrudRepository<Account, Long> {
fun findByEmail(email: String): Account?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pt.up.fe.ni.website.backend.service

import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.model.dto.AccountDto
import pt.up.fe.ni.website.backend.repository.AccountRepository

@Service
class AccountService(private val repository: AccountRepository) {
fun getAllAccounts(): List<Account> = repository.findAll().toList()

fun createAccount(dto: AccountDto): Account {
repository.findByEmail(dto.email)?.let {
throw IllegalArgumentException("email already exists")
}

val account = dto.create()
return repository.save(account)
}

fun getAccountById(id: Long): Account = repository.findByIdOrNull(id)
?: throw NoSuchElementException("account not found with id $id")
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ server.error.whitelabel.enabled=false
# Jackson
spring.jackson.default-property-inclusion=non_null
spring.jackson.deserialization.fail-on-null-creator-properties=true
spring.jackson.date-format=dd-MM-yyyy