commit 70736063937479cfff6de5881434321804294cc3 Author: Reinhard Prechtl Date: Fri Jun 9 12:20:53 2017 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff081ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ +logs/ diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..9cc84ea Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c315043 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip diff --git a/domain/pom.xml b/domain/pom.xml new file mode 100644 index 0000000..05f6b6e --- /dev/null +++ b/domain/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + domain + + + de.rpr.mycity + spring-kotlin-jpa + 0.0.1-SNAPSHOT + + + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hibernate + hibernate-validator + + + org.hibernate + hibernate-java8 + + + + + \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/DoubleAttributeConverter.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/DoubleAttributeConverter.kt new file mode 100644 index 0000000..f5c6789 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/DoubleAttributeConverter.kt @@ -0,0 +1,19 @@ +package de.rpr.mycity.domain + +import java.math.BigDecimal +import javax.persistence.AttributeConverter + +class DoubleAttributeConverter : AttributeConverter { + + override fun convertToDatabaseColumn(attribute: Double?): BigDecimal? { + return if (attribute != null) { + BigDecimal(attribute) + } else { + null + } + } + + override fun convertToEntityAttribute(dbData: BigDecimal?): Double? { + return dbData?.toDouble() + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/DefaultCityService.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/DefaultCityService.kt new file mode 100644 index 0000000..b40a0a0 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/DefaultCityService.kt @@ -0,0 +1,42 @@ +package de.rpr.mycity.domain.city + +import de.rpr.mycity.domain.city.api.CityService +import de.rpr.mycity.domain.city.api.dto.CityDto +import de.rpr.mycity.domain.city.api.dto.CreateCityDto +import de.rpr.mycity.domain.city.api.dto.UpdateCityDto +import de.rpr.mycity.domain.city.entity.CityEntity +import de.rpr.mycity.domain.city.repository.CityRepository +import org.slf4j.Logger +import org.springframework.stereotype.Service +import javax.transaction.Transactional + +@Service +@Transactional +internal class DefaultCityService(val cityRepo: CityRepository, val log: Logger) : CityService { + + override fun retrieveCity(cityId: String): CityDto? { + log.debug("Retrieving city: {}", cityId) + + return cityRepo.findOne(cityId)?.toDto() + } + + override fun retrieveCities(): List { + log.debug("Retrieving cities") + + return cityRepo.findAll().map { it.toDto() } + } + + override fun updateCity(id: String, city: UpdateCityDto): CityDto? { + log.debug("Updating city: {} with data: {}", id, city) + + val currentCity = cityRepo.findOne(id) + return if (currentCity != null) cityRepo.save(CityEntity.fromDto(city, currentCity)).toDto() + else null + } + + override fun addCity(city: CreateCityDto): CityDto { + log.debug("Adding City: {}", city) + + return cityRepo.save(CityEntity.fromDto(city)).toDto() + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/InternalCityConfig.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/InternalCityConfig.kt new file mode 100644 index 0000000..da63645 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/InternalCityConfig.kt @@ -0,0 +1,14 @@ +package de.rpr.mycity.domain.city + +import de.rpr.mycity.domain.city.entity.CityEntity +import de.rpr.mycity.domain.city.repository.CityRepository +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.transaction.annotation.EnableTransactionManagement + +@Configuration +@EnableJpaRepositories(basePackageClasses = arrayOf(CityRepository::class)) +@EntityScan(basePackageClasses = arrayOf(CityEntity::class)) +@EnableTransactionManagement +internal class InternalCityConfig \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityConfig.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityConfig.kt new file mode 100644 index 0000000..578cc51 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityConfig.kt @@ -0,0 +1,9 @@ +package de.rpr.mycity.domain.city.api + +import de.rpr.mycity.domain.city.InternalCityConfig +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +@Configuration +@ComponentScan(basePackageClasses = arrayOf(InternalCityConfig::class)) +class CityConfig diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityService.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityService.kt new file mode 100644 index 0000000..7236a67 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/CityService.kt @@ -0,0 +1,16 @@ +package de.rpr.mycity.domain.city.api + +import de.rpr.mycity.domain.city.api.dto.CityDto +import de.rpr.mycity.domain.city.api.dto.CreateCityDto +import de.rpr.mycity.domain.city.api.dto.UpdateCityDto + +interface CityService { + + fun retrieveCity(cityId: String): CityDto? + + fun retrieveCities(): List + + fun addCity(city: CreateCityDto): CityDto + + fun updateCity(id: String, city: UpdateCityDto): CityDto? +} \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CityDto.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CityDto.kt new file mode 100644 index 0000000..1bc409b --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CityDto.kt @@ -0,0 +1,12 @@ +package de.rpr.mycity.domain.city.api.dto + +import de.rpr.mycity.domain.location.api.CoordinateDto +import java.time.LocalDateTime + +data class CityDto( + var id: String, + var name: String, + var description: String? = null, + var location: CoordinateDto, + var updatedAt: LocalDateTime, + var createdAt: LocalDateTime) \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CreateCityDto.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CreateCityDto.kt new file mode 100644 index 0000000..35b5ea1 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/CreateCityDto.kt @@ -0,0 +1,11 @@ +package de.rpr.mycity.domain.city.api.dto + +import de.rpr.mycity.domain.location.api.CoordinateDto +import org.hibernate.validator.constraints.NotEmpty +import javax.validation.Valid + +data class CreateCityDto( + @NotEmpty var id: String, + @NotEmpty var name: String, + var description: String? = null, + @Valid var location: CoordinateDto) \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/UpdateCityDto.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/UpdateCityDto.kt new file mode 100644 index 0000000..2c66bab --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/api/dto/UpdateCityDto.kt @@ -0,0 +1,8 @@ +package de.rpr.mycity.domain.city.api.dto + +import de.rpr.mycity.domain.location.api.CoordinateDto + +data class UpdateCityDto( + val name: String?, + val description: String?, + val location: CoordinateDto?) \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/entity/CityEntity.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/entity/CityEntity.kt new file mode 100644 index 0000000..dca234e --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/entity/CityEntity.kt @@ -0,0 +1,66 @@ +package de.rpr.mycity.domain.city.entity + +import de.rpr.mycity.domain.city.api.dto.CityDto +import de.rpr.mycity.domain.city.api.dto.CreateCityDto +import de.rpr.mycity.domain.city.api.dto.UpdateCityDto +import de.rpr.mycity.domain.location.jpa.Coordinate +import java.time.LocalDateTime +import javax.persistence.Embedded +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table + + +@Entity +@Table(name = "city") +internal data class CityEntity( + @Id val id: String? = null, + val name: String, + val description: String? = null, + @Embedded val location: Coordinate, + val updatedAt: LocalDateTime = LocalDateTime.now(), + val createdAt: LocalDateTime = LocalDateTime.now()) { + + // Default constructor for JPA + @Suppress("unused") + private constructor() : this( + name = "", + location = Coordinate.origin(), + updatedAt = LocalDateTime.MIN) + + fun toDto(): CityDto = CityDto( + id = this.id!!, + name = this.name, + description = this.description, + location = this.location.toDto(), + updatedAt = this.updatedAt, + createdAt = this.createdAt + ) + + companion object { + + fun fromDto(dto: CityDto) = CityEntity( + id = dto.id, + name = dto.name, + description = dto.description, + location = Coordinate.fromDto(dto.location), + updatedAt = dto.updatedAt, + createdAt = dto.createdAt) + + fun fromDto(dto: CreateCityDto) = CityEntity( + id = dto.id, + name = dto.name, + description = dto.description, + location = Coordinate(dto.location.longitude, dto.location.latitude)) + + fun fromDto(dto: UpdateCityDto, defaultCity: CityEntity) = CityEntity( + id = defaultCity.id!!, + name = dto.name ?: defaultCity.name, + description = dto.description ?: defaultCity.description, + location = if (dto.location != null) Coordinate(dto.location.longitude, dto.location.latitude) else defaultCity.location, + updatedAt = LocalDateTime.now(), + createdAt = defaultCity.createdAt) + + } + +} \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/city/repository/CityRepository.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/city/repository/CityRepository.kt new file mode 100644 index 0000000..ee71a35 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/city/repository/CityRepository.kt @@ -0,0 +1,10 @@ +package de.rpr.mycity.domain.city.repository + +import de.rpr.mycity.domain.city.entity.CityEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import javax.transaction.Transactional + +@Repository +@Transactional(Transactional.TxType.MANDATORY) +internal interface CityRepository : JpaRepository \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/location/api/CoordinateDto.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/location/api/CoordinateDto.kt new file mode 100644 index 0000000..5cde160 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/location/api/CoordinateDto.kt @@ -0,0 +1,10 @@ +package de.rpr.mycity.domain.location.api + +data class CoordinateDto( + val longitude: Double, + val latitude: Double) { + + companion object { + fun origin() = CoordinateDto(0.0, 0.0) + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/de/rpr/mycity/domain/location/jpa/Coordinate.kt b/domain/src/main/kotlin/de/rpr/mycity/domain/location/jpa/Coordinate.kt new file mode 100644 index 0000000..26506a8 --- /dev/null +++ b/domain/src/main/kotlin/de/rpr/mycity/domain/location/jpa/Coordinate.kt @@ -0,0 +1,23 @@ +package de.rpr.mycity.domain.location.jpa + +import de.rpr.mycity.domain.DoubleAttributeConverter +import de.rpr.mycity.domain.location.api.CoordinateDto +import javax.persistence.Convert +import javax.persistence.Embeddable + +@Embeddable +internal data class Coordinate( + @Convert(converter = DoubleAttributeConverter::class) val longitude: Double, + @Convert(converter = DoubleAttributeConverter::class) val latitude: Double) { + + private constructor() : this(0.0, 0.0) + + fun toDto(): CoordinateDto = CoordinateDto(this.longitude, this.latitude) + + companion object { + + fun origin() = Coordinate() + + fun fromDto(dto: CoordinateDto): Coordinate = Coordinate(dto.longitude, dto.latitude) + } +} \ No newline at end of file diff --git a/domain/src/main/resources/db/migration/V1.0__initial_structure.sql b/domain/src/main/resources/db/migration/V1.0__initial_structure.sql new file mode 100644 index 0000000..32b8f0e --- /dev/null +++ b/domain/src/main/resources/db/migration/V1.0__initial_structure.sql @@ -0,0 +1,12 @@ +CREATE SEQUENCE hibernate_sequence; + +CREATE TABLE city ( + id VARCHAR(15) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + description VARCHAR(1024) NULL, + latitude NUMERIC NOT NULL, + longitude NUMERIC NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL +); + diff --git a/domain/src/test/kotlin/de/rpr/mycity/domain/city/DefaultCityServiceTest.kt b/domain/src/test/kotlin/de/rpr/mycity/domain/city/DefaultCityServiceTest.kt new file mode 100644 index 0000000..1e55754 --- /dev/null +++ b/domain/src/test/kotlin/de/rpr/mycity/domain/city/DefaultCityServiceTest.kt @@ -0,0 +1,115 @@ +package de.rpr.mycity.domain.city + +import de.rpr.mycity.domain.city.api.CityConfig +import de.rpr.mycity.domain.city.api.CityService +import de.rpr.mycity.domain.city.api.dto.CreateCityDto +import de.rpr.mycity.domain.city.api.dto.UpdateCityDto +import de.rpr.mycity.domain.city.entity.CityEntity +import de.rpr.mycity.domain.city.repository.CityRepository +import de.rpr.mycity.domain.location.api.CoordinateDto +import de.rpr.mycity.domain.location.jpa.Coordinate +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.JUnitSoftAssertions +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.slf4j.Logger +import org.springframework.beans.factory.InjectionPoint +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Scope +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit4.SpringRunner +import javax.transaction.Transactional + +@RunWith(SpringRunner::class) +@ContextConfiguration(classes = arrayOf( + DefaultCityServiceTest.Config::class, + CityConfig::class)) +@Transactional +@DataJpaTest +internal class DefaultCityServiceTest { + + class Config { + + @Bean + @Scope("prototype") + fun logger(injectionPoint: InjectionPoint): Logger = mock(Logger::class.java) + } + + @Autowired + lateinit var service: CityService + @Autowired + lateinit var repository: CityRepository + + @get:Rule + var softly = JUnitSoftAssertions() + + @Test + fun `'retrieveCities' should retrieve empty list if repository doesn't contain entities`() { + assertThat(service.retrieveCities()).isEmpty() + } + + @Test + fun `'retrieveCity' should return null if city for cityId doesnt exist`() { + assertThat(service.retrieveCity("invalid")).isNull() + } + + @Test + fun `'retrieveCity' should map existing entity from repository`() { + repository.save(CityEntity("city", "cityname", "description", Coordinate(1.0, -1.0))) + + val result = service.retrieveCity("city") + softly.assertThat(result?.id).isNotNull + softly.assertThat(result?.name).isEqualTo("cityname") + softly.assertThat(result?.description).isEqualTo("description") + softly.assertThat(result?.location).isEqualTo(CoordinateDto(1.0, -1.0)) + } + + @Test + fun `'retrieveCities' should map entity from repository`() { + repository.save(CityEntity("city", "cityname", "description", Coordinate(1.0, -1.0))) + val result = service.retrieveCities() + + softly.assertThat(result).hasSize(1) + result.forEach { + softly.assertThat(it.id).isNotNull + softly.assertThat(it.name).isEqualTo("cityname") + softly.assertThat(it.description).isEqualTo("description") + softly.assertThat(it.location).isEqualTo(CoordinateDto(1.0, -1.0)) + } + } + + @Test + fun `'addCity' should return created entity`() { + val (id, name, description, location) = service.addCity(CreateCityDto("id", "name", "description", CoordinateDto(1.0, 1.0))) + softly.assertThat(id).isEqualTo("id") + softly.assertThat(name).isEqualTo("name") + softly.assertThat(description).isEqualTo("description") + softly.assertThat(location).isEqualTo(CoordinateDto(1.0, 1.0)) + } + + @Test + fun `'updateCity' should update existing values`() { + val existingCity = repository.save(CityEntity("city", "cityname", "description", Coordinate(1.0, -1.0))) + val result = service.updateCity(existingCity.id!!, UpdateCityDto("new name", "new description", CoordinateDto(-1.0, -1.0))) + softly.assertThat(result).isNotNull + softly.assertThat(result?.id).isEqualTo(existingCity.id) + softly.assertThat(result?.name).isEqualTo("new name") + softly.assertThat(result?.description).isEqualTo("new description") + softly.assertThat(result?.location).isEqualTo(CoordinateDto(-1.0, -1.0)) + } + + @Test + fun `'updateCity' shouldn't update null values`() { + val existingCity = repository.save(CityEntity("city", "cityname", "description", Coordinate(1.0, -1.0))) + val result = service.updateCity(existingCity.id!!, UpdateCityDto(null, null, null)) + softly.assertThat(result).isNotNull + softly.assertThat(result?.id).isEqualTo(existingCity.id) + softly.assertThat(result?.name).isEqualTo("cityname") + softly.assertThat(result?.description).isEqualTo("description") + softly.assertThat(result?.location).isEqualTo(CoordinateDto(1.0, -1.0)) + } +} \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..5bf251c --- /dev/null +++ b/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..019bd74 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fd7f17a --- /dev/null +++ b/pom.xml @@ -0,0 +1,138 @@ + + + 4.0.0 + + de.rpr.mycity + spring-kotlin-jpa + 0.0.1-SNAPSHOT + pom + + + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.5.3.RELEASE + + + + + true + UTF-8 + UTF-8 + 1.8 + 1.1.2-2 + 1.1.2-4 + 2.8.9 + + + + web + domain + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.jetbrains.kotlin + kotlin-stdlib-jre8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.flywaydb + flyway-core + 4.2.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.nhaarman + mockito-kotlin + 1.4.0 + test + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 1.8 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + diff --git a/web/pom.xml b/web/pom.xml new file mode 100644 index 0000000..d82de42 --- /dev/null +++ b/web/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + de.rpr.mycity + spring-kotlin-jpa + 0.0.1-SNAPSHOT + + + web + jar + + + + de.rpr.mycity + domain + ${project.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-hateoas + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + org.postgresql + postgresql + + + com.h2database + h2 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/web/src/main/kotlin/de/rpr/mycity/Application.kt b/web/src/main/kotlin/de/rpr/mycity/Application.kt new file mode 100644 index 0000000..5e4774c --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/Application.kt @@ -0,0 +1,33 @@ +package de.rpr.mycity + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.InjectionPoint +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Scope + +@SpringBootApplication +class Application { + + @Bean + @Scope("prototype") + fun logger(injectionPoint: InjectionPoint): Logger = LoggerFactory.getLogger(injectionPoint.methodParameter.containingClass) + + @Bean + fun objectMapper(): ObjectMapper { + val mapper = ObjectMapper().registerModule(KotlinModule()) + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + return mapper + } + +} + +fun main(args: Array) { + SpringApplication.run(Application::class.java, *args) +} + diff --git a/web/src/main/kotlin/de/rpr/mycity/web/CityController.kt b/web/src/main/kotlin/de/rpr/mycity/web/CityController.kt new file mode 100644 index 0000000..543e6e1 --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/web/CityController.kt @@ -0,0 +1,80 @@ +package de.rpr.mycity.web + +import de.rpr.mycity.domain.city.api.CityService +import de.rpr.mycity.domain.city.api.dto.CreateCityDto +import de.rpr.mycity.domain.city.api.dto.UpdateCityDto +import de.rpr.mycity.web.CITIES_PATH +import de.rpr.mycity.web.resource.CityResource +import org.slf4j.Logger +import org.springframework.hateoas.Resources +import org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo +import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn +import org.springframework.http.HttpEntity +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.web.util.UriComponentsBuilder + +@RestController +@RequestMapping( + value = CITIES_PATH, + produces = arrayOf( + MediaType.APPLICATION_JSON_VALUE, + MediaType.TEXT_XML_VALUE, + MediaType.APPLICATION_XML_VALUE)) +class CityController(val cityService: CityService, val log: Logger) { + + @GetMapping + fun retrieveCities(): HttpEntity> { + log.debug("Retrieving cities") + + val result = cityService.retrieveCities() + return ResponseEntity.ok(Resources(result.map { CityResource.fromDto(it) })) + } + + + @GetMapping("{id}") + fun retrieveCity(@PathVariable("id") cityId: String): HttpEntity { + log.debug("Retrieving city: {}", cityId) + + val result = cityService.retrieveCity(cityId) + if (result != null) { + val resource = CityResource.fromDto(result) + resource.add(linkTo(methodOn(this::class.java).retrieveCity(result.id)).withSelfRel()) + return ResponseEntity.ok(resource) + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build() + } + } + + @PostMapping(consumes = arrayOf( + MediaType.APPLICATION_JSON_VALUE, + MediaType.TEXT_XML_VALUE, + MediaType.APPLICATION_XML_VALUE)) + fun addCity(@RequestBody city: CreateCityDto, uriBuilder: UriComponentsBuilder): HttpEntity { + log.debug("Request to add a city") + + val result = cityService.addCity(city) + val resource = CityResource.fromDto(result) + resource.add(linkTo(methodOn(this::class.java).retrieveCity(result.id)).withSelfRel()) + return ResponseEntity + .created(uriBuilder.path("$CITIES_PATH/{id}").buildAndExpand(result.id).toUri()) + .body(resource) + } + + @PutMapping("{id}") + fun updateCity(@PathVariable("id") cityId: String, @RequestBody city: UpdateCityDto): HttpEntity { + log.debug("Request to update city: {}", cityId) + + val result = cityService.updateCity(cityId, city) + if (result != null) { + val resource = CityResource.fromDto(result) + resource.add(linkTo(methodOn(this::class.java).retrieveCity(result.id)).withSelfRel()) + return ResponseEntity.ok(resource) + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build() + } + } +} + diff --git a/web/src/main/kotlin/de/rpr/mycity/web/IndexController.kt b/web/src/main/kotlin/de/rpr/mycity/web/IndexController.kt new file mode 100644 index 0000000..36b4396 --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/web/IndexController.kt @@ -0,0 +1,11 @@ +package de.rpr.mycity.web + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class IndexController { + + @GetMapping + fun index() = "index" +} \ No newline at end of file diff --git a/web/src/main/kotlin/de/rpr/mycity/web/PathMappings.kt b/web/src/main/kotlin/de/rpr/mycity/web/PathMappings.kt new file mode 100644 index 0000000..053e302 --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/web/PathMappings.kt @@ -0,0 +1,8 @@ +package de.rpr.mycity.web + +/* +This file contains path constants for the controllers + */ + +internal const val CITIES_PATH = "cities" +internal const val VENUES_PATH = "venues" \ No newline at end of file diff --git a/web/src/main/kotlin/de/rpr/mycity/web/conversion/CoordinateType.kt b/web/src/main/kotlin/de/rpr/mycity/web/conversion/CoordinateType.kt new file mode 100644 index 0000000..466e041 --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/web/conversion/CoordinateType.kt @@ -0,0 +1,56 @@ +package de.rpr.mycity.web.conversion + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import de.rpr.mycity.domain.location.api.CoordinateDto + + +@JsonSerialize(using = CoordinateSerializer::class) +@JsonDeserialize(using = CoordinateDeserializer::class) +data class CoordinateType( + val longitude: Double, + val latitude: Double) { + companion object { + + fun fromDto(dto: CoordinateDto): CoordinateType = CoordinateType( + longitude = dto.longitude, + latitude = dto.latitude + ) + + } +} + +class CoordinateSerializer : JsonSerializer() { + + override fun serialize( + value: CoordinateType?, + gen: JsonGenerator?, + serializers: SerializerProvider?) { + if (gen != null && value != null) { + gen.writeStartObject() + gen.writeNumberField("lat", value.latitude) + gen.writeNumberField("long", value.longitude) + gen.writeEndObject() + } + } + +} + +class CoordinateDeserializer : JsonDeserializer() { + override fun deserialize( + p: JsonParser?, + ctxt: DeserializationContext?): CoordinateType? { + if (p != null && ctxt != null) { + val node: JsonNode = p.codec.readTree(p) + return CoordinateType( + longitude = node.get("long").doubleValue(), + latitude = node.get("lat").doubleValue()) + } else { + return null + } + } + +} diff --git a/web/src/main/kotlin/de/rpr/mycity/web/resource/CityResource.kt b/web/src/main/kotlin/de/rpr/mycity/web/resource/CityResource.kt new file mode 100644 index 0000000..a163839 --- /dev/null +++ b/web/src/main/kotlin/de/rpr/mycity/web/resource/CityResource.kt @@ -0,0 +1,27 @@ +package de.rpr.mycity.web.resource + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import de.rpr.mycity.domain.city.api.dto.CityDto +import de.rpr.mycity.web.conversion.CoordinateType +import org.springframework.hateoas.ResourceSupport + +data class CityResource +@JsonCreator +constructor( + @JsonProperty("id") val _id: String, + @JsonProperty("name") val name: String, + @JsonProperty("desc") val description: String?, + @JsonProperty("loc") val location: CoordinateType) : ResourceSupport() { + + companion object { + + fun fromDto(dto: CityDto): CityResource = + CityResource( + _id = dto.id, + name = dto.name, + description = dto.description, + location = CoordinateType.fromDto(dto.location) + ) + } +} \ No newline at end of file diff --git a/web/src/main/resources/application.properties b/web/src/main/resources/application.properties new file mode 100644 index 0000000..2e86727 --- /dev/null +++ b/web/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:6432/mycity +spring.datasource.username=mycity +spring.datasource.password=mycity +spring.jpa.hibernate.ddl-auto=validate \ No newline at end of file diff --git a/web/src/main/resources/log4j2.yml b/web/src/main/resources/log4j2.yml new file mode 100644 index 0000000..79b3b45 --- /dev/null +++ b/web/src/main/resources/log4j2.yml @@ -0,0 +1,25 @@ +Configutation: + name: Default + + Appenders: + + Console: + name: Console_Appender + target: SYSTEM_OUT + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" + + Loggers: + + Root: + level: INFO + AppenderRef: + - ref: Console_Appender + + Logger: + - name: org.springframework + level: INFO + + Logger: + - name: de.rpr.mycity + level: DEBUG \ No newline at end of file diff --git a/web/src/main/resources/templates/index.html b/web/src/main/resources/templates/index.html new file mode 100644 index 0000000..8fb00cd --- /dev/null +++ b/web/src/main/resources/templates/index.html @@ -0,0 +1,12 @@ + + + + Spring Framework Guru + + + +

Hello

+ +

Fellow Spring Framework Gurus!!!

+ + \ No newline at end of file diff --git a/web/src/test/kotlin/de/rpr/mycity/ApplicationTests.kt b/web/src/test/kotlin/de/rpr/mycity/ApplicationTests.kt new file mode 100644 index 0000000..833253f --- /dev/null +++ b/web/src/test/kotlin/de/rpr/mycity/ApplicationTests.kt @@ -0,0 +1,18 @@ +package de.rpr.mycity + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringBootTest +@TestPropertySource(locations = arrayOf("/test-application.properties")) +class ApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/web/src/test/kotlin/de/rpr/mycity/web/CityControllerTest.kt b/web/src/test/kotlin/de/rpr/mycity/web/CityControllerTest.kt new file mode 100644 index 0000000..2b8badd --- /dev/null +++ b/web/src/test/kotlin/de/rpr/mycity/web/CityControllerTest.kt @@ -0,0 +1,93 @@ +package de.rpr.mycity.web + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.reset +import com.nhaarman.mockito_kotlin.verify +import de.rpr.mycity.domain.city.api.CityService +import de.rpr.mycity.domain.city.api.dto.CityDto +import de.rpr.mycity.domain.location.api.CoordinateDto +import org.hamcrest.CoreMatchers.equalTo +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.http.MediaType +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import java.time.LocalDateTime + +@RunWith(SpringRunner::class) +@WebMvcTest(CityController::class) +class CityControllerTest { + + @Autowired + lateinit var mockMvc: MockMvc + @Autowired + lateinit var cityService: CityService + + @TestConfiguration + class Config { + + @Bean + fun cityService(): CityService = Mockito.mock(CityService::class.java) + } + + @Before + fun setup() { + reset(cityService) + } + + @Test + fun `Retrieving an unknown city should result in status 404`() { + mockMvc.perform(get("/cities/unknown") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound) + } + + @Test + fun `Creating a city with an invalid request body should result in status 400 `() { + mockMvc.perform(post("/cities") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest) + } + + @Test + fun `Creating a city with a valid request body should result in status 201 and a location header`() { + `when`(cityService.addCity(any())) + .thenReturn(CityDto("city", "cityname", null, CoordinateDto.origin(), LocalDateTime.now(), LocalDateTime.now())) + mockMvc.perform(post("/cities") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"id\":\"city\", \"name\":\"Cityname\", \"location\": {\"longitude\": 0.0, \"latitude\": 0.0}}")) + .andExpect(status().isCreated) + .andExpect(header().string("location", "http://localhost/cities/city")) + verify(cityService).addCity(any()) + } + + @Test + fun `Successfully updating a city should result in status 200`() { + `when`(cityService.updateCity(any(), any())) + .thenReturn(CityDto("cityId", "name", "description", CoordinateDto(1.0, -1.0), LocalDateTime.now(), LocalDateTime.now())) + + mockMvc.perform(put("/cities/cityId") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"Cityname\", \"location\": {\"longitude\": 0.0, \"latitude\": 0.0}}")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.id", equalTo("cityId"))) + .andExpect(jsonPath("$.name", equalTo("name"))) + .andExpect(jsonPath("$.desc", equalTo("description"))) + .andExpect(jsonPath("$.loc.long", equalTo(1.0))) + .andExpect(jsonPath("$.loc.lat", equalTo(-1.0))) + + verify(cityService).updateCity(eq("cityId"), any()) + } +} + diff --git a/web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/web/src/test/resources/test-application.properties b/web/src/test/resources/test-application.properties new file mode 100644 index 0000000..23a4f8d --- /dev/null +++ b/web/src/test/resources/test-application.properties @@ -0,0 +1,5 @@ +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url= +spring.datasource.username= +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=validate \ No newline at end of file