diff --git a/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesAware.scala b/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesAware.scala new file mode 100644 index 0000000..f9ad741 --- /dev/null +++ b/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesAware.scala @@ -0,0 +1,128 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.scala.beans.factory.config + +import org.springframework.context.EmbeddedValueResolverAware +import org.springframework.util.StringValueResolver +import scala.collection.immutable.StringLike +import scala.language.dynamics + +/** + * Beans extending this trait have automatic access to application's properties. + * Trait introduces variable `$` of type [[org.springframework.scala.beans.factory.config.DynamicPropertyResource]] + * which allows easy access to the properties. + * + * __''Important notice:''__ Because of the fact that [[http://static.springsource.org/spring/docs/3.2.x/javadoc-api/org/springframework/util/StringValueResolver.html StringValueResolver]] + * is injected to the trait after the bean is constructed, references to properties can + * only be used in lazy initialized value definitions or in methods. + * + * == Referencing properties == + * + * There are two ways of reading property value: + * + * 1. by string representation passed to `$` - for example `$("my.property.name")` + * 1. by dynamic properties of `$` - for example `$.my.property.name` (which is equivalent to point 1.) + * + * Examples: + * {{{ + * lazy val version:String = $.app.version + * lazy val revision:Int = $("app.revision").toInt + * lazy val debugMode:Boolean = $.`throw`.spear.toBoolean + * }}} + * In both cases, the value of the expression is of type [[org.springframework.scala.beans.factory.config.Property]] + * which, when needed, is implicitly converted to [[http://docs.oracle.com/javase/7/docs/api/java/lang/String.html String]]. + * What's more, [[org.springframework.scala.beans.factory.config.Property]] + * extends [[http://www.scala-lang.org/api/current/#scala.collection.immutable.StringLike StringLike]] + * trait giving it additional type conversion capabilities by `toBoolean()`, `toByte()`, `toShort()`, + * `toInt()`, `toLong()`, `toFloat()` and `toDouble()` methods. + * + * __''Important notice:''__ When property key contains scala keywords, they should be + * surrounded with backticks when using second access method mentioned above. Example: + * `$.`throw`.`new`.exception` which is equivalent to `$("throw.new.exception")`. + * + * When property is not found, default Spring action that occurs when accessing nonexistent + * properties is triggered (for example may throw [[http://docs.oracle.com/javase/7/docs/api/java/lang/IllegalArgumentException.html IllegalArgumentException]] + * or return unescaped value like `${my.property.name}`). + * + * @author Maciej Zientarski + * @since 1.0 + */ +trait PropertiesAware extends EmbeddedValueResolverAware { + protected[config] var $: DynamicPropertyResource = new NotConfiguredDynamicPropertyResource + + implicit def propertyToString(property: Property) = property.toString + + override def setEmbeddedValueResolver(resolver: StringValueResolver) { + $ = new DynamicPropertyResource(resolver) + } +} + +/** + * Represents `key` and `value` of application property. Returned by + * [[org.springframework.scala.beans.factory.config.DynamicPropertyResource]]. + * Provides utility methods to convert `value` `toBoolean()`, `toByte()`, `toShort()`, + * `toInt()`, `toLong()`, `toFloat()`, `toDouble()` and `toString()`. + * + * If `value` for the `key` is not defined then [[http://docs.oracle.com/javase/7/docs/api/java/lang/IllegalArgumentException.html IllegalArgumentException]] + * is thrown on type conversion attempt. + * + * @author Maciej Zientarski + * @since 1.0 + */ +class Property(key: String, val value: Option[String], tryToResolve: (String => Property)) extends Dynamic with StringLike[Property] { + + def selectDynamic(subKey: String) = tryToResolve(s"$key.$subKey") + + override protected[this] def newBuilder = null + + override def seq: IndexedSeq[Char] = toString + + override def slice(from: Int, until: Int) = new Property(key, Option(toString.slice(from, until)), tryToResolve) + + override def toString = value.getOrElse(throw new IllegalArgumentException(s"Could not resolve placeholder '$key'")) +} + +/** + * @author Maciej Zientarski + * @since 1.0 + */ +private[config] class DynamicPropertyResource(resolver: StringValueResolver) extends Dynamic { + private def tryToResolve(name: String): Property = { + val resolved = Option( + try { + resolver.resolveStringValue("${%s}".format(name)) + } catch { + case iae: IllegalArgumentException => null + case e: Exception => throw e + } + ) + + new Property(name, resolved, tryToResolve) + } + + def selectDynamic(path: String) = tryToResolve(path) + + def apply(path: String) = tryToResolve(path) +} + +private[config] class NotConfiguredDynamicPropertyResource extends DynamicPropertyResource(null) { + private val message = "StringValueResolver was not injected yet. You should reference properties by lazy-initialized value definitions or inside functions." + + override def selectDynamic(path: String) = throw new IllegalStateException(message) + + override def apply(path: String) = throw new IllegalStateException(message) +} \ No newline at end of file diff --git a/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesResolver.scala b/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesResolver.scala new file mode 100644 index 0000000..ad929f7 --- /dev/null +++ b/src/main/scala/org/springframework/scala/beans/factory/config/PropertiesResolver.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.scala.beans.factory.config + +import org.springframework.scala.context.function.FunctionalConfiguration +import org.springframework.context.support.GenericApplicationContext +import org.springframework.beans.factory.support.BeanNameGenerator + +/** + * Defines additional [[org.springframework.scala.context.function.FunctionalConfiguration]] + * elements to simplify access to application properties. + * + * Introduces variable `$` of type [[org.springframework.scala.beans.factory.config.DynamicPropertyResource]] + * which can be used to read application properties in one of the following ways: + * + * 1. referencing by string - for example `$("logs.folder")` + * 1. referencing by dynamic properties of [[org.springframework.scala.beans.factory.config.DynamicPropertyResource]] + * and [[org.springframework.scala.beans.factory.config.Property]] - for example `$.logs.folder`. + * + * For more information see `Referencing properties` section of [[org.springframework.scala.beans.factory.config.PropertiesAware]] scaladoc. + * + * Note that this trait does not configure properties source, it only provides convenient + * way for accessing them. + * + * @author Maciej Zientarski + * @since 1.0 + */ +trait PropertiesResolver { + this: FunctionalConfiguration => + + implicit def propertyToString(property: Property) = property.toString + + onRegister((applicationContext: GenericApplicationContext, + beanNameGenerator: BeanNameGenerator) => { + bean("dynamicPropertyResolver") { + new PropertiesAware() {} + } + }) + + lazy val $ = (() => getBean[PropertiesAware]("dynamicPropertyResolver"))().$ +} diff --git a/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesAwareTest.scala b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesAwareTest.scala new file mode 100644 index 0000000..ef54025 --- /dev/null +++ b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesAwareTest.scala @@ -0,0 +1,196 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.scala.beans.factory.config + +import org.scalatest.FunSuite +import org.springframework.beans.factory.BeanCreationException + +/** + * @author Maciej Zientarski + * @since 1.0 + */ +class PropertiesAwareTest extends FunSuite with PropertiesTestUtils { + test("get property by string") { + //given test class + class TestedBean extends PropertiesAware { + def getProperty(path: String): String = $(path); + } + + //and context that initializes the class + val context = new TestConfig { + withProperties("we.are.the" -> "champions") + + bean("testedBean") { + new TestedBean() + } + } + + //when context is registered + registerContext(context) + + //and bean is found + val testedBean = applicationContext.getBean("testedBean", classOf[TestedBean]) + + //then it is possible to read properties + assert("champions".equals(testedBean.getProperty("we.are.the"))) + } + + test("get property by dynamic properties") { + //given test class + class TestedBean extends PropertiesAware { + lazy val theProperty: String = $.easy.come + } + + //and context that initializes the class + val context = new TestConfig { + withProperties("easy.come" -> "easy go") + + bean("testedBean") { + new TestedBean() + } + } + + //when context is registered + registerContext(context) + + //and bean is found + val testedBean = applicationContext.getBean("testedBean", classOf[TestedBean]) + + //then it is possible to read properties + assert("easy go".equals(testedBean.theProperty)) + } + + test("dynamic properties casting") { + //given test class + class TestedBean extends PropertiesAware { + lazy val int: Int = $.easy.int.toInt + + lazy val float: Float = $.easy.float.toFloat + + lazy val boolean: Boolean = $.easy.boolean.toBoolean + } + + //and context that initializes the class + val context = new TestConfig { + withProperties( + "easy.int" -> "17", + "easy.float" -> "1.3", + "easy.boolean" -> "true" + ) + + bean("testedBean") { + new TestedBean() + } + } + + //when context is registered + registerContext(context) + + //and bean is found + val testedBean = applicationContext.getBean("testedBean", classOf[TestedBean]) + + //then it is possible to read properties + assert(17 === testedBean.int) + assert(true === testedBean.boolean) + assert(1.3f === testedBean.float) + } + + test("get nonexistent property") { + //given test class + class TestedBean extends PropertiesAware { + lazy val someInt = $.i.am.not.here.toInt + } + + //and context that initializes the class + val context = new TestConfig { + withProperties("we.are.the" -> "champions") + + bean("testedBean") { + new TestedBean() + } + } + + //when context is registered + registerContext(context) + + //and bean is found + val testedBean = applicationContext.getBean("testedBean", classOf[TestedBean]) + intercept[IllegalArgumentException] { + testedBean.someInt + } + } + + test("no property resolver defined") { + //given test class + class TestedBean extends PropertiesAware { + lazy val someProperty: String = $.i.am.not.here + } + + //and context that initializes the class + val context = new TestConfig { + bean("testedBean") { + new TestedBean() + } + } + + //when context is registered + registerContext(context) + + //and bean is found + val testedBean = applicationContext.getBean("testedBean", classOf[TestedBean]) + assert("${i.am.not.here}".equals(testedBean.someProperty)) + } + + test("val instead of lazy val - by dynamic properties") { + //given test class + class TestedBean extends PropertiesAware { + val someProperty: String = $.i.am.not.here + } + + //and context that initializes the class + val context = new TestConfig { + bean("testedBean") { + new TestedBean() + } + } + + //then + intercept[BeanCreationException] { + registerContext(context) + } + } + + test("val instead of lazy val - by string") { + //given test class + class TestedBean extends PropertiesAware { + val someProperty: String = $("i.am.not.here") + } + + //and context that initializes the class + val context = new TestConfig { + bean("testedBean") { + new TestedBean() + } + } + + //then + intercept[BeanCreationException] { + registerContext(context) + } + } +} + diff --git a/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesResolverTest.scala b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesResolverTest.scala new file mode 100644 index 0000000..5812c1a --- /dev/null +++ b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesResolverTest.scala @@ -0,0 +1,160 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.scala.beans.factory.config + +import org.scalatest.{BeforeAndAfterEach, FunSuite} +import org.springframework.scala.context.function.{FunctionalConfiguration, FunctionalConfigApplicationContext} +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + +/** + * @author Maciej Zientarski + * @since 1.0 + */ +class PropertiesResolverTest extends FunSuite with PropertiesTestUtils { + + test("nested placeholder") { + //given + val context = new TestConfig { + withProperties("subject" -> "Frog", "predicate" -> "croaks", "sentence" -> "${subject} ${predicate}") + withPropertyValueDefinition { + $.sentence + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("Frog croaks".equals(propertyValue)) + } + + test("nested path") { + //given + val context = new TestConfig { + withProperties("dot.net" -> "sucks") + withPropertyValueDefinition { + $.dot.net + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("sucks".equals(propertyValue)) + } + + test("nested path with intermediate value available") { + //given + val context = new TestConfig { + withProperties("dot" -> "department of transportation", "dot.net" -> "sucks") + withPropertyValueDefinition { + $.dot.net + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("sucks".equals(propertyValue)) + } + + test("as string") { + //given + val context = new TestConfig { + withProperties("throw.new" -> "exception") + withPropertyValueDefinition { + $("throw.new") + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("exception".equals(propertyValue)) + } + + test("scala keywords") { + //given + val context = new TestConfig { + withProperties("throw.new" -> "exception") + withPropertyValueDefinition { + $.`throw`.`new` + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("exception".equals(propertyValue)) + } + + test("scala keyword in the middle of the path") { + //given + val context = new TestConfig { + withProperties("dont.throw.that" -> "exception") + withPropertyValueDefinition { + $.dont.`throw`.that + } + } + + //then + val propertyValue: String = registerContext(context).propertyValue + + assert("exception".equals(propertyValue)) + } + + + test("nonexistent property") { + //given + val context = new TestConfig { + withProperties("dont" -> "touch me") + withPropertyValueDefinition { + $.ok + } + } + + intercept[IllegalArgumentException] { + registerContext(context).propertyValue + } + } + + test("more nested nonexistent property") { + //given + val context = new TestConfig { + withProperties("im" -> "having a good time") + withPropertyValueDefinition { + $.dont.stop.me.now + } + } + + intercept[IllegalArgumentException] { + registerContext(context).propertyValue + } + } + + test("no property resolver defined") { + //given + val context = new TestConfig { + withPropertyValueDefinition { + $.dont.stop.me.now + } + } + + val propertyValue: String = registerContext(context).propertyValue + + assert("${dont.stop.me.now}" === propertyValue) + } +} diff --git a/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesTestUtils.scala b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesTestUtils.scala new file mode 100644 index 0000000..c511d19 --- /dev/null +++ b/src/test/scala/org/springframework/scala/beans/factory/config/PropertiesTestUtils.scala @@ -0,0 +1,66 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * Licensed 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. + */ + +package org.springframework.scala.beans.factory.config + +import org.springframework.scala.context.function.{FunctionalConfigApplicationContext, FunctionalConfiguration} +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +import org.scalatest.{BeforeAndAfterEach, FunSuite} + +/** + * @author Maciej Zientarski + * @since 1.0 + */ +private[config] trait PropertiesTestUtils extends FunSuite with BeforeAndAfterEach { + var applicationContext: FunctionalConfigApplicationContext = _ + + override protected def beforeEach() { + applicationContext = new FunctionalConfigApplicationContext() + } + + def registerContext(context: TestConfig): TestConfig = { + applicationContext.registerConfigurations(context) + applicationContext.refresh() + context + } + + implicit def map2Properties(map: Map[String, String]): java.util.Properties = { + val props = new java.util.Properties() + map foreach { + case (key, value) => props.put(key, value) + } + props + } + + class TestConfig extends FunctionalConfiguration with PropertiesResolver { + def withProperties(properties: (String, String)*) { + bean("propertiesConfig") { + new PropertyPlaceholderConfigurer { + setProperties(Map(properties: _*)) + } + } + } + + def withPropertyValueDefinition(function: => Any) { + bean("propertyValue") { + function + } + } + + def propertyValue: String = applicationContext.getBean("propertyValue", classOf[Property]) + } + +}