diff --git a/build.sbt b/build.sbt index 463d74f..acdc972 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -version := "0.9.0" +version := "0.9.1" name := "dsl-client-scala" diff --git a/core/src/main/scala/com/dslplatform/api/patterns/ServiceLocator.scala b/core/src/main/scala/com/dslplatform/api/patterns/ServiceLocator.scala index 7b7b995..a011d28 100644 --- a/core/src/main/scala/com/dslplatform/api/patterns/ServiceLocator.scala +++ b/core/src/main/scala/com/dslplatform/api/patterns/ServiceLocator.scala @@ -1,9 +1,8 @@ package com.dslplatform.api.patterns -import scala.reflect.runtime.universe.TypeTag import scala.reflect.ClassTag -import java.lang.reflect.Type -import java.lang.reflect.ParameterizedType +import scala.reflect.runtime.universe.TypeTag +import java.lang.reflect.{ParameterizedType, Type} /** * Service for resolving other services. @@ -18,7 +17,7 @@ trait ServiceLocator { /** * Resolve a service registered in the locator. * - * @param clazz class or interface + * @param tpe class or interface * @return registered implementation */ def resolve[T](tpe: Type): T diff --git a/http/src/main/scala/com/dslplatform/api/client/Bootstrap.scala b/http/src/main/scala/com/dslplatform/api/client/Bootstrap.scala index 60f978b..71cde39 100644 --- a/http/src/main/scala/com/dslplatform/api/client/Bootstrap.scala +++ b/http/src/main/scala/com/dslplatform/api/client/Bootstrap.scala @@ -1,18 +1,18 @@ package com.dslplatform.api.client +import java.util.{Collections, Properties} + import com.dslplatform.api.patterns.PersistableRepository import com.dslplatform.api.patterns.SearchableRepository import com.dslplatform.api.patterns.ServiceLocator import com.dslplatform.api.patterns.Repository -import java.io.InputStream -import java.io.FileInputStream +import java.io.{InputStream, FileInputStream} -import org.slf4j.LoggerFactory +import org.slf4j.{Logger, LoggerFactory} -import java.util.concurrent.Executors -import java.util.concurrent.ExecutorService -import scala.concurrent.ExecutionContext +import java.util.concurrent.{TimeUnit, AbstractExecutorService, Executors, ExecutorService} +import scala.concurrent.{ExecutionContextExecutorService, ExecutionContext} /** * DSL client Java initialization. @@ -36,31 +36,82 @@ object Bootstrap { staticLocator } + /** + * Initialize service locator using provided project.ini stream. + * + * @param inputStream stream for project.props + * @return initialized service locator + * @throws IOException in case of failure to read stream + */ + def init(inputStream: InputStream): ServiceLocator = { + val properties = new Properties() + properties.load(inputStream) + init(properties) + } /** * Initialize service locator using provided project.ini stream. * - * @param iniStream stream for project.ini - * @return initialized service locator - * @throws IOException in case of failure to read stream + * @param properties properties + * @return initialized service locator + * @throws IOException in case of failure to read stream + */ + def init(properties: Properties): ServiceLocator = init(properties, Map.empty[Object, AnyRef]) + + /** + * Initialize service locator using provided project.ini stream. + * + * @param properties properties + * @param initialComponents Map of initial components Logger, ExecutionContext + * or HttpHeaderProvider + * @return initialized service locator + * @throws IOException in case of failure to read stream */ - def init(iniStream: InputStream): ServiceLocator = { - val threadPool = Executors.newCachedThreadPool() - val locator = - new MapServiceLocator(LoggerFactory.getLogger("dsl-client-http")) - .register[ProjectSettings](new ProjectSettings(iniStream)) - .register[JsonSerialization] - .register[ExecutorService](threadPool) - .register[ExecutionContext](ExecutionContext.fromExecutorService(threadPool)) - .register[HttpClient] - .register[ApplicationProxy, HttpApplicationProxy] - .register[CrudProxy, HttpCrudProxy] - .register[DomainProxy, HttpDomainProxy] - .register[StandardProxy, HttpStandardProxy] - .register[ReportingProxy, HttpReportingProxy] - .register(classOf[Repository[_]], classOf[ClientRepository[_]]) - .register(classOf[SearchableRepository[_]], classOf[ClientSearchableRepository[_]]) - .register(classOf[PersistableRepository[_]], classOf[ClientPersistableRepository[_]]) + def init(properties: Properties, initialComponents: Map[Object, AnyRef]): ServiceLocator = { + + val locator = new MapServiceLocator( + initialComponents ++ + (if (initialComponents.contains(classOf[Logger])) + Map.empty + else Map(classOf[Logger] -> LoggerFactory.getLogger("dsl-client-scala"))) + ) + locator.register[Properties](properties) + + (initialComponents.get(classOf[ExecutionContext]), initialComponents.get(classOf[ExecutorService])) match { + case (Some(ec), None) => + val ecc = ec.asInstanceOf[ExecutionContext] + val threadPool = new AbstractExecutorService with ExecutionContextExecutorService { + override def prepare(): ExecutionContext = ecc + override def isShutdown = false + override def isTerminated = false + override def shutdown() = () + override def shutdownNow() = Collections.emptyList[Runnable] + override def execute(runnable: Runnable): Unit = ecc execute runnable + override def reportFailure(t: Throwable): Unit = ecc reportFailure t + override def awaitTermination(length: Long, unit: TimeUnit): Boolean = false + } + locator.register[ExecutorService](threadPool) + case (None, Some(tp)) => + locator.register[ExecutionContext](ExecutionContext.fromExecutorService(tp.asInstanceOf[ExecutorService])) + case _ => + val tp = Executors.newCachedThreadPool() + locator.register[ExecutorService](tp) + locator.register[ExecutionContext](ExecutionContext.fromExecutorService(tp)) + } + if (!initialComponents.contains(classOf[HttpHeaderProvider])) { + locator.register[HttpHeaderProvider](new SettingsHeaderProvider(properties)) + } + locator + .register[JsonSerialization] + .register[HttpClient] + .register[ApplicationProxy, HttpApplicationProxy] + .register[CrudProxy, HttpCrudProxy] + .register[DomainProxy, HttpDomainProxy] + .register[StandardProxy, HttpStandardProxy] + .register[ReportingProxy, HttpReportingProxy] + .register(classOf[Repository[_]], classOf[ClientRepository[_]]) + .register(classOf[SearchableRepository[_]], classOf[ClientSearchableRepository[_]]) + .register(classOf[PersistableRepository[_]], classOf[ClientPersistableRepository[_]]) staticLocator = locator locator @@ -69,16 +120,26 @@ object Bootstrap { /** * Initialize service locator using provided dsl-project.ini path. * - * @param iniPath path to project.ini + * @param propertiesPath path to project.ini * @return initialized service locator * @throws IOException in case of failure to read project.ini */ - def init(iniPath: String): ServiceLocator = { - val iniStream: InputStream = new FileInputStream(iniPath) + def init(propertiesPath: String, initialComponents: Map[Object, AnyRef] = Map.empty[Object, AnyRef]): ServiceLocator = { + val inputStream: InputStream = { + val file: java.io.File = new java.io.File(propertiesPath) + if (file.exists()) { + new FileInputStream(file) + } else { + getClass.getResourceAsStream(propertiesPath) + } + } + if (inputStream == null) throw new RuntimeException(s"$propertiesPath was not found in the file system or the classpath.") try { - init(iniStream) + val properties = new Properties() + properties.load(inputStream) + init(properties, initialComponents) } finally { - iniStream.close() + inputStream.close() } } } diff --git a/http/src/main/scala/com/dslplatform/api/client/HttpClient.scala b/http/src/main/scala/com/dslplatform/api/client/HttpClient.scala index b459f27..b64470f 100644 --- a/http/src/main/scala/com/dslplatform/api/client/HttpClient.scala +++ b/http/src/main/scala/com/dslplatform/api/client/HttpClient.scala @@ -1,5 +1,7 @@ package com.dslplatform.api.client +import java.util.Properties + import com.dslplatform.api.patterns.ServiceLocator import java.io.IOException @@ -48,39 +50,25 @@ private[client] object HttpClientUtil { class HttpClient( locator: ServiceLocator, - projectSettings: ProjectSettings, + properties: Properties, json: JsonSerialization, + authHeaders: HttpHeaderProvider, logger: Logger, executorService: java.util.concurrent.ExecutorService) { import HttpClientUtil._ - private val remoteUrl = Option(projectSettings.get("api-url")).getOrElse( - throw new RuntimeException("Missing api-url from the properties.") - ) - private val domainPrefix = Option(projectSettings.get("package-name")).getOrElse { - logger.warn("Could not find package-name in the properties defaulting to \"\"") - "" - } + private val remoteUrl = Option(properties.getProperty("api-url")) + .getOrElse(throw new RuntimeException("Missing api-url from the properties.")) + private val domainPrefix = Option(properties.getProperty("package-name")) + .getOrElse{throw new RuntimeException("Missing package-name from the properties.")} private val domainPrefixLength = if (domainPrefix.length > 0) domainPrefix.length + 1 else 0 - private val basicAuth = makeBasicAuth + private val auth = authHeaders.getHeaders.mapValues(Set(_)) private val MimeType = "application/json" private [client] implicit val ec = ExecutionContext.fromExecutorService(executorService) private val commonHeaders: Headers = Map( "Accept" -> Set(MimeType), "Content-Type" -> Set(MimeType) - ) ++ makeBasicAuth - - private def makeBasicAuth: Headers = { - val username = projectSettings.get("username") - val password = projectSettings.get("project-id") - - if (username == null || password == null) Map.empty - else { - val token = username + ':' + password - val basicAuth = "Basic " + new String(Base64.encode(token.getBytes("UTF-8"))) - Map("Authorization" -> Set(basicAuth)) - } - } + ) ++ auth private[client] def getDslName[T](implicit ct: ClassTag[T]): String = getDslName(ct.runtimeClass) @@ -100,9 +88,9 @@ class HttpClient( username [{}] api: [{}] pid: [{}]""", - projectSettings.get("username"), - projectSettings.get("api-url"), - projectSettings.get("project-id")); + properties.get("username"), + properties.get("api-url"), + properties.get("project-id")); } private def makeNingHeaders(additionalHeaders: Map[String, Set[String]]): java.util.Map[String, java.util.Collection[String]] = { diff --git a/http/src/main/scala/com/dslplatform/api/client/HttpHeaderProvider.scala b/http/src/main/scala/com/dslplatform/api/client/HttpHeaderProvider.scala new file mode 100644 index 0000000..1f614f7 --- /dev/null +++ b/http/src/main/scala/com/dslplatform/api/client/HttpHeaderProvider.scala @@ -0,0 +1,5 @@ +package com.dslplatform.api.client + +trait HttpHeaderProvider { + def getHeaders: Map[String, String] +} diff --git a/http/src/main/scala/com/dslplatform/api/client/JsonSerialization.scala b/http/src/main/scala/com/dslplatform/api/client/JsonSerialization.scala index 7716bf3..aad7d64 100644 --- a/http/src/main/scala/com/dslplatform/api/client/JsonSerialization.scala +++ b/http/src/main/scala/com/dslplatform/api/client/JsonSerialization.scala @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.dslplatform.api.patterns.ServiceLocator import com.fasterxml.jackson.databind.JavaType -import com.fasterxml.jackson.module.scala.ser.CustomDefaultScalaModule class JsonSerialization(locator: ServiceLocator) { @@ -90,7 +89,7 @@ class JsonSerialization(locator: ServiceLocator) { private val serializationMapper = new ObjectMapper() - .registerModule(CustomDefaultScalaModule) + .registerModule(com.fasterxml.jackson.module.scala.DefaultScalaModule) //.registerModule(DefaultScalaModule) .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) .registerModule(serializationModule) diff --git a/http/src/main/scala/com/dslplatform/api/client/MapServiceLocator.scala b/http/src/main/scala/com/dslplatform/api/client/MapServiceLocator.scala index 39e38e9..3f0e620 100644 --- a/http/src/main/scala/com/dslplatform/api/client/MapServiceLocator.scala +++ b/http/src/main/scala/com/dslplatform/api/client/MapServiceLocator.scala @@ -8,18 +8,17 @@ import java.lang.reflect.Type import java.lang.reflect.TypeVariable import java.lang.reflect.ParameterizedType import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl -import scala.reflect.ClassTag +import scala.reflect.{ClassTag, classTag} import scala.reflect.runtime.universe._ import java.lang.reflect.Modifier -class MapServiceLocator(logger: Logger, cacheResult: Boolean = true) +class MapServiceLocator(initialComponents: Map[Object, AnyRef], cacheResult: Boolean = true) extends ServiceLocator { - private val components: MMap[Object, AnyRef] = - MMap( - classOf[ServiceLocator] -> this, - classOf[Logger] -> logger - ) + private val logger = initialComponents.get(classOf[Logger]).asInstanceOf[Some[Logger]] + .getOrElse(throw new RuntimeException("Logger was not provided with initial components.")) + + private val components: MMap[Object, AnyRef] = MMap.empty ++ initialComponents + (classOf[ServiceLocator] -> this) def register(target: Class[_], service: AnyRef): MapServiceLocator = { logger.trace("About to register class " + target.getName() + " " + service) @@ -129,7 +128,7 @@ class MapServiceLocator(logger: Logger, cacheResult: Boolean = true) val genType = ParameterizedTypeImpl.make(mt, args, null) val genClazz = genType.getRawType() tryConstruct(typ, genClazz.getConstructors()) - case _ => + case impl => Some(impl) } } diff --git a/http/src/main/scala/com/dslplatform/api/client/ProjectSettings.scala b/http/src/main/scala/com/dslplatform/api/client/ProjectSettings.scala deleted file mode 100644 index 11bcf4d..0000000 --- a/http/src/main/scala/com/dslplatform/api/client/ProjectSettings.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.dslplatform.api.client - -import java.io.InputStream -import java.util.Properties -/** - * Project.ini key->value pairs - * Stream to project.ini file - * - * @param iniStream project.ini stream - * @throws IOException in case of error reading stream - */ -class ProjectSettings(iniStream: InputStream) { - val properties: Properties = new Properties() - properties.load(iniStream) - - /** - * get value for provided property in project.ini - * property = value - * - * @param property key - * @return found value - */ - def get(property: String): String = { - properties.getProperty(property) - } -} diff --git a/http/src/main/scala/com/dslplatform/api/client/SettingsHeaderProvider.scala b/http/src/main/scala/com/dslplatform/api/client/SettingsHeaderProvider.scala new file mode 100644 index 0000000..65f1df1 --- /dev/null +++ b/http/src/main/scala/com/dslplatform/api/client/SettingsHeaderProvider.scala @@ -0,0 +1,24 @@ +package com.dslplatform.api.client + +import java.util.Properties + +import com.ning.http.util.Base64 + +class SettingsHeaderProvider(properties: Properties) extends HttpHeaderProvider { + + private val headers: Map[String, String] = Option(properties.getProperty("auth")).map { + auth => Map("Authorization" -> ("Basic " + auth)) + }.orElse { + Option(properties.getProperty("username")).flatMap { + username => + Option(properties.getProperty("project-id")).map { + projectId => + val token = username + ':' + projectId + val basicAuth = "Basic " + new String(Base64.encode(token.getBytes("UTF-8"))) + Map[String, String]("Authorization" -> basicAuth) + } + } + }.getOrElse(Map.empty[String, String]) + + override def getHeaders: Map[String, String] = headers +} diff --git a/http/src/test/resources/logback.xml b/http/src/test/resources/logback.xml new file mode 100644 index 0000000..4e5425b --- /dev/null +++ b/http/src/test/resources/logback.xml @@ -0,0 +1,10 @@ + + + + [%4p] [%d{HH:mm:ss}]: %m%n + + + + + + diff --git a/http/src/test/resources/test-project.props b/http/src/test/resources/test-project.props new file mode 100644 index 0000000..dc80456 --- /dev/null +++ b/http/src/test/resources/test-project.props @@ -0,0 +1,4 @@ +username=testUser@dsl-platform.com +project-id=test-password +api-url=https://dsl-platform.com/test +package-name=test.package \ No newline at end of file diff --git a/http/src/test/scala/AuthHeaderTest.scala b/http/src/test/scala/AuthHeaderTest.scala new file mode 100644 index 0000000..16c4333 --- /dev/null +++ b/http/src/test/scala/AuthHeaderTest.scala @@ -0,0 +1,65 @@ +import java.io.ByteArrayInputStream + +import com.dslplatform.api.client.{HttpHeaderProvider, SettingsHeaderProvider, HttpClient} +import org.specs2._ + +class AuthHeaderTest extends Specification { + + def is = s2""" + Header Provider is resolved from the ServiceLocator + provide with auth header $auth + provide with project id $pid + privide custom $custom + """ + + private val withAuthHeader = + """ + |auth=someAuth + |api-url=https://dsl-platform.com/test + |package-name=model + """.stripMargin + + private val withPidHeader = + """ + |username=user + |project-id=0-0-0-0-0 + |api-url=https://dsl-platform.com/test + |package-name=model + """.stripMargin + + def auth = { + val properties = new java.util.Properties() + properties.load(new ByteArrayInputStream(withAuthHeader.getBytes("UTF-8"))) + val locator = com.dslplatform.api.client.Bootstrap.init(properties) + try { + locator.resolve[SettingsHeaderProvider].getHeaders("Authorization").contains("someAuth") must beTrue + } finally { + locator.resolve[HttpClient].shutdown() + } + } + + def pid = { + val properties = new java.util.Properties() + properties.load(new ByteArrayInputStream(withPidHeader.getBytes("UTF-8"))) + val locator = com.dslplatform.api.client.Bootstrap.init(properties) + try { + locator.resolve[SettingsHeaderProvider].getHeaders.get("Authorization") must beSome + } finally { + locator.resolve[HttpClient].shutdown() + } + } + + def custom = { + val locator = com.dslplatform.api.client.Bootstrap.init("/test-project.props", + Map[Object, AnyRef](classOf[HttpHeaderProvider] -> new HttpHeaderProvider { + override def getHeaders: Map[String, String] = Map("Do" -> "More") + })) + try { + val headers: Map[String, String] = locator.resolve[HttpHeaderProvider].getHeaders + println(headers) + headers("Do") === "More" + } finally { + locator.resolve[HttpClient].shutdown() + } + } +} diff --git a/http/src/test/scala/BootstrapTest.scala b/http/src/test/scala/BootstrapTest.scala new file mode 100644 index 0000000..61a0baf --- /dev/null +++ b/http/src/test/scala/BootstrapTest.scala @@ -0,0 +1,56 @@ +import java.util.concurrent.{Executors, ExecutorService} + +import com.dslplatform.api.client.{ClientPersistableRepository, HttpClient} +import com.dslplatform.api.patterns.PersistableRepository +import org.slf4j.{Logger, LoggerFactory} +import org.specs2._ + +class BootstrapTest extends Specification { + + def is = s2""" + This specification checks the dependency management + resolve default $defaults + resolve provided logger $initial + resolve provided initial Executor $initialExecutor + resolve a repository $resolveRepository + """ + + def defaults = { + val locator = com.dslplatform.api.client.Bootstrap.init("/test-project.props") + try { + locator.resolve[HttpClient].isInstanceOf[HttpClient] must beTrue + } finally { + locator.resolve[HttpClient].shutdown() + } + } + + def initial = { + val logger = LoggerFactory.getLogger("test-logger") + val initialComponents: Map[Object, AnyRef] = Map(classOf[Logger] -> logger) + val locator = com.dslplatform.api.client.Bootstrap.init("/test-project.props", initialComponents) + try { + locator.resolve[Logger] mustEqual(logger) + } finally { + locator.resolve[HttpClient].shutdown() + } + } + + def resolveRepository = { + val locator = com.dslplatform.api.client.Bootstrap.init("/test-project.props") + try { + locator.resolveUnsafe[PersistableRepository[M.Agg]].isInstanceOf[ClientPersistableRepository[_]] must beTrue + } finally { + locator.resolve[HttpClient].shutdown() + } + } + + def initialExecutor = { + val initialComponents: Map[Object, AnyRef] = Map(classOf[ExecutorService] -> Executors.newCachedThreadPool()) + val locator = com.dslplatform.api.client.Bootstrap.init("/test-project.props", initialComponents) + try { + locator.resolve[HttpClient].isInstanceOf[HttpClient] must beTrue + } finally { + locator.resolve[HttpClient].shutdown() + } + } +} diff --git a/http/src/test/scala/M.Agg.scala b/http/src/test/scala/M.Agg.scala new file mode 100644 index 0000000..e7c1930 --- /dev/null +++ b/http/src/test/scala/M.Agg.scala @@ -0,0 +1,10 @@ +package M + +import com.dslplatform.api.patterns._ +import com.dslplatform.api.client._ + +class Agg extends AggregateRoot { + def URI = "uri_value" +} + +object Agg extends AggregateRootCompanion[Agg]{}