Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ plugins {
id 'org.springframework.boot' version '1.5.9.RELEASE'
id 'org.jetbrains.kotlin.jvm' version '1.2.10'
id 'org.jetbrains.kotlin.plugin.spring' version '1.2.10'
id "org.jetbrains.kotlin.plugin.jpa" version "1.2.10"
id 'org.jmailen.kotlinter' version '1.5.0'
// id 'com.gorylenko.gradle-git-properties' version '1.4.17'
id "io.gitlab.arturbosch.detekt" version "1.0.0.RC5-6"
Expand Down Expand Up @@ -178,8 +179,8 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-actuator-docs')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-jdbc')

compile('org.springframework.boot:spring-boot-starter-data-jpa')
// security
compile('org.springframework.boot:spring-boot-starter-security')
compile("io.jsonwebtoken:jjwt:${property('jjwt.version')}")
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/buildit/bookit/BookitApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.buildit.bookit.auth.JwtAuthenticationFilter
import com.buildit.bookit.auth.OpenIdAuthenticator
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.autoconfigure.security.SecurityProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy.STATELESS
Expand All @@ -28,6 +30,9 @@ import java.time.Clock
*/
@SpringBootApplication
@EnableConfigurationProperties(BookitProperties::class)
@EntityScan(
basePackageClasses = [BookitApplication::class, Jsr310JpaConverters.ZoneIdConverter::class]
)
class BookitApplication {
@Bean
fun defaultClock(): Clock = Clock.systemUTC()
Expand Down
65 changes: 29 additions & 36 deletions src/main/kotlin/com/buildit/bookit/v1/booking/BookingController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import com.buildit.bookit.auth.UserPrincipal
import com.buildit.bookit.v1.booking.dto.Booking
import com.buildit.bookit.v1.booking.dto.BookingRequest
import com.buildit.bookit.v1.booking.dto.interval
import com.buildit.bookit.v1.booking.dto.maskSubjectIfOtherUser
import com.buildit.bookit.v1.location.LocationRepository
import com.buildit.bookit.v1.location.bookable.BookableRepository
import com.buildit.bookit.v1.location.bookable.InvalidBookable
import com.buildit.bookit.v1.location.bookable.dto.Bookable
import com.buildit.bookit.v1.location.dto.Location
import com.buildit.bookit.v1.user.UserService
import com.buildit.bookit.v1.user.dto.maskSubjectIfOtherUser
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
Expand All @@ -31,7 +30,6 @@ import java.net.URI
import java.time.Clock
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.temporal.ChronoUnit.MINUTES
import javax.validation.Valid

Expand All @@ -58,7 +56,6 @@ class BookableNotAvailable : RuntimeException("Bookable is not available. Pleas
@Transactional
class BookingController(private val bookingRepository: BookingRepository,
private val bookableRepository: BookableRepository,
private val locationRepository: LocationRepository,
private val userService: UserService,
private val clock: Clock
) {
Expand All @@ -79,16 +76,13 @@ class BookingController(private val bookingRepository: BookingRepository,
throw EndBeforeStartException()
}

val allBookings = bookingRepository.getAllBookings()
val allBookings = bookingRepository.findAll().toList()
if (start == LocalDate.MIN && end == LocalDate.MAX)
return allBookings.map { maskSubjectIfOtherUser(it, user) }

val locationTimezones = locationRepository.getLocations().associate { Pair(it.id, ZoneId.of(it.timeZone)) }
val bookableTimezones = bookableRepository.getAllBookables().associate { Pair(it.id, locationTimezones[it.locationId]) }

return allBookings
.filter { booking ->
val timezone = bookableTimezones[booking.bookableId] ?: throw IllegalStateException("Encountered an incomplete booking: $booking. Not able to determine booking's timezone.")
val timezone = booking.bookable.location.timeZone
Copy link
Contributor

Choose a reason for hiding this comment

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

Changed due to constraints guaranteeing presence?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

val desiredInterval = Interval.of(
start.atStartOfDay(timezone).toInstant(),
end.atStartOfDay(timezone).toInstant()
Expand All @@ -99,45 +93,45 @@ class BookingController(private val bookingRepository: BookingRepository,
}

@GetMapping("/{id}")
fun getBooking(@PathVariable("id") bookingId: String, @AuthenticationPrincipal user: UserPrincipal): Booking =
bookingRepository.getAllBookings().find { it.id == bookingId }.let { maskSubjectIfOtherUser(it ?: throw BookingNotFound(), user) }
fun getBooking(@PathVariable("id") booking: Booking?, @AuthenticationPrincipal user: UserPrincipal): Booking =
Copy link
Contributor

Choose a reason for hiding this comment

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

What witchcraft is this? Am looking at the diffs in the browser, so probably just need context.

booking?.let { maskSubjectIfOtherUser(it, user) } ?: throw BookingNotFound()

@DeleteMapping("/{id}")
fun deleteBooking(@PathVariable("id") id: String, @AuthenticationPrincipal userPrincipal: UserPrincipal): ResponseEntity<Unit> {
val booking = bookingRepository.getAllBookings().find { it.id == id }
if (booking != null && booking.user.externalId != userPrincipal.subject) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
fun deleteBooking(@PathVariable("id") booking: Booking?, @AuthenticationPrincipal userPrincipal: UserPrincipal): ResponseEntity<Unit> =
when {
booking == null -> ResponseEntity.noContent().build()
booking.user.externalId != userPrincipal.subject -> ResponseEntity.status(HttpStatus.FORBIDDEN).build()
else -> {
bookingRepository.delete(booking)
ResponseEntity.noContent().build()
}
}
bookingRepository.delete(id)
return ResponseEntity.noContent().build()
}

@Suppress("UnsafeCallOnNullableType")
@PostMapping()
fun createBooking(@Valid @RequestBody bookingRequest: BookingRequest, @AuthenticationPrincipal userPrincipal: UserPrincipal, errors: Errors? = null): ResponseEntity<Booking> {

val bookable = bookableRepository.getAllBookables().find { it.id == bookingRequest.bookableId } ?: throw InvalidBookable()
val location = locationRepository.getLocations().single { it.id == bookable.locationId }

if (errors?.hasErrors() == true) {
val errorMessage = errors.allErrors.joinToString(",", transform = { it.defaultMessage })

throw InvalidBooking(errorMessage)
}
val bookable = bookableRepository.findOne(bookingRequest.bookableId) ?: throw InvalidBookable()

val startDateTimeTruncated = bookingRequest.start!!.truncatedTo(MINUTES)
val endDateTimeTruncated = bookingRequest.end!!.truncatedTo(MINUTES)

validateBooking(location, startDateTimeTruncated, endDateTimeTruncated, bookable)
validateBooking(bookable.location, startDateTimeTruncated, endDateTimeTruncated, bookable)

val user = userService.register(userPrincipal)

val booking = bookingRepository.insertBooking(
bookingRequest.bookableId!!,
bookingRequest.subject!!,
startDateTimeTruncated,
endDateTimeTruncated,
user
val booking = bookingRepository.save(
Booking(
bookable,
bookingRequest.subject!!,
startDateTimeTruncated,
endDateTimeTruncated,
user
)
)

return ResponseEntity
Expand All @@ -146,7 +140,7 @@ class BookingController(private val bookingRepository: BookingRepository,
}

private fun validateBooking(location: Location, startDateTimeTruncated: LocalDateTime, endDateTimeTruncated: LocalDateTime, bookable: Bookable) {
val now = LocalDateTime.now(clock.withZone(ZoneId.of(location.timeZone)))
val now = LocalDateTime.now(clock.withZone(location.timeZone))
if (!startDateTimeTruncated.isAfter(now)) {
throw StartInPastException()
}
Expand All @@ -156,17 +150,16 @@ class BookingController(private val bookingRepository: BookingRepository,
}

val interval = Interval.of(
startDateTimeTruncated.atZone(ZoneId.of(location.timeZone)).toInstant(),
endDateTimeTruncated.atZone(ZoneId.of(location.timeZone)).toInstant()
startDateTimeTruncated.atZone(location.timeZone).toInstant(),
endDateTimeTruncated.atZone(location.timeZone).toInstant()
)

val unavailable = bookingRepository.getAllBookings()
.filter { it.bookableId == bookable.id }
val unavailable = bookingRepository.findByBookable(bookable)
.any {
interval.overlaps(
Interval.of(
it.start.atZone(ZoneId.of(location.timeZone)).toInstant(),
it.end.atZone(ZoneId.of(location.timeZone)).toInstant()))
it.start.atZone(location.timeZone).toInstant(),
it.end.atZone(location.timeZone).toInstant()))
}

if (unavailable) {
Expand Down
83 changes: 4 additions & 79 deletions src/main/kotlin/com/buildit/bookit/v1/booking/BookingRepository.kt
Original file line number Diff line number Diff line change
@@ -1,84 +1,9 @@
package com.buildit.bookit.v1.booking

import com.buildit.bookit.v1.booking.dto.Booking
import com.buildit.bookit.v1.booking.dto.User
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.simple.SimpleJdbcInsert
import org.springframework.stereotype.Repository
import java.sql.ResultSet
import java.time.LocalDateTime
import java.util.UUID
import com.buildit.bookit.v1.location.bookable.dto.Bookable
import org.springframework.data.repository.CrudRepository

interface BookingRepository {
fun getAllBookings(): Collection<Booking>
/**
* @param bookableId The id for the pre-exiting <code>Bookable</code> that is being booked.
* @param subject The purpose of the booking.
* @param startDateTime The start date of the booking.
* @param endDateTime The end date of the booking.
* @param creatingUser A pre-existing, fully-formed <code>User</code> object representing the person creating the booking.
*/
fun insertBooking(bookableId: String, subject: String, startDateTime: LocalDateTime, endDateTime: LocalDateTime, creatingUser: User): Booking

fun delete(id: String): Boolean
}

@Repository
class BookingDatabaseRepository(private val jdbcTemplate: JdbcTemplate) : BookingRepository {

private val tableName = "BOOKING"

override fun getAllBookings(): Collection<Booking> {
val sql =
"""
|SELECT b.BOOKING_ID,
| b.BOOKABLE_ID,
| b.SUBJECT,
| b.START_DATE,
| b.END_DATE,
| u.USER_ID,
| u.GIVEN_NAME,
| u.FAMILY_NAME,
| u.EXTERNAL_USER_ID
|FROM $tableName b
|LEFT JOIN USER u on b.USER_ID = u.USER_ID""".trimMargin()

return jdbcTemplate.query(sql) { rs, _ ->
Booking(
rs.getString("BOOKING_ID"),
rs.getString("BOOKABLE_ID"),
rs.getString("SUBJECT"),
rs.getObject("START_DATE", LocalDateTime::class.java),
rs.getObject("END_DATE", LocalDateTime::class.java),
makeUser(rs)
)
}
}

override fun insertBooking(bookableId: String, subject: String, startDateTime: LocalDateTime, endDateTime: LocalDateTime, creatingUser: User): Booking {
val bookingId = UUID.randomUUID().toString()

SimpleJdbcInsert(jdbcTemplate).withTableName(tableName).apply {
execute(
mapOf("BOOKING_ID" to bookingId,
"BOOKABLE_ID" to bookableId,
"SUBJECT" to subject,
"START_DATE" to startDateTime,
"END_DATE" to endDateTime,
"USER_ID" to creatingUser.id
)
)
}

return Booking(bookingId, bookableId, subject, startDateTime, endDateTime, creatingUser)
}

override fun delete(id: String): Boolean = jdbcTemplate.update("DELETE FROM $tableName WHERE BOOKING_ID = ?", id) == 1

private fun makeUser(rs: ResultSet): User =
User(
rs.getString("USER_ID"),
"${rs.getString("GIVEN_NAME")} ${rs.getString("FAMILY_NAME")}",
rs.getString("EXTERNAL_USER_ID")
)
interface BookingRepository : CrudRepository<Booking, String> {
fun findByBookable(bookable: Bookable): List<Booking>
}
37 changes: 18 additions & 19 deletions src/main/kotlin/com/buildit/bookit/v1/booking/dto/Booking.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.buildit.bookit.v1.booking.dto

import com.buildit.bookit.auth.UserPrincipal
import com.buildit.bookit.v1.location.bookable.dto.Bookable
import com.buildit.bookit.v1.user.dto.User
import com.fasterxml.jackson.annotation.JsonFormat
import org.hibernate.annotations.GenericGenerator
import org.hibernate.validator.constraints.NotBlank
import org.threeten.extra.Interval
import java.time.LocalDateTime
import java.time.ZoneId
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.ManyToOne
import javax.validation.constraints.NotNull

/**
Expand All @@ -22,31 +29,23 @@ data class BookingRequest(
val end: LocalDateTime?
)

const val MASKED_STRING = "**********"
fun maskSubjectIfOtherUser(booking: Booking, otherUser: UserPrincipal): Booking =
when {
booking.user.externalId != otherUser.subject -> booking.copy(subject = MASKED_STRING)
else -> booking
}

/**
* Booking response
*/
@Entity
data class Booking(
val id: String,
val bookableId: String,
@ManyToOne(optional = false)
val bookable: Bookable,
@Column(nullable = false)
val subject: String,
@field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@Column(nullable = false) @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
val start: LocalDateTime,
@field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@Column(nullable = false) @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
val end: LocalDateTime,
val user: User
)

data class User(
val id: String,
val name: String,
val externalId: String
@ManyToOne(optional = false)
val user: User,
@Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "uuid2")
val id: String? = null
Copy link
Contributor Author

Choose a reason for hiding this comment

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

generally moved optional IDs to end

Copy link
Contributor

Choose a reason for hiding this comment

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

I slightly prefer a degree of isomorphism between the schema and the code, and like PKs first in a schema, but I can't what JPA will do in the schema anyway with regard to order (maybe it sorts keys to the top?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fair point... i'll have a looksie as to what it actually does

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hibernate: drop table bookable if exists
Hibernate: drop table booking if exists
Hibernate: drop table location if exists
Hibernate: drop table user if exists
Hibernate: create table bookable (id varchar(255) not null, closed boolean not null, reason varchar(255) not null, name varchar(255) not null, location_id varchar(255) not null, primary key (id))
Hibernate: create table booking (id varchar(255) not null, end timestamp not null, start timestamp not null, subject varchar(255) not null, bookable_id varchar(255) not null, user_id varchar(255) not null, primary key (id))
Hibernate: create table location (id varchar(255) not null, name varchar(255) not null, time_zone varchar(255) not null, primary key (id))
Hibernate: create table user (id varchar(255) not null, external_id varchar(255) not null, family_name varchar(255) not null, given_name varchar(255) not null, primary key (id))
Hibernate: alter table bookable add constraint UK_6ietqnvn35wqiovo8qrmwk2ne unique (location_id, name)
Hibernate: alter table location add constraint UK_sahixf1v7f7xns19cbg12d946 unique (name)
Hibernate: alter table user add constraint UK_4eu2tvn9rj53a93fufx7ayr20 unique (external_id)
Hibernate: alter table bookable add constraint FK1dc19oypan7r3ry418vn2h5sc foreign key (location_id) references location
Hibernate: alter table booking add constraint FKtbhx4q4q4m1kv4muhynct3p81 foreign key (bookable_id) references bookable
Hibernate: alter table booking add constraint FKkgseyy7t56x7lkjgu3wah5s3t foreign key (user_id) references user

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so looks like it does the right thing i'd like to see from a schema perspective wrt order

)

fun Booking.interval(timeZone: ZoneId): Interval =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import org.springframework.web.bind.annotation.RestController
@Transactional
class LocationController(private val locationRepository: LocationRepository) {
@GetMapping
fun getLocations(): Collection<Any> = locationRepository.getLocations()
fun getLocations(): Collection<Location> = locationRepository.findAll().toList()

/**
* Get information about a location
*/
@GetMapping("/{id}")
fun getLocation(@PathVariable("id") location: String): Location =
locationRepository.getLocations().find { it.id == location } ?: throw LocationNotFound()
fun getLocation(@PathVariable("id") location: Location?): Location =
location ?: throw LocationNotFound()
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
package com.buildit.bookit.v1.location

import com.buildit.bookit.v1.location.dto.Location
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

interface LocationRepository {
fun getLocations(): Collection<Location>
}

@Repository
class LocationDatabaseRepository(private val jdbcTemplate: JdbcTemplate) : LocationRepository {

override fun getLocations(): Collection<Location> = jdbcTemplate.query(
"SELECT LOCATION_ID, LOCATION_NAME, LOCATION_TZ FROM LOCATION") { rs, _ ->
Location(
rs.getString("LOCATION_ID"),
rs.getString("LOCATION_NAME"),
rs.getString("LOCATION_TZ")
)
}
}
interface LocationRepository : org.springframework.data.repository.CrudRepository<Location, String>
Loading