diff --git a/build.gradle b/build.gradle index 94f5986..04b91e4 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { compile('org.springframework.boot:spring-boot-starter-jdbc') compile('org.springframework.boot:spring-boot-devtools') compile('cz.jirutka.spring:spring-data-jdbc-repository:0.5.1') + compile('org.jetbrains.exposed:exposed:0.4-SNAPSHOT') compile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") // Always use the latest "dash revision", here -2 diff --git a/src/main/kotlin/io/spring/messenger/config/DatabaseConfig.kt b/src/main/kotlin/io/spring/messenger/config/DatabaseConfig.kt index 65eb7a3..ccd017c 100644 --- a/src/main/kotlin/io/spring/messenger/config/DatabaseConfig.kt +++ b/src/main/kotlin/io/spring/messenger/config/DatabaseConfig.kt @@ -2,43 +2,44 @@ package io.spring.messenger.config import io.spring.messenger.domain.Message import io.spring.messenger.domain.User -import io.spring.messenger.repository.MessageRepository -import io.spring.messenger.repository.UserRepository +import io.spring.messenger.repository.* +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.exists import org.springframework.boot.CommandLineRunner import javax.sql.DataSource import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.jdbc.datasource.DataSourceTransactionManager -import org.springframework.transaction.PlatformTransactionManager -import org.springframework.transaction.annotation.EnableTransactionManagement -@EnableTransactionManagement @Configuration open class DatabaseConfig { @Bean - open fun transactionManager(dataSource: DataSource): PlatformTransactionManager { - return DataSourceTransactionManager(dataSource) - } + open fun db(dataSource: DataSource) = Database.connect(dataSource) @Bean - open fun init(userRepository: UserRepository, messageRepository: MessageRepository) = CommandLineRunner { + open fun init(db: Database, userRepository: UserRepository, messageRepository: MessageRepository) = CommandLineRunner { val swhite = User("swhite", "Skyler", "White") val jpinkman = User("jpinkman", "Jesse", "Pinkman") val walter = User("wwhite", "Walter", "White") val sgoodman = User("sgoodman", "Saul", "Goodman") + db.transaction { + logger.addLogger(StdOutSqlLogger()) - if(userRepository.count() == 0L) { - userRepository.create(swhite) - userRepository.create(jpinkman) - userRepository.create(walter) - userRepository.create(sgoodman) - } + if(!Users.exists()) { + create(Users) + userRepository.create(swhite) + userRepository.create(jpinkman) + userRepository.create(walter) + userRepository.create(sgoodman) + } - if(messageRepository.count() == 0L) { - messageRepository.create(Message("This is a test!", swhite.userName)) + if(!Messages.exists()) { + create(Messages) + messageRepository.create(Message("This is a test!", swhite.userName)) + } } } diff --git a/src/main/kotlin/io/spring/messenger/domain/Message.kt b/src/main/kotlin/io/spring/messenger/domain/Message.kt index a116188..b5bad27 100644 --- a/src/main/kotlin/io/spring/messenger/domain/Message.kt +++ b/src/main/kotlin/io/spring/messenger/domain/Message.kt @@ -7,5 +7,6 @@ data class Message( var content: String, var author: String, var location: Point? = null, - @Id var id: Long? = null -) \ No newline at end of file + @Id var id: Int? = null +) + diff --git a/src/main/kotlin/io/spring/messenger/domain/MessageWithAuthor.kt b/src/main/kotlin/io/spring/messenger/domain/MessageWithAuthor.kt deleted file mode 100644 index e797bdf..0000000 --- a/src/main/kotlin/io/spring/messenger/domain/MessageWithAuthor.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.spring.messenger.domain - -import org.postgis.Point -import org.springframework.data.annotation.Id - -data class MessageWithAuthor( - var content: String, - var author: User, - var location: Point? = null, - @Id var id: Long? = null -) \ No newline at end of file diff --git a/src/main/kotlin/io/spring/messenger/repository/MessageRepository.kt b/src/main/kotlin/io/spring/messenger/repository/MessageRepository.kt index a738d9f..5df2239 100644 --- a/src/main/kotlin/io/spring/messenger/repository/MessageRepository.kt +++ b/src/main/kotlin/io/spring/messenger/repository/MessageRepository.kt @@ -1,44 +1,36 @@ package io.spring.messenger.repository -import com.nurkiewicz.jdbcrepository.JdbcRepository -import com.nurkiewicz.jdbcrepository.RowUnmapper -import com.nurkiewicz.jdbcrepository.TableDescription import io.spring.messenger.domain.Message +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.postgis.PGbox2d -import org.postgis.PGgeometry -import org.postgis.Point -import org.springframework.jdbc.core.RowMapper +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository @Repository -open class MessageRepository : JdbcRepository(mapper(), unmapper(), TableDescription("messages", "id")) { +open class MessageRepository @Autowired constructor(val db: Database) { - open fun findByBoundingBox(box: PGbox2d): List - = jdbcOperations.query("""SELECT * FROM ${table.name} - WHERE location && - ST_MakeEnvelope(${box.llb.x}, ${box.llb.y}, ${box.urt.x}, ${box.urt.y} - , 4326)""", rowMapper) - - override fun postCreate(entity: S, generatedId: Number?): S { - if (generatedId != null) entity.id = generatedId.toLong() - return entity + open fun create(m: Message) = db.transaction { + m.id = Messages.insert(map(m)).get(Messages.id) + m } -} -private fun mapper() = RowMapper { - rs, rowNum -> Message( - rs.getString("content"), - rs.getString("author"), - (rs.getObject("location") as PGgeometry?)?.geometry as Point?, - rs.getLong("id")) -} + open fun findAll() = db.transaction { + unmap(Messages.selectAll()) + } -private fun unmapper() = RowUnmapper { - m -> - val map = mutableMapOf(Pair("id", m.id), Pair("content", m.content), Pair("author", m.author)) - if (m.location != null) { - m.location!!.srid = 4326 - map["location"] = PGgeometry(m.location) - } - map -} + open fun findByBoundingBox(box: PGbox2d) = db.transaction { + unmap(Messages.select { Users.location within box }) + } + + private fun map(m: Message): Messages.(UpdateBuilder<*>) -> Unit = { + if (m.id != null) it[id] = m.id + it[content] = m.content + it[author] = m.author + it[location] = m.location + } + + private fun unmap(rows: SizedIterable): List = + rows.map { Message(it[Messages.content], it[Messages.author], it[Messages.location], it[Messages.id]) } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/spring/messenger/repository/SpatialExtension.kt b/src/main/kotlin/io/spring/messenger/repository/SpatialExtension.kt new file mode 100644 index 0000000..84219b4 --- /dev/null +++ b/src/main/kotlin/io/spring/messenger/repository/SpatialExtension.kt @@ -0,0 +1,39 @@ +package io.spring.messenger.repository + +import org.jetbrains.exposed.sql.* +import org.postgis.PGbox2d +import org.postgis.PGgeometry +import org.postgis.Point + +fun Table.point(name: String, srid: Int = 4326): Column = registerColumn(name, PointColumnType()) + +class PointColumnType(val srid: Int = 4326): ColumnType() { + + override fun sqlType(): String = "GEOMETRY(Point, $srid)" + + override fun valueFromDB(value: Any): Any { + if (value is PGgeometry) { + return value.geometry + } + return value + } + + override fun notNullValueToDB(value: Any): Any { + if (value is Point) { + if (value.srid == Point.UNKNOWN_SRID) value.srid = srid + return PGgeometry(value) + } + return value + } +} + +infix fun ExpressionWithColumnType<*>.within(box: PGbox2d) : Op { + return WithinOp(this, box) +} + +class WithinOp(val expr1: Expression<*>, val box: PGbox2d) : Op() { + + override fun toSQL(queryBuilder: QueryBuilder) = """${expr1.toSQL(queryBuilder)} + && ST_MakeEnvelope(${box.llb.x}, ${box.llb.y}, ${box.urt.x}, ${box.urt.y}, 4326)""" +} + diff --git a/src/main/kotlin/io/spring/messenger/repository/Tables.kt b/src/main/kotlin/io/spring/messenger/repository/Tables.kt new file mode 100644 index 0000000..819cf02 --- /dev/null +++ b/src/main/kotlin/io/spring/messenger/repository/Tables.kt @@ -0,0 +1,17 @@ +package io.spring.messenger.repository + +import org.jetbrains.exposed.sql.Table + +object Messages : Table() { + val id = integer("id").autoIncrement().primaryKey() + val content = text("content") + val author = reference("author", Users.userName) + val location = point("location").nullable() +} + +object Users : Table() { + val userName = text("user_name").primaryKey() + val firstName = text("first_name") + val lastName = text("last_name") + val location = point("location").nullable() +} \ No newline at end of file diff --git a/src/main/kotlin/io/spring/messenger/repository/UserRepository.kt b/src/main/kotlin/io/spring/messenger/repository/UserRepository.kt index a8d2f4b..ffdb147 100644 --- a/src/main/kotlin/io/spring/messenger/repository/UserRepository.kt +++ b/src/main/kotlin/io/spring/messenger/repository/UserRepository.kt @@ -1,47 +1,42 @@ package io.spring.messenger.repository -import com.nurkiewicz.jdbcrepository.JdbcRepository -import com.nurkiewicz.jdbcrepository.RowUnmapper import io.spring.messenger.domain.User +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.postgis.PGbox2d -import org.postgis.PGgeometry import org.postgis.Point -import org.springframework.jdbc.core.RowMapper +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository @Repository -open class UserRepository : JdbcRepository(mapper(), unmapper(), "\"users\"", "user_name") { +open class UserRepository @Autowired constructor(val db: Database) { - open fun updateLocation(userName:String, location: Point): Unit { + open fun updateLocation(userName:String, location: Point) = db.transaction { + logger.addLogger(StdOutSqlLogger()) location.srid = 4326 - jdbcOperations.update("UPDATE ${table.name} SET location = '${PGgeometry(location)}' WHERE user_name = '$userName'") + Users.update({Users.userName eq userName}) { it[Users.location] = location} } - open fun findByBoundingBox(box: PGbox2d): List - = jdbcOperations.query("""SELECT * FROM ${table.name} - WHERE location && - ST_MakeEnvelope(${box.llb.x}, ${box.llb.y}, ${box.urt.x}, ${box.urt.y} - , 4326)""", rowMapper) + open fun create(user: User) = db.transaction { + Users.insert( map(user) ) + } -} + open fun findAll() = db.transaction { + unmap(Users.selectAll()) + } -private fun mapper() = RowMapper { - rs, rowNum -> User( - rs.getString("user_name"), - rs.getString("first_name"), - rs.getString("last_name"), - (rs.getObject("location") as PGgeometry?)?.geometry as Point?) -} + open fun findByBoundingBox(box: PGbox2d) = db.transaction { + unmap(Users.select { Users.location within box }) + } + + private fun map(u: User): Users.(UpdateBuilder<*>) -> Unit = { + it[userName] = u.userName + it[firstName] = u.firstName + it[lastName] = u.lastName + it[location] = u.location + } + + private fun unmap(rows: SizedIterable): List = + rows.map { User(it[Users.userName], it[Users.firstName], it[Users.lastName], it[Users.location]) } -private fun unmapper() = RowUnmapper { - user -> - val map = mutableMapOf( - Pair("user_name", user.userName), - Pair("first_name", user.firstName), - Pair("last_name", user.lastName)) - if (user.location != null) { - user.location!!.srid = 4326 - map["location"] = PGgeometry(user.location) - } - map; } \ No newline at end of file diff --git a/src/main/kotlin/io/spring/messenger/web/MessageController.kt b/src/main/kotlin/io/spring/messenger/web/MessageController.kt index 4aac316..f53bd2e 100644 --- a/src/main/kotlin/io/spring/messenger/web/MessageController.kt +++ b/src/main/kotlin/io/spring/messenger/web/MessageController.kt @@ -21,8 +21,9 @@ class MessageController @Autowired constructor(val repository: MessageRepository fun findMessages() = repository.findAll() @GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}") - fun findByBoundingBox(@PathVariable userName:String, @PathVariable xMin:Double, - @PathVariable yMin:Double, @PathVariable xMax:Double, @PathVariable yMax:Double) + fun findByBoundingBox(@PathVariable userName:String, + @PathVariable xMin:Double, @PathVariable yMin:Double, + @PathVariable xMax:Double, @PathVariable yMax:Double) = repository.findByBoundingBox(PGbox2d(Point(xMin, yMin), Point(xMax, yMax))) @GetMapping("/subscribe") diff --git a/src/main/kotlin/io/spring/messenger/web/SseBroadcaster.kt b/src/main/kotlin/io/spring/messenger/web/SseBroadcaster.kt new file mode 100644 index 0000000..67baf1b --- /dev/null +++ b/src/main/kotlin/io/spring/messenger/web/SseBroadcaster.kt @@ -0,0 +1,25 @@ +package io.spring.messenger.web + +import org.springframework.http.MediaType +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter +import java.util.* +import java.util.Collections.synchronizedSet + +class SseBroadcaster { + + private var sseEmitters = synchronizedSet(HashSet()); + + fun subscribe(): SseEmitter { + val sseEmitter: SseEmitter = SseEmitter() + sseEmitter.onCompletion({ this.sseEmitters.remove(sseEmitter) }); + this.sseEmitters.add(sseEmitter); + return sseEmitter + } + + fun send(o:Any) { + synchronized (sseEmitters) { + sseEmitters.iterator().forEach { it.send(o, MediaType.APPLICATION_JSON) } + } + } +} + diff --git a/src/main/kotlin/io/spring/messenger/web/UserController.kt b/src/main/kotlin/io/spring/messenger/web/UserController.kt index 0905216..5b01b49 100644 --- a/src/main/kotlin/io/spring/messenger/web/UserController.kt +++ b/src/main/kotlin/io/spring/messenger/web/UserController.kt @@ -15,15 +15,16 @@ class UserController @Autowired constructor(val repository: UserRepository) { fun findAll() = repository.findAll() @PostMapping - fun create(@RequestBody user: User) = repository.create(user) + fun create(@RequestBody u: User) = repository.create(u) @PutMapping("/{userName}/location/{x},{y}") @ResponseStatus(NO_CONTENT) fun updateLocation(@PathVariable userName:String, @PathVariable x: Double, @PathVariable y: Double) = repository.updateLocation(userName, Point(x, y)) @GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}") - fun findByBoundingBox(@PathVariable userName:String, @PathVariable xMin:Double, - @PathVariable yMin:Double, @PathVariable xMax:Double, @PathVariable yMax:Double) + fun findByBoundingBox(@PathVariable userName:String, + @PathVariable xMin:Double, @PathVariable yMin:Double, + @PathVariable xMax:Double, @PathVariable yMax:Double) = repository.findByBoundingBox(PGbox2d(Point(xMin, yMin), Point(xMax, yMax))) } \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 40830bb..0000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS postgis; - -CREATE TABLE IF NOT EXISTS "users" ( - user_name text PRIMARY KEY, - first_name text, - last_name text, - location GEOMETRY(Point, 4326) -); -CREATE INDEX IF NOT EXISTS users_gix - ON "users" - USING GIST (location); - - -CREATE TABLE IF NOT EXISTS messages ( - id SERIAL PRIMARY KEY, - content text NOT NULL, - author text REFERENCES users(user_name), - location GEOMETRY(Point, 4326) -); -CREATE INDEX IF NOT EXISTS messages_gix - ON messages - USING GIST (location); \ No newline at end of file diff --git a/src/main/resources/static/map.js b/src/main/resources/static/map.js index 66639d1..eb5b510 100644 --- a/src/main/resources/static/map.js +++ b/src/main/resources/static/map.js @@ -38,7 +38,7 @@ var centerDefined = false; geolocation.on("change:position", function () { var coordinates = geolocation.getPosition(); $.ajax({ - url: "/user/" + $('#select-user').val() + "swhite/location/" + coordinates[0] + "," + coordinates[1], type: "PUT" + url: "/user/" + $('#select-user').val() + "/location/" + coordinates[0] + "," + coordinates[1], type: "PUT" }); if (!centerDefined) { view.setCenter(coordinates);