Skip to content

Instantly share code, notes, and snippets.

@pshirshov
Last active November 18, 2015 18:32
Show Gist options
  • Save pshirshov/44799f4461c43f8e83c2 to your computer and use it in GitHub Desktop.
Save pshirshov/44799f4461c43f8e83c2 to your computer and use it in GitHub Desktop.
OSGi complex config
// flat <-> hierarchical conversions
import java.util
import java.util.regex.Pattern
import scala.collection.JavaConversions._
import com.typesafe.config.{Config, ConfigFactory, ConfigList, ConfigObject, ConfigUtil}
import com.typesafe.scalalogging.StrictLogging
/**
*/
case class LoadedConfig(effective: Config, reference: Map[String, Any], notPersisted: Set[String])
object ConfigUtils extends StrictLogging {
private val listElementPattern = Pattern.compile("^.*\\.\\d+(|\\..*)$")
private val digits = Pattern.compile("^[1-9]+$")
def loadConfigAndSetDefaultValues(clazz: Class[_], properties: java.util.Map[String, Object]) = {
val systemProperties = ConfigFactory.systemProperties()
val plainReference = ConfigFactory.parseResources(clazz.getClassLoader, "reference.conf")
val refMap = asMap(plainReference)
logger.debug(s"Given config::\n$properties")
logger.debug(s"Reference:\n$refMap")
// ConfigFactory.defaultReference(clazz.getClassLoader)
val config = composeConfig(plainReference, ConfigFactory.parseMap(properties), systemProperties)
logger.trace(s"Module configuration:\n${config.root().render()}")
import scala.collection.JavaConversions._
val diff = refMap.keySet.diff(properties.keySet())
val missingKeys = diff.filter(!listElementPattern.matcher(_).matches())
val missingLists = diff.filter(k => listElementPattern.matcher(k).matches() && isFirstElement(k))
logger.debug(s"""These keys are set in reference but missing in config: $missingKeys""")
logger.debug(s"""These lists are set in reference but missing in config: $missingLists""")
val allMissing = missingKeys ++ missingLists
LoadedConfig(config, refMap, allMissing)
}
private def isFirstElement(key:String) = {
val parts: util.List[String] = ConfigUtil.splitPath(key)
parts.forall(!digits.matcher(_).matches())
}
def asMap(config:Config) = {
toMap(None, config.root())
}
private def toMap(top: Option[String], config:ConfigObject): Map[String, Any] = {
config.flatMap {
case (key, value: ConfigObject) =>
toMap(Some(key), value).map {
case (k1, v1) =>
addTopKey(Some(key), k1, v1)
}
case (key, value: ConfigList) =>
value.zipWithIndex.flatMap {
case (lv:ConfigObject, index) =>
val submap = toMap(None, lv)
submap.map {
case (skey, svalue) =>
(s"$key.$index.$skey", svalue)
}.toSeq
case (lv, index) =>
Map(s"$key.$index" -> lv.unwrapped())
}.toMap
case (key, value) =>
Map(key -> value.unwrapped())
}.toMap
}
private def addTopKey(top:Option[String], key:String, value:Any) = {
top match {
case None =>
(s"$key", value)
case Some(topkey) =>
(s"$topkey.$key", value)
}
}
def composeConfig(reference: Config, config:Config, systemProperties:Config): Config = {
val result: Config = systemProperties
.withFallback(config)
.withFallback(systemProperties)
.withFallback(reference)
.resolve()
result.checkValid(reference)
result
}
}
// OSGi util
import scala.collection.JavaConversions._
import aQute.bnd.annotation.component._
import com.typesafe.config.Config
import com.typesafe.scalalogging.StrictLogging
import org.osgi.framework.{BundleContext, FrameworkUtil}
import org.osgi.service.cm.ConfigurationAdmin
import org.osgi.service.component.ComponentContext
/**
*/
trait ConfigurableComponent[T] extends StrictLogging {
@Activate
final def start(bundleContext: BundleContext, componentContext: ComponentContext, properties: java.util.Map[String, Object]) = {
val config = ConfigUtils.loadConfigAndSetDefaultValues(getClass, properties)
writeDefaults(config, properties)
val tsConfig = transformConfig(config.effective)
onStart(bundleContext, componentContext, tsConfig)
}
@Deactivate
final def stop() = {
onStop()
}
protected def writeDefaults(config: LoadedConfig, properties: java.util.Map[String, Object]) = {
if (config.notPersisted.nonEmpty) {
val pid = if (!getClass.isAnnotationPresent(classOf[Component])) {
getClass.getCanonicalName
} else {
val name = getClass.getAnnotation(classOf[Component]).name()
if (name.isEmpty) {
getClass.getCanonicalName
} else {
name
}
}
val notPersistedReferenceValues = if (properties.keySet().forall(_.startsWith("component."))) {
logger.info(s"Configuration $pid is empty")
config.reference
} else {
config.notPersisted.map {
k => (k, config.reference(k))
}.toMap
}
logger.info(s"Updating configuration $pid with reference values: $notPersistedReferenceValues")
val bundleContext = FrameworkUtil.getBundle(getClass).getBundleContext
val configServiceReference = bundleContext.getServiceReference(classOf[ConfigurationAdmin])
val configurationAdmin = bundleContext.getService(configServiceReference)
val configuration = configurationAdmin.getConfiguration(pid)
configuration.update(asJavaDictionary(scala.collection.mutable.Map(notPersistedReferenceValues.toSeq:_*)))
}
}
protected def onStart(bundleContext: BundleContext, componentContext: ComponentContext, config:T): Unit
protected def onStop(): Unit
// TODO: default proxy-generating implementation
protected def transformConfig(config:Config): T
}
// sample
import java.net.URI
import scala.concurrent.duration.Duration
import aQute.bnd.annotation.metatype.Meta
// TODO: replicate OCD-like service
@Meta.OCD
trait DNSConfig {
def bindTo(): Iterable[URI]
def serviceDomain(): String
def zoneTtl(): Duration
}
@Component(immediate = true, configurationPolicy = ConfigurationPolicy.optional)
class DNSServerComponent extends StrictLogging with ConfigurableComponent[DNSConfig] {
var server: Option[DNSServer] = None
var discovery: Option[Discovery] = None
@Reference(dynamic = true)
def setDiscovery(provider: Discovery): Unit = {
logger.debug(s"Cluster state installed: $provider")
this.discovery = Option(provider)
}
def unsetDiscovery(provider: Discovery): Unit = {
logger.debug(s"Cluster state removed: $provider")
this.discovery = None
}
override def onStart(bundleContext: BundleContext, componentContext: ComponentContext, dnsConfig: DNSConfig): Unit = {
stop()
if (discovery.isEmpty) {
logger.error("No ClusterState defined")
throw new IllegalArgumentException("No ClusterState defined")
}
server = Some(new DNSServer(discovery.get, dnsConfig))
try {
server.foreach(_.start())
} catch {
case t:Throwable =>
logger.error("Can't start DNS server", t)
}
}
override protected def onStop(): Unit = {
try {
server.foreach(_.stop())
} catch {
case t:Throwable =>
logger.error("Can't stop DNS server", t)
}
server = None
}
override protected def transformConfig(config: Config): DNSConfig = {
import scala.collection.JavaConversions._
val dnsConfig = new DNSConfig {
override def bindTo(): Iterable[URI] = {
// scala bug
/*import scala.collection.JavaConverters._
val slist = new java.util.ArrayList[String]()
slist.asScala.toSet.map(s => new URI(s))*/
val urisSet = config.getStringList("discovery.dns.bind_to").toSet
urisSet.map(s => new URI(s))
}
override def serviceDomain(): String =
config.getString("discovery.dns.domain")
override def zoneTtl(): Duration =
Duration.fromNanos(config.getDuration("discovery.dns.zone_ttl").toNanos)
}
dnsConfig
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment