diff --git a/.gitignore b/.gitignore
index 6ea91077096..68afe18e0cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,22 @@
-**/target/
+target/
+src-gen/
+xtend-gen/
+bin/
+.antlr*
.metadata/
+*/plugin.xml_gen
+.DS_Store
+*.iml
+.idea/
-/*features*/*/src/main/history/
-maven-metadata-local.xml
dependency-reduced-pom.xml
-npm-debug.log
+bundles/antlr-generator-3.2.0-patch.jar
+bundles/org.openhab.core.model.*/META-INF/
+features/karaf/*/src/main/history
-bundles/org.openhab.ui.homebuilder/web/dist
-bundles/org.openhab.ui.homebuilder/npm_cache
-bundles/org.openhab.ui.homebuilder/web/node
+Californium.properties
+
+*.orig
+
+generated/
+userdata/
diff --git a/.travis.yml b/.travis.yml
index 3ad911c79e2..bedd61b0198 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,10 +3,20 @@ dist: trusty
language: java
jdk: oraclejdk8
+
+cache:
+ directories:
+ - $HOME/.m2
+ - $HOME/.p2
+ - extensions/ui/org.eclipse.smarthome.ui.paper/npm_cache
+
before_install: echo "MAVEN_OPTS='-Xms1g -Xmx2g -XX:PermSize=512m -XX:MaxPermSize=1g'" > ~/.mavenrc
+#$ mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+#$ mvn test -B
+
install:
- - echo 'mvn clean install -B -V 1> .build.stdout 2> .build.stderr' > .build.sh
+ - echo 'mvn clean verify -B -V -Dspotbugs.skip=true 1> .build.stdout 2> .build.stderr' > .build.sh
- chmod 0755 .build.sh
script:
- travis_wait 60 ./.build.sh
diff --git a/bom/compile-model/.project b/bom/compile-model/.project
new file mode 100644
index 00000000000..eb3143a80a2
--- /dev/null
+++ b/bom/compile-model/.project
@@ -0,0 +1,17 @@
+
+
+ org.openhab.core.bom.compile-model
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/compile-model/.settings/org.eclipse.core.resources.prefs b/bom/compile-model/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/compile-model/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/compile-model/.settings/org.eclipse.m2e.core.prefs b/bom/compile-model/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/compile-model/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/compile-model/pom.xml b/bom/compile-model/pom.xml
new file mode 100644
index 00000000000..a2189c0ddcc
--- /dev/null
+++ b/bom/compile-model/pom.xml
@@ -0,0 +1,134 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.compile-model
+ pom
+
+ openHAB Core :: BOM :: Compile Model
+
+
+ 2.12.0
+ 2.11.0
+ 1.3.21.201705291010
+ 2.9.1.201705291010
+
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.generator
+ ${xtext.version}
+ compile
+
+
+ org.eclipse.platform
+ org.eclipse.osgi
+
+
+
+
+
+
+ org.eclipse.emf
+ org.eclipse.emf.common
+ ${emf.1.version}
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore
+ ${emf.1.version}
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore.change
+ ${emf.3.version}
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore.xmi
+ ${emf.1.version}
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.codegen
+ ${emf.3.version}
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.codegen.ecore
+ ${emf.1.version}
+ compile
+
+
+
+
+ org.eclipse.emf
+ org.eclipse.emf.mwe2.launch
+ ${emf.mwe2.version}
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.common.types
+ ${xtext.version}
+ compile
+
+
+ org.eclipse.platform
+ org.eclipse.osgi
+
+
+
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase
+ ${xtext.version}
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase.lib
+ ${xtext.version}
+ compile
+
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase.ide
+ ${xtext.version}
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.ide
+ ${xtext.version}
+ compile
+
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext
+ ${xtext.version}
+ compile
+
+
+
+
+
diff --git a/bom/compile/.project b/bom/compile/.project
new file mode 100644
index 00000000000..50935ba0b9a
--- /dev/null
+++ b/bom/compile/.project
@@ -0,0 +1,17 @@
+
+
+ org.openhab.core.bom.compile
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/compile/.settings/org.eclipse.core.resources.prefs b/bom/compile/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/compile/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/compile/.settings/org.eclipse.m2e.core.prefs b/bom/compile/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/compile/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/compile/pom.xml b/bom/compile/pom.xml
new file mode 100644
index 00000000000..99be53f0dc6
--- /dev/null
+++ b/bom/compile/pom.xml
@@ -0,0 +1,271 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.compile
+ pom
+
+ openHAB Core :: BOM :: Compile
+ The dependencies that are used to compile the core bundles
+
+
+ 9.3.25.v20180904
+
+
+
+
+ org.osgi
+ osgi.core
+ 6.0.0
+
+
+ org.osgi
+ osgi.cmpn
+ 6.0.0
+
+
+ org.osgi
+ osgi.annotation
+ 6.0.0
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ compile
+
+
+ org.eclipse.jdt
+ org.eclipse.jdt.annotation
+ 2.2.100
+
+
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.2
+ compile
+
+
+
+
+ commons-collections
+ commons-collections
+ 3.2.1
+ compile
+
+
+ org.apache.commons
+ commons-exec
+ 1.1
+ compile
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.commons-httpclient
+ 3.1_7
+ compile
+
+
+ commons-io
+ commons-io
+ 2.2
+ compile
+
+
+ commons-lang
+ commons-lang
+ 2.6
+ compile
+
+
+
+
+ com.eclipsesource.jaxrs
+ publisher
+ 5.3.1
+ compile
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.3.1
+ compile
+
+
+
+
+ org.codehaus.jackson
+ jackson-core-asl
+ 1.9.2
+ compile
+
+
+ org.codehaus.jackson
+ jackson-mapper-asl
+ 1.9.2
+ compile
+
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-proxy
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-client
+ ${jetty.version}
+ compile
+
+
+
+
+ org.jmdns
+ jmdns
+ 3.5.5
+ compile
+
+
+
+
+ joda-time
+ joda-time
+ 2.9.2
+ compile
+
+
+
+
+ org.jupnp
+ org.jupnp
+ 2.5.1
+ compile
+
+
+
+
+ org.mapdb
+ mapdb
+ 1.0.9
+ compile
+
+
+
+
+ javax.measure
+ unit-api
+ 1.0
+ compile
+
+
+ tec.uom
+ uom-se
+ 1.0.8
+ compile
+
+
+ tec.uom.lib
+ uom-lib-common
+ 1.0.2
+ compile
+
+
+
+
+ org.eclipse.paho
+ org.eclipse.paho.client.mqttv3
+ 1.2.0
+ compile
+
+
+
+
+ org.quartz-scheduler
+ quartz
+ 2.2.1
+
+
+ org.quartz-scheduler
+ quartz-jobs
+ 2.2.1
+
+
+
+
+
+ org.vesalainen.comm
+ javaxcomm
+ 1.0.1
+ compile
+
+
+ com.neuronrobotics
+ nrjavaserial
+ 3.14.0
+ compile
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ compile
+
+
+
+
+ io.swagger
+ swagger-annotations
+ 1.5.5
+ compile
+
+
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+ 2.0.1
+ compile
+
+
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.xstream
+ 1.4.7_1
+ compile
+
+
+
+
+
+
diff --git a/bom/openhab-core/.project b/bom/openhab-core/.project
new file mode 100644
index 00000000000..0cd0af64372
--- /dev/null
+++ b/bom/openhab-core/.project
@@ -0,0 +1,17 @@
+
+
+ org.openhab.core.bom.esh-core
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/openhab-core/.settings/org.eclipse.core.resources.prefs b/bom/openhab-core/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/openhab-core/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/openhab-core/.settings/org.eclipse.m2e.core.prefs b/bom/openhab-core/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/openhab-core/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml
new file mode 100644
index 00000000000..b397e43a577
--- /dev/null
+++ b/bom/openhab-core/pom.xml
@@ -0,0 +1,531 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.openhab-core
+ pom
+
+ openHAB Core :: BOM :: openHAB Core
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.test
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.test.magic
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.boot
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.compat1x
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.karaf
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.thing
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.audio
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.voice
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.binding.xml
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.extension.sample
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.id
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.persistence
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.scheduler
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.semantics
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.thing.xml
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.transform
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.auth.jaas
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.auth.oauth2client
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.net
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.http
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.mdns
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.serial
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console.eclipse
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console.rfc147
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console.karaf
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.http.auth
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.http.auth.basic
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.monitor
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.auth
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.core
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.log
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.mdns
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.optimize
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.sitemap
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.sse
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest.voice
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.dbus
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.mqtt
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.serial.javacomm
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.serial.rxtx
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.serial.rxtx.rfc2217
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.transport.upnp
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.jetty.certificate
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.core
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery.mdns
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery.usbserial
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery.usbserial.linuxsysfs
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery.upnp
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.dispatch
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.serial
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.xml
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.module.script
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.module.media
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.module.script.rulesupport
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.rest
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.core
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.sitemap
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.item
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.item.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.item.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.persistence
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.persistence.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.script
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.rule
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.rule.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.script.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.sitemap.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.thing
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.thing.ide
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.lsp
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.persistence.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.rule.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.script.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.sitemap.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.model.thing.runtime
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.ui
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.ui.dashboard
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.ui.icon
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.storage.json
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.storage.mapdb
+ ${project.version}
+ compile
+
+
+
+
diff --git a/bom/pom.xml b/bom/pom.xml
new file mode 100644
index 00000000000..0e64de1f4b8
--- /dev/null
+++ b/bom/pom.xml
@@ -0,0 +1,28 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core
+ org.openhab.core.reactor
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ pom
+
+ openHAB Core :: BOM
+
+
+ compile
+ compile-model
+ runtime
+ runtime-index
+ test
+ test-index
+ openhab-core
+
+
+
diff --git a/bom/runtime-index/.classpath b/bom/runtime-index/.classpath
new file mode 100644
index 00000000000..5e8a55fefbd
--- /dev/null
+++ b/bom/runtime-index/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bom/runtime-index/.project b/bom/runtime-index/.project
new file mode 100644
index 00000000000..ba2559cc6f7
--- /dev/null
+++ b/bom/runtime-index/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.bom.runtime-index
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/runtime-index/.settings/org.eclipse.core.resources.prefs b/bom/runtime-index/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/runtime-index/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/runtime-index/.settings/org.eclipse.jdt.core.prefs b/bom/runtime-index/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bom/runtime-index/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bom/runtime-index/.settings/org.eclipse.m2e.core.prefs b/bom/runtime-index/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/runtime-index/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/runtime-index/pom.xml b/bom/runtime-index/pom.xml
new file mode 100644
index 00000000000..f90af1153a3
--- /dev/null
+++ b/bom/runtime-index/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.runtime-index
+
+ openHAB Core :: BOM :: Runtime Index
+
+
+
+ org.openhab.core.bom
+ org.openhab.core.bom.runtime
+ pom
+ compile
+ true
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+ biz.aQute.bnd
+ bnd-indexer-maven-plugin
+
+
+
+
+
diff --git a/bom/runtime/.project b/bom/runtime/.project
new file mode 100644
index 00000000000..ef6a1fe6040
--- /dev/null
+++ b/bom/runtime/.project
@@ -0,0 +1,17 @@
+
+
+ org.openhab.core.bom.runtime
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/runtime/.settings/org.eclipse.core.resources.prefs b/bom/runtime/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/runtime/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/runtime/.settings/org.eclipse.m2e.core.prefs b/bom/runtime/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/runtime/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
new file mode 100644
index 00000000000..9e93d3e88f0
--- /dev/null
+++ b/bom/runtime/pom.xml
@@ -0,0 +1,590 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.runtime
+ pom
+
+ openHAB Core :: BOM :: Runtime
+
+
+ 7.2.3
+ 9.4.11.v20180605
+
+
+
+
+ org.osgi.enroute
+ impl-index
+ 7.0.0
+ pom
+ compile
+
+
+ org.apache.felix
+ org.apache.felix.http.jetty
+
+
+
+
+
+ org.osgi.enroute
+ debug-bundles
+ 7.0.0
+ pom
+ compile
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ compile
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.0
+ compile
+
+
+ ch.qos.logback
+ logback-core
+ 1.2.0
+ compile
+
+
+ org.apache.felix
+ org.apache.felix.log
+ 1.2.0
+ compile
+
+
+ org.osgi
+ osgi.core
+
+
+
+
+ org.apache.felix
+ org.apache.felix.logback
+ 1.0.0
+ compile
+
+
+
+
+
+ commons-collections
+ commons-collections
+ 3.2.1
+ compile
+
+
+ org.apache.commons
+ commons-exec
+ 1.1
+ compile
+
+
+ commons-fileupload
+ commons-fileupload
+ 1.3.3
+ compile
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.commons-httpclient
+ 3.1_7
+ compile
+
+
+ commons-io
+ commons-io
+ 2.2
+ compile
+
+
+ commons-lang
+ commons-lang
+ 2.6
+ compile
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.3.1
+ compile
+
+
+ org.eclipse.orbit.bundles
+ com.google.gson
+ 2.7.0.v20170129-0911
+ compile
+
+
+
+
+ org.jmdns
+ jmdns
+ 3.5.5
+ compile
+
+
+
+
+ org.jupnp
+ org.jupnp
+ 2.5.1
+ compile
+
+
+
+
+ org.mapdb
+ mapdb
+ 1.0.9
+ compile
+
+
+
+
+ javax.measure
+ unit-api
+ 1.0
+ compile
+
+
+ tec.uom
+ uom-se
+ 1.0.8
+ compile
+
+
+ tec.uom.lib
+ uom-lib-common
+ 1.0.2
+ compile
+
+
+
+
+ org.apache.servicemix.bundles
+ org.apache.servicemix.bundles.xstream
+ 1.4.7_1
+ compile
+
+
+
+
+ org.ops4j.pax.web
+ pax-web-api
+ ${pax.web.version}
+ compile
+
+
+ org.ops4j.pax.web
+ pax-web-extender-whiteboard
+ ${pax.web.version}
+ compile
+
+
+ org.ops4j.pax.web
+ pax-web-jetty
+ ${pax.web.version}
+ compile
+
+
+ org.ops4j.pax.web
+ pax-web-jsp
+ ${pax.web.version}
+ compile
+
+
+ org.ops4j.pax.web
+ pax-web-runtime
+ ${pax.web.version}
+ compile
+
+
+ org.ops4j.pax.web
+ pax-web-spi
+ ${pax.web.version}
+ compile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-continuation
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-deploy
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-http
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-io
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-jaas
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-jmx
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-jndi
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-plus
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-proxy
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-rewrite
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-security
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-jaspi
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-servlets
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-util-ajax
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-webapp
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-api
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-client
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-common
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ javax-websocket-client-impl
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ javax-websocket-server-impl
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-server
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty.websocket
+ websocket-servlet
+ ${jetty.version}
+ compile
+
+
+ org.eclipse.jetty
+ jetty-xml
+ ${jetty.version}
+ compile
+
+
+
+
+ org.apache.xbean
+ xbean-bundleutils
+ 4.6
+ compile
+
+
+ org.apache.xbean
+ xbean-finder
+ 4.6
+ compile
+
+
+
+
+ com.google.guava
+ guava
+ 15.0
+ compile
+
+
+
+
+ de.maggu2810.p2redist
+ org.antlr.runtime
+ 3.2.0.v201101311130
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.common
+ 2.12.0
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore
+ 2.12.0
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore.change
+ 2.11.0
+ compile
+
+
+ org.eclipse.emf
+ org.eclipse.emf.ecore.xmi
+ 2.12.0
+ compile
+
+
+ org.eclipse.xtend
+ org.eclipse.xtend.lib
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtend
+ org.eclipse.xtend.lib.macro
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.common.types
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.ide
+ 2.14.0
+ compile
+
+
+ com.google.code.gson
+ gson
+
+
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.util
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase.ide
+ 2.14.0
+ compile
+
+
+ org.eclipse.xtext
+ org.eclipse.xtext.xbase.lib
+ 2.14.0
+ compile
+
+
+ org.glassfish.hk2
+ hk2-api
+ 2.4.0-b34
+ compile
+
+
+ org.glassfish.hk2.external
+ aopalliance-repackaged
+ 2.4.0-b34
+ compile
+
+
+ org.glassfish.hk2.external
+ javax.inject
+ 2.4.0-b34
+ compile
+
+
+ org.glassfish.hk2
+ hk2-locator
+ 2.4.0-b34
+ compile
+
+
+ org.glassfish.hk2
+ osgi-resource-locator
+ 1.0.1
+ compile
+
+
+ org.glassfish.hk2
+ hk2-utils
+ 2.4.0-b34
+ compile
+
+
+ log4j
+ log4j
+ 1.2.17
+ compile
+
+
+ org.ow2.asm
+ asm
+ 6.1.1
+ compile
+
+
+
+
+ joda-time
+ joda-time
+ 2.9.2
+ compile
+
+
+
+
+ com.eclipsesource.jaxrs
+ publisher
+ 5.3.1
+ compile
+
+
+
+
+
diff --git a/bom/test-index/.classpath b/bom/test-index/.classpath
new file mode 100644
index 00000000000..5e8a55fefbd
--- /dev/null
+++ b/bom/test-index/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bom/test-index/.project b/bom/test-index/.project
new file mode 100644
index 00000000000..0987fe9bc75
--- /dev/null
+++ b/bom/test-index/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.bom.test-index
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/test-index/.settings/org.eclipse.core.resources.prefs b/bom/test-index/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/test-index/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/test-index/.settings/org.eclipse.jdt.core.prefs b/bom/test-index/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bom/test-index/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bom/test-index/.settings/org.eclipse.m2e.core.prefs b/bom/test-index/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/test-index/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/test-index/pom.xml b/bom/test-index/pom.xml
new file mode 100644
index 00000000000..9622e3fc634
--- /dev/null
+++ b/bom/test-index/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.test-index
+
+ openHAB Core :: BOM :: Test Index
+
+
+
+ org.openhab.core.bom
+ org.openhab.core.bom.test
+ pom
+ compile
+ true
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+ biz.aQute.bnd
+ bnd-indexer-maven-plugin
+
+
+
+
+
diff --git a/bom/test/.project b/bom/test/.project
new file mode 100644
index 00000000000..e8aaf92c49c
--- /dev/null
+++ b/bom/test/.project
@@ -0,0 +1,17 @@
+
+
+ org.openhab.core.bom.test
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bom/test/.settings/org.eclipse.core.resources.prefs b/bom/test/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/bom/test/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/bom/test/.settings/org.eclipse.m2e.core.prefs b/bom/test/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bom/test/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bom/test/pom.xml b/bom/test/pom.xml
new file mode 100644
index 00000000000..f107384b0a4
--- /dev/null
+++ b/bom/test/pom.xml
@@ -0,0 +1,59 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bom
+ org.openhab.core.reactor.bom
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.bom.test
+ pom
+
+ openHAB Core :: BOM :: Test
+
+
+ 9.3.25.v20180904
+
+
+
+
+ org.osgi
+ osgi.enroute.junit.wrapper
+ 4.12.0
+
+
+ org.osgi
+ osgi.enroute.hamcrest.wrapper
+ 1.3.0
+
+
+ org.mockito
+ mockito-core
+ 2.13.0
+
+
+
+
+
+
+
+
+ org.codehaus.groovy
+ groovy
+ 2.5.5
+
+
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${jetty.version}
+ compile
+
+
+
+
diff --git a/bundles/org.openhab.core.audio/.classpath b/bundles/org.openhab.core.audio/.classpath
new file mode 100644
index 00000000000..372f1810d64
--- /dev/null
+++ b/bundles/org.openhab.core.audio/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.audio/.project b/bundles/org.openhab.core.audio/.project
new file mode 100644
index 00000000000..7ab717cfb50
--- /dev/null
+++ b/bundles/org.openhab.core.audio/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.audio
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.audio/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.audio/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..00777dc9bed
--- /dev/null
+++ b/bundles/org.openhab.core.audio/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/=UTF-8
+encoding/ESH-INF=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.audio/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.audio/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.audio/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.audio/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.audio/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.audio/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.audio/NOTICE b/bundles/org.openhab.core.audio/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/org.openhab.core.audio/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/org.openhab.core.audio/pom.xml b/bundles/org.openhab.core.audio/pom.xml
new file mode 100644
index 00000000000..09274f6ec3e
--- /dev/null
+++ b/bundles/org.openhab.core.audio/pom.xml
@@ -0,0 +1,34 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.audio
+
+ openHAB Core :: Bundles :: Audio
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.core
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.http
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java
new file mode 100644
index 00000000000..e2a38719328
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+/**
+ * General purpose audio exception
+ *
+ * @author Harald Kuhn - Initial API
+ * @author Kelly Davis - Modified to match discussion in #584
+ */
+public class AudioException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public AudioException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message Detail message
+ * @param cause The cause
+ */
+ public AudioException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public AudioException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public AudioException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java
new file mode 100644
index 00000000000..f6da5016148
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java
@@ -0,0 +1,440 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.util.Set;
+
+/**
+ * An audio format definition
+ *
+ * @author Harald Kuhn - Initial API
+ * @author Kelly Davis - Modified to match discussion in #584
+ * @author Kai Kreuzer - Moved class, included constants, added toString
+ */
+public class AudioFormat {
+
+ // generic mp3 format without any further constraints
+ public static final AudioFormat MP3 = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, null,
+ null, null);
+
+ // generic wav format without any further constraints
+ public static final AudioFormat WAV = new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED,
+ null, null, null, null);
+
+ // generic OGG format without any further constraints
+ public static final AudioFormat OGG = new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, null,
+ null, null, null);
+
+ // generic AAC format without any further constraints
+ public static final AudioFormat AAC = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, null, null,
+ null, null);
+
+ /**
+ * {@link AudioCodec} encoded data without any container header or footer,
+ * e.g. MP3 is a non-container format
+ */
+ public static final String CONTAINER_NONE = "NONE";
+
+ /**
+ * Microsofts wave container format
+ *
+ * @see WAV Format
+ * @see Supported codecs
+ * @see RIFF container format
+ */
+ public static final String CONTAINER_WAVE = "WAVE";
+
+ /**
+ * OGG container format
+ *
+ * @see OGG
+ */
+ public static final String CONTAINER_OGG = "OGG";
+
+ /**
+ * PCM Signed
+ *
+ * @see PCM Types
+ */
+ public static final String CODEC_PCM_SIGNED = "PCM_SIGNED";
+
+ /**
+ * PCM Unsigned
+ *
+ * @see PCM Types
+ */
+ public static final String CODEC_PCM_UNSIGNED = "PCM_UNSIGNED";
+
+ /**
+ * PCM A-law
+ *
+ * @see PCM Types
+ */
+ public static final String CODEC_PCM_ALAW = "ALAW";
+
+ /**
+ * PCM u-law
+ *
+ * @see PCM Types
+ */
+ public static final String CODEC_PCM_ULAW = "ULAW";
+
+ /**
+ * MP3 Codec
+ *
+ * @see MP3 Codec
+ */
+ public static final String CODEC_MP3 = "MP3";
+
+ /**
+ * Vorbis Codec
+ *
+ * @see Vorbis
+ */
+ public static final String CODEC_VORBIS = "VORBIS";
+
+ /**
+ * AAC Codec
+ */
+ public static final String CODEC_AAC = "AAC";
+
+ /**
+ * Codec
+ */
+ private final String codec;
+
+ /**
+ * Container
+ */
+ private final String container;
+
+ /**
+ * Big endian or little endian
+ */
+ private final Boolean bigEndian;
+
+ /**
+ * Bit depth
+ *
+ * @see Bit Depth
+ */
+ private final Integer bitDepth;
+
+ /**
+ * Bit rate
+ *
+ * @see Bit Rate
+ */
+ private final Integer bitRate;
+
+ /**
+ * Sample frequency
+ */
+ private final Long frequency;
+
+ /**
+ * Constructs an instance with the specified properties.
+ *
+ * Note that any properties that are null indicate that
+ * the corresponding AudioFormat allows any value for
+ * the property.
+ *
+ * Concretely this implies that if, for example, one
+ * passed null for the value of frequency, this would
+ * mean the created AudioFormat allowed for any valid
+ * frequency.
+ *
+ * @param container The container for the audio
+ * @param codec The audio codec
+ * @param bigEndian If the audo data is big endian
+ * @param bitDepth The bit depth of the audo data
+ * @param bitRate The bit rate of the audio
+ * @param frequency The frequency at which the audio was sampled
+ */
+ public AudioFormat(String container, String codec, Boolean bigEndian, Integer bitDepth, Integer bitRate,
+ Long frequency) {
+ this.container = container;
+ this.codec = codec;
+ this.bigEndian = bigEndian;
+ this.bitDepth = bitDepth;
+ this.bitRate = bitRate;
+ this.frequency = frequency;
+ }
+
+ /**
+ * Gets codec
+ *
+ * @return The codec
+ */
+ public String getCodec() {
+ return codec;
+ }
+
+ /**
+ * Gets container
+ *
+ * @return The container
+ */
+ public String getContainer() {
+ return container;
+ }
+
+ /**
+ * Is big endian?
+ *
+ * @return If format is big endian
+ */
+ public Boolean isBigEndian() {
+ return bigEndian;
+ }
+
+ /**
+ * Gets bit depth
+ *
+ * @see Bit Depth
+ * @return Bit depth
+ */
+ public Integer getBitDepth() {
+ return bitDepth;
+ }
+
+ /**
+ * Gets bit rate
+ *
+ * @see Bit Rate
+ * @return Bit rate
+ */
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ /**
+ * Gets frequency
+ *
+ * @return The frequency
+ */
+ public Long getFrequency() {
+ return frequency;
+ }
+
+ /**
+ * Determines if the passed AudioFormat is compatible with this AudioFormat.
+ *
+ * This AudioFormat is compatible with the passed AudioFormat if both have
+ * the same value for all non-null members of this instance.
+ */
+ public boolean isCompatible(AudioFormat audioFormat) {
+ if (audioFormat == null) {
+ return false;
+ }
+ if ((null != getContainer()) && (!getContainer().equals(audioFormat.getContainer()))) {
+ return false;
+ }
+ if ((null != getCodec()) && (!getCodec().equals(audioFormat.getCodec()))) {
+ return false;
+ }
+ if ((null != isBigEndian()) && (!isBigEndian().equals(audioFormat.isBigEndian()))) {
+ return false;
+ }
+ if ((null != getBitDepth()) && (!getBitDepth().equals(audioFormat.getBitDepth()))) {
+ return false;
+ }
+ if ((null != getBitRate()) && (!getBitRate().equals(audioFormat.getBitRate()))) {
+ return false;
+ }
+ if ((null != getFrequency()) && (!getFrequency().equals(audioFormat.getFrequency()))) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determines the best match between a list of audio formats supported by a source and a sink.
+ *
+ * @param inputs the supported audio formats of an audio source
+ * @param outputs the supported audio formats of an audio sink
+ * @return the best matching format or null, if source and sink are incompatible
+ */
+ public static AudioFormat getBestMatch(Set inputs, Set outputs) {
+ AudioFormat preferredFormat = getPreferredFormat(inputs);
+ if (preferredFormat != null) {
+ for (AudioFormat output : outputs) {
+ if (output.isCompatible(preferredFormat)) {
+ return preferredFormat;
+ } else {
+ for (AudioFormat input : inputs) {
+ if (output.isCompatible(input)) {
+ return input;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the first concrete AudioFormat in the passed set or a preferred one
+ * based on 16bit, 16KHz, big endian default
+ *
+ * @param audioFormats The AudioFormats from which to choose
+ * @return The preferred AudioFormat or null if none could be determined. A passed concrete format is preferred
+ * adding default values to an abstract AudioFormat in the passed set.
+ */
+ public static AudioFormat getPreferredFormat(Set audioFormats) {
+ // Return the first concrete AudioFormat found
+ for (AudioFormat currentAudioFormat : audioFormats) {
+ // Check if currentAudioFormat is abstract
+ if (null == currentAudioFormat.getCodec()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getContainer()) {
+ continue;
+ }
+ if (null == currentAudioFormat.isBigEndian()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getBitDepth()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getBitRate()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getFrequency()) {
+ continue;
+ }
+
+ // Prefer WAVE container
+ if (!currentAudioFormat.getContainer().equals("WAVE")) {
+ continue;
+ }
+
+ // As currentAudioFormat is concrete, use it
+ return currentAudioFormat;
+ }
+
+ // There's no concrete AudioFormat so we must create one
+ for (AudioFormat currentAudioFormat : audioFormats) {
+ // Define AudioFormat to return
+ AudioFormat format = currentAudioFormat;
+
+ // Not all Codecs and containers can be supported
+ if (null == format.getCodec()) {
+ continue;
+ }
+ if (null == format.getContainer()) {
+ continue;
+ }
+
+ // Prefer WAVE container
+ if (!format.getContainer().equals(AudioFormat.CONTAINER_WAVE)) {
+ continue;
+ }
+
+ // If required set BigEndian, BitDepth, BitRate, and Frequency to default values
+ if (null == format.isBigEndian()) {
+ format = new AudioFormat(format.getContainer(), format.getCodec(), new Boolean(true),
+ format.getBitDepth(), format.getBitRate(), format.getFrequency());
+ }
+ if (null == format.getBitDepth() || null == format.getBitRate() || null == format.getFrequency()) {
+ // Define default values
+ int defaultBitDepth = 16;
+ long defaultFrequency = 16384;
+
+ // Obtain current values
+ Integer bitRate = format.getBitRate();
+ Long frequency = format.getFrequency();
+ Integer bitDepth = format.getBitDepth();
+
+ // These values must be interdependent (bitRate = bitDepth * frequency)
+ if (null == bitRate) {
+ if (null == bitDepth) {
+ bitDepth = new Integer(defaultBitDepth);
+ }
+ if (null == frequency) {
+ frequency = new Long(defaultFrequency);
+ }
+ bitRate = new Integer(bitDepth.intValue() * frequency.intValue());
+ } else if (null == bitDepth) {
+ if (null == frequency) {
+ frequency = new Long(defaultFrequency);
+ }
+ bitDepth = new Integer(bitRate.intValue() / frequency.intValue());
+ } else if (null == frequency) {
+ frequency = new Long(bitRate.longValue() / bitDepth.longValue());
+ }
+
+ format = new AudioFormat(format.getContainer(), format.getCodec(), format.isBigEndian(), bitDepth,
+ bitRate, frequency);
+ }
+
+ // Return preferred AudioFormat
+ return format;
+ }
+
+ // Return null indicating failure
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof AudioFormat) {
+ AudioFormat format = (AudioFormat) obj;
+ if (!(null == getCodec() ? null == format.getCodec() : getCodec().equals(format.getCodec()))) {
+ return false;
+ }
+ if (!(null == getContainer() ? null == format.getContainer()
+ : getContainer().equals(format.getContainer()))) {
+ return false;
+ }
+ if (!(null == isBigEndian() ? null == format.isBigEndian() : isBigEndian().equals(format.isBigEndian()))) {
+ return false;
+ }
+ if (!(null == getBitDepth() ? null == format.getBitDepth() : getBitDepth().equals(format.getBitDepth()))) {
+ return false;
+ }
+ if (!(null == getBitRate() ? null == format.getBitRate() : getBitRate().equals(format.getBitRate()))) {
+ return false;
+ }
+ if (!(null == getFrequency() ? null == format.getFrequency()
+ : getFrequency().equals(format.getFrequency()))) {
+ return false;
+ }
+ return true;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((bigEndian == null) ? 0 : bigEndian.hashCode());
+ result = prime * result + ((bitDepth == null) ? 0 : bitDepth.hashCode());
+ result = prime * result + ((bitRate == null) ? 0 : bitRate.hashCode());
+ result = prime * result + ((codec == null) ? 0 : codec.hashCode());
+ result = prime * result + ((container == null) ? 0 : container.hashCode());
+ result = prime * result + ((frequency == null) ? 0 : frequency.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "AudioFormat [" + (codec != null ? "codec=" + codec + ", " : "")
+ + (container != null ? "container=" + container + ", " : "")
+ + (bigEndian != null ? "bigEndian=" + bigEndian + ", " : "")
+ + (bitDepth != null ? "bitDepth=" + bitDepth + ", " : "")
+ + (bitRate != null ? "bitRate=" + bitRate + ", " : "")
+ + (frequency != null ? "frequency=" + frequency : "") + "]";
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioHTTPServer.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioHTTPServer.java
new file mode 100644
index 00000000000..3f97d752fe4
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioHTTPServer.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import org.eclipse.smarthome.core.audio.internal.AudioServlet;
+
+/**
+ * This is an interface that is implemented by {@link AudioServlet} and which allows exposing audio streams through
+ * HTTP.
+ * Streams are only served a single time and then discarded.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public interface AudioHTTPServer {
+
+ /**
+ * Creates a relative url for a given {@link AudioStream} where it can be requested a single time.
+ * Note that the HTTP header only contains "Content-length", if the passed stream is an instance of
+ * {@link FixedLengthAudioStream}.
+ * If the client that requests the url expects this header field to be present, make sure to pass such an instance.
+ * Streams are closed after having been served.
+ *
+ * @param stream the stream to serve on HTTP
+ * @return the relative URL to access the stream starting with a '/'
+ */
+ String serve(AudioStream stream);
+
+ /**
+ * Creates a relative url for a given {@link AudioStream} where it can be requested multiple times within the given
+ * time frame.
+ * This method only accepts {@link FixedLengthAudioStream}s, since it needs to be able to create multiple concurrent
+ * streams from it, which isn't possible with a regular {@link AudioStream}.
+ * Streams are closed, once they expire.
+ *
+ * @param stream the stream to serve on HTTP
+ * @param seconds number of seconds for which the stream is available through HTTP
+ * @return the relative URL to access the stream starting with a '/'
+ */
+ String serve(FixedLengthAudioStream stream, int seconds);
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioManager.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioManager.java
new file mode 100644
index 00000000000..accc7b5cdd0
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioManager.java
@@ -0,0 +1,196 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.PercentType;
+
+/**
+ * This service provides functionality around audio services and is the central service to be used directly by others.
+ *
+ * @author Karel Goderis - Initial contribution and API
+ * @author Kai Kreuzer - removed unwanted dependencies
+ * @author Christoph Weitkamp - Added parameter to adjust the volume
+ * @author Wouter Born - Added methods for getting all sinks and sources
+ */
+@NonNullByDefault
+public interface AudioManager {
+
+ /**
+ * Name of the sub-directory of the config folder, holding sound files.
+ */
+ static final String SOUND_DIR = "sounds";
+
+ /**
+ * Plays the passed audio stream using the default audio sink.
+ *
+ * @param audioStream The audio stream to play or null if streaming should be stopped
+ */
+ void play(@Nullable AudioStream audioStream);
+
+ /**
+ * Plays the passed audio stream on the given sink.
+ *
+ * @param audioStream The audio stream to play or null if streaming should be stopped
+ * @param sinkId The id of the audio sink to use or null for the default
+ */
+ void play(@Nullable AudioStream audioStream, @Nullable String sinkId);
+
+ /**
+ * Plays the passed audio stream on the given sink.
+ *
+ * @param audioStream The audio stream to play or null if streaming should be stopped
+ * @param sinkId The id of the audio sink to use or null for the default
+ * @param volume The volume to be used or null if the default notification volume should be used
+ */
+ void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume);
+
+ /**
+ * Plays an audio file from the "sounds" folder using the default audio sink.
+ *
+ * @param fileName The file from the "sounds" folder
+ * @throws AudioException in case the file does not exist or cannot be opened
+ */
+ void playFile(String fileName) throws AudioException;
+
+ /**
+ * Plays an audio file with the given volume from the "sounds" folder using the default audio sink.
+ *
+ * @param fileName The file from the "sounds" folder
+ * @param volume The volume to be used or null if the default notification volume should be used
+ * @throws AudioException in case the file does not exist or cannot be opened
+ */
+ void playFile(String fileName, @Nullable PercentType volume) throws AudioException;
+
+ /**
+ * Plays an audio file from the "sounds" folder using the given audio sink.
+ *
+ * @param fileName The file from the "sounds" folder
+ * @param sinkId The id of the audio sink to use or null for the default
+ * @throws AudioException in case the file does not exist or cannot be opened
+ */
+ void playFile(String fileName, @Nullable String sinkId) throws AudioException;
+
+ /**
+ * Plays an audio file with the given volume from the "sounds" folder using the given audio sink.
+ *
+ * @param fileName The file from the "sounds" folder
+ * @param sinkId The id of the audio sink to use or null for the default
+ * @param volume The volume to be used or null if the default notification volume should be used
+ * @throws AudioException in case the file does not exist or cannot be opened
+ */
+ void playFile(String fileName, @Nullable String sinkId, @Nullable PercentType volume) throws AudioException;
+
+ /**
+ * Stream audio from the passed url using the default audio sink.
+ *
+ * @param url The url to stream from or null if streaming should be stopped
+ * @throws AudioException in case the url stream cannot be opened
+ */
+ void stream(@Nullable String url) throws AudioException;
+
+ /**
+ * Stream audio from the passed url to the given sink
+ *
+ * @param url The url to stream from or null if streaming should be stopped
+ * @param sinkId The id of the audio sink to use or null for the default
+ * @throws AudioException in case the url stream cannot be opened
+ */
+ void stream(@Nullable String url, @Nullable String sinkId) throws AudioException;
+
+ /**
+ * Retrieves the current volume of a sink
+ *
+ * @param sinkId the sink to get the volume for or null for the default
+ * @return the volume as a value between 0 and 100
+ * @throws IOException if the sink is not able to determine the volume
+ */
+ PercentType getVolume(@Nullable String sinkId) throws IOException;
+
+ /**
+ * Sets the volume for a sink.
+ *
+ * @param volume the volume to set as a value between 0 and 100
+ * @param sinkId the sink to set the volume for or null for the default
+ * @throws IOException if the sink is not able to set the volume
+ */
+ void setVolume(PercentType volume, @Nullable String sinkId) throws IOException;
+
+ /**
+ * Retrieves an AudioSource.
+ * If a default name is configured and the service available, this is returned. If no default name is configured,
+ * the first available service is returned, if one exists. If no service with the default name is found, null is
+ * returned.
+ *
+ * @return an AudioSource or null, if no service is available or if a default is configured, but no according
+ * service is found
+ */
+ @Nullable
+ AudioSource getSource();
+
+ /**
+ * Retrieves all audio sources
+ *
+ * @return all audio sources
+ */
+ Set getAllSources();
+
+ /**
+ * Retrieves an AudioSink.
+ * If a default name is configured and the service available, this is returned. If no default name is configured,
+ * the first available service is returned, if one exists. If no service with the default name is found, null is
+ * returned.
+ *
+ * @return an AudioSink or null, if no service is available or if a default is configured, but no according service
+ * is found
+ */
+ @Nullable
+ AudioSink getSink();
+
+ /**
+ * Retrieves all audio sinks
+ *
+ * @return all audio sinks
+ */
+ Set getAllSinks();
+
+ /**
+ * Get a list of source ids that match a given pattern
+ *
+ * @param pattern pattern to search, can include `*` and `?` placeholders
+ * @return ids of matching sources
+ */
+ Set getSourceIds(String pattern);
+
+ /**
+ * Retrieves the sink for a given id
+ *
+ * @param sinkId the id of the sink or null for the default
+ * @return the sink instance for the id or the default sink
+ */
+ @Nullable
+ AudioSink getSink(@Nullable String sinkId);
+
+ /**
+ * Get a list of sink ids that match a given pattern
+ *
+ * @param pattern pattern to search, can include `*` and `?` placeholders
+ * @return ids of matching sinks
+ */
+ Set getSinkIds(String pattern);
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java
new file mode 100644
index 00000000000..e12ab8ebfff
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.PercentType;
+
+/**
+ * Definition of an audio output like headphones, a speaker or for writing to
+ * a file / clip.
+ *
+ * @author Harald Kuhn - Initial API
+ * @author Kelly Davis - Modified to match discussion in #584
+ * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
+ *
+ */
+@NonNullByDefault
+public interface AudioSink {
+
+ /**
+ * Returns a simple string that uniquely identifies this service
+ *
+ * @return an id that identifies this service
+ */
+ public String getId();
+
+ /**
+ * Returns a localized human readable label that can be used within UIs.
+ *
+ * @param locale the locale to provide the label for
+ * @return a localized string to be used in UIs
+ */
+ @Nullable
+ public String getLabel(Locale locale);
+
+ /**
+ * Processes the passed {@link AudioStream}
+ *
+ * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException}
+ * is thrown.
+ *
+ * If the passed {@link AudioStream} has a {@link AudioFormat} not supported by this instance,
+ * an {@link UnsupportedAudioFormatException} is thrown.
+ *
+ * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream.
+ *
+ * @param audioStream the audio stream to play or null to keep quiet
+ * @throws UnsupportedAudioFormatException If audioStream format is not supported
+ * @throws UnsupportedAudioStreamException If audioStream is not supported
+ */
+ void process(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException;
+
+ /**
+ * Gets a set containing all supported audio formats
+ *
+ * @return A Set containing all supported audio formats
+ */
+ public Set getSupportedFormats();
+
+ /**
+ * Gets a set containing all supported audio stream formats
+ *
+ * @return A Set containing all supported audio stream formats
+ */
+ public Set> getSupportedStreams();
+
+ /**
+ * Gets the volume
+ *
+ * @return a PercentType value between 0 and 100 representing the actual volume
+ * @throws IOException if the volume can not be determined
+ */
+ public PercentType getVolume() throws IOException;
+
+ /**
+ * Sets the volume
+ *
+ * @param volume a PercentType value between 0 and 100 representing the desired volume
+ * @throws IOException if the volume can not be set
+ */
+ public void setVolume(PercentType volume) throws IOException;
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java
new file mode 100644
index 00000000000..45b5e39dfe2
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * This is an audio source, which can provide a continuous live stream of audio.
+ * Its main use is for microphones and other "line-in" sources and it can be registered as a service in order to make
+ * it available throughout the system.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public interface AudioSource {
+
+ /**
+ * Returns a simple string that uniquely identifies this service
+ *
+ * @return an id that identifies this service
+ */
+ String getId();
+
+ /**
+ * Returns a localized human readable label that can be used within UIs.
+ *
+ * @param locale the locale to provide the label for
+ * @return a localized string to be used in UIs
+ */
+ String getLabel(Locale locale);
+
+ /**
+ * Obtain the audio formats supported by this AudioSource
+ *
+ * @return The audio formats supported by this service
+ */
+ Set getSupportedFormats();
+
+ /**
+ * Gets an AudioStream for reading audio data in supported audio format
+ *
+ * @param format the expected audio format of the stream
+ * @return AudioStream for reading audio data
+ * @throws AudioException If problem occurs obtaining the stream
+ */
+ AudioStream getInputStream(AudioFormat format) throws AudioException;
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java
new file mode 100644
index 00000000000..a2d74dceb9e
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.InputStream;
+
+/**
+ * Wrapper for a source of audio data.
+ *
+ * In contrast to {@link AudioSource}, this is often a "one time use" instance for passing some audio data,
+ * but it is not meant to be registered as a service.
+ *
+ * The stream needs to be closed by the client that uses it.
+ *
+ * @author Harald Kuhn - Initial API
+ * @author Kelly Davis - Modified to match discussion in #584
+ * @author Kai Kreuzer - Refactored to be only a temporary instance for the stream
+ */
+public abstract class AudioStream extends InputStream {
+
+ /**
+ * Gets the supported audio format
+ *
+ * @return The supported audio format
+ */
+ public abstract AudioFormat getFormat();
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/ByteArrayAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/ByteArrayAudioStream.java
new file mode 100644
index 00000000000..7b29708e49a
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/ByteArrayAudioStream.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This is an implementation of a {@link FixedLengthAudioStream}, which is based on a simple byte array.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class ByteArrayAudioStream extends FixedLengthAudioStream {
+
+ private byte[] bytes;
+ private AudioFormat format;
+ private ByteArrayInputStream stream;
+
+ public ByteArrayAudioStream(byte[] bytes, AudioFormat format) {
+ this.bytes = bytes;
+ this.format = format;
+ this.stream = new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public AudioFormat getFormat() {
+ return format;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return stream.read();
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+
+ @Override
+ public long length() {
+ return bytes.length;
+ }
+
+ @Override
+ public InputStream getClonedStream() {
+ return new ByteArrayAudioStream(bytes, format);
+ }
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FileAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FileAudioStream.java
new file mode 100644
index 00000000000..25a7743340c
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FileAudioStream.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.eclipse.smarthome.core.audio.utils.AudioStreamUtils;
+
+/**
+ * This is an AudioStream from an audio file
+ *
+ * @author Karel Goderis - Initial contribution and API
+ * @author Kai Kreuzer - Refactored to take a file as input
+ * @author Christoph Weitkamp - Refactored use of filename extension
+ *
+ */
+public class FileAudioStream extends FixedLengthAudioStream {
+
+ public static final String WAV_EXTENSION = "wav";
+ public static final String MP3_EXTENSION = "mp3";
+ public static final String OGG_EXTENSION = "ogg";
+ public static final String AAC_EXTENSION = "aac";
+
+ private final File file;
+ private final AudioFormat audioFormat;
+ private InputStream inputStream;
+ private final long length;
+
+ public FileAudioStream(File file) throws AudioException {
+ this(file, getAudioFormat(file));
+ }
+
+ public FileAudioStream(File file, AudioFormat format) throws AudioException {
+ this.file = file;
+ this.inputStream = getInputStream(file);
+ this.audioFormat = format;
+ this.length = file.length();
+ }
+
+ private static AudioFormat getAudioFormat(File file) throws AudioException {
+ final String filename = file.getName().toLowerCase();
+ final String extension = AudioStreamUtils.getExtension(filename);
+ switch (extension) {
+ case WAV_EXTENSION:
+ return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 705600,
+ 44100L);
+ case MP3_EXTENSION:
+ return AudioFormat.MP3;
+ case OGG_EXTENSION:
+ return AudioFormat.OGG;
+ case AAC_EXTENSION:
+ return AudioFormat.AAC;
+ default:
+ throw new AudioException("Unsupported file extension!");
+ }
+ }
+
+ private static InputStream getInputStream(File file) throws AudioException {
+ try {
+ return new FileInputStream(file);
+ } catch (FileNotFoundException e) {
+ throw new AudioException("File '" + file.getAbsolutePath() + "' not found!");
+ }
+ }
+
+ @Override
+ public AudioFormat getFormat() {
+ return audioFormat;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return inputStream.read();
+ }
+
+ @Override
+ public void close() throws IOException {
+ inputStream.close();
+ super.close();
+ }
+
+ @Override
+ public long length() {
+ return this.length;
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ IOUtils.closeQuietly(inputStream);
+ try {
+ inputStream = getInputStream(file);
+ } catch (AudioException e) {
+ throw new IOException("Cannot reset file input stream: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public InputStream getClonedStream() throws AudioException {
+ return getInputStream(file);
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FixedLengthAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FixedLengthAudioStream.java
new file mode 100644
index 00000000000..631403eefa8
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/FixedLengthAudioStream.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.InputStream;
+
+/**
+ * This is an {@link AudioStream}, which can provide information about its absolute length and is able to provide
+ * cloned streams.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public abstract class FixedLengthAudioStream extends AudioStream {
+
+ /**
+ * Provides the length of the stream in bytes.
+ *
+ * @return absolute length in bytes
+ */
+ public abstract long length();
+
+ /**
+ * Returns a new, fully independent stream instance, which can be read and closed without impacting the original
+ * instance.
+ *
+ * @return a new input stream that can be consumed by the caller
+ * @throws AudioException if stream cannot be created
+ */
+ public abstract InputStream getClonedStream() throws AudioException;
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/URLAudioStream.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/URLAudioStream.java
new file mode 100644
index 00000000000..bab4563fda5
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/URLAudioStream.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.eclipse.smarthome.core.audio.utils.AudioStreamUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is an AudioStream from an URL. Note that some sinks, like Sonos, can directly handle URL
+ * based streams, and therefore can/should call getURL() to get an direct reference to the URL.
+ *
+ * @author Karel Goderis - Initial contribution and API
+ * @author Kai Kreuzer - Refactored to not require a source
+ * @author Christoph Weitkamp - Refactored use of filename extension
+ *
+ */
+public class URLAudioStream extends AudioStream {
+
+ private static final Pattern PLS_STREAM_PATTERN = Pattern.compile("^File[0-9]=(.+)$");
+
+ public static final String M3U_EXTENSION = "m3u";
+ public static final String PLS_EXTENSION = "pls";
+
+ private final Logger logger = LoggerFactory.getLogger(URLAudioStream.class);
+
+ private final AudioFormat audioFormat;
+ private final InputStream inputStream;
+ private String url;
+
+ private Socket shoutCastSocket;
+
+ public URLAudioStream(String url) throws AudioException {
+ if (url == null) {
+ throw new IllegalArgumentException("url must not be null!");
+ }
+ this.url = url;
+ this.audioFormat = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, false, 16, null, null);
+ this.inputStream = createInputStream();
+ }
+
+ private InputStream createInputStream() throws AudioException {
+ final String filename = url.toLowerCase();
+ final String extension = AudioStreamUtils.getExtension(filename);
+ try {
+ switch (extension) {
+ case M3U_EXTENSION:
+ try (final InputStream isM3U = new URL(url).openStream()) {
+ for (final String line : IOUtils.readLines(isM3U)) {
+ if (!line.isEmpty() && !line.startsWith("#")) {
+ url = line;
+ break;
+ }
+ }
+ }
+ break;
+ case PLS_EXTENSION:
+ try (final InputStream isPLS = new URL(url).openStream()) {
+ for (final String line : IOUtils.readLines(isPLS)) {
+ if (!line.isEmpty() && line.startsWith("File")) {
+ final Matcher matcher = PLS_STREAM_PATTERN.matcher(line);
+ if (matcher.find()) {
+ url = matcher.group(1);
+ break;
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ URL streamUrl = new URL(url);
+ URLConnection connection = streamUrl.openConnection();
+ if (connection.getContentType().equals("unknown/unknown")) {
+ // Java does not parse non-standard headers used by SHOUTCast
+ int port = streamUrl.getPort() > 0 ? streamUrl.getPort() : 80;
+ // Manipulate User-Agent to receive a stream
+ shoutCastSocket = new Socket(streamUrl.getHost(), port);
+
+ OutputStream os = shoutCastSocket.getOutputStream();
+ String userAgent = "WinampMPEG/5.09";
+ String req = "GET / HTTP/1.0\r\nuser-agent: " + userAgent
+ + "\r\nIcy-MetaData: 1\r\nConnection: keep-alive\r\n\r\n";
+ os.write(req.getBytes());
+ return shoutCastSocket.getInputStream();
+ } else {
+ // getInputStream() method is more error-proof than openStream(),
+ // because openStream() does openConnection().getInputStream(),
+ // which opens a new connection and does not reuse the old one.
+ return connection.getInputStream();
+ }
+ } catch (MalformedURLException e) {
+ logger.error("URL '{}' is not a valid url: {}", url, e.getMessage(), e);
+ throw new AudioException("URL not valid");
+ } catch (IOException e) {
+ logger.error("Cannot set up stream '{}': {}", url, e.getMessage(), e);
+ throw new AudioException("IO Error");
+ }
+ }
+
+ @Override
+ public AudioFormat getFormat() {
+ return audioFormat;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return inputStream.read();
+ }
+
+ public String getURL() {
+ return url;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ if (shoutCastSocket != null) {
+ shoutCastSocket.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return url;
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java
new file mode 100644
index 00000000000..3616da2a20f
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+/**
+ * Thrown when a requested format is not supported by an {@link AudioSource}
+ * or {@link AudioSink} implementation
+ *
+ * @author Harald Kuhn - Initial API
+ * @author Kelly Davis - Modified to match discussion in #584
+ */
+public class UnsupportedAudioFormatException extends AudioException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Unsupported {@link AudioFormat}
+ */
+ private AudioFormat unsupportedFormat;
+
+ /**
+ * Constructs a new exception with the specified detail message, unsupported format, and cause.
+ *
+ * @param message Detail message
+ * @param unsupportedFormat Unsupported format
+ * @param cause The cause
+ */
+ public UnsupportedAudioFormatException(String message, AudioFormat unsupportedFormat, Throwable cause) {
+ super(message, cause);
+ this.unsupportedFormat = unsupportedFormat;
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and unsupported format.
+ *
+ * @param message Detail message
+ * @param unsupportedFormat Unsupported format
+ */
+ public UnsupportedAudioFormatException(String message, AudioFormat unsupportedFormat) {
+ this(message, unsupportedFormat, null);
+ }
+
+ /**
+ * Gets the unsupported format
+ *
+ * @return The unsupported format
+ */
+ public AudioFormat getUnsupportedFormat() {
+ return unsupportedFormat;
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioStreamException.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioStreamException.java
new file mode 100644
index 00000000000..46b782037e6
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioStreamException.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio;
+
+/**
+ * Thrown when a requested {@link AudioStream} is not supported by an {@link AudioSource} or {@link AudioSink}
+ * implementation
+ *
+ * @author Christoph Weitkamp - Initial contribution and API
+ *
+ */
+public class UnsupportedAudioStreamException extends AudioException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Unsupported {@link AudioStream}
+ */
+ private Class extends AudioStream> unsupportedAudioStreamClass;
+
+ /**
+ * Constructs a new exception with the specified detail message, unsupported format, and cause.
+ *
+ * @param message The message
+ * @param unsupportedAudioStreamClass The unsupported audio stream class
+ * @param cause The cause
+ */
+ public UnsupportedAudioStreamException(String message, Class extends AudioStream> unsupportedAudioStreamClass,
+ Throwable cause) {
+ super(message, cause);
+ this.unsupportedAudioStreamClass = unsupportedAudioStreamClass;
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and unsupported format.
+ *
+ * @param message The message
+ * @param unsupportedAudioStreamClass The unsupported audio stream class
+ */
+ public UnsupportedAudioStreamException(String message, Class extends AudioStream> unsupportedAudioStreamClass) {
+ this(message, unsupportedAudioStreamClass, null);
+ }
+
+ /**
+ * Gets the unsupported audio stream class.
+ *
+ * @return The unsupported audio stream class
+ */
+ public Class extends AudioStream> getUnsupportedAudioStreamClass() {
+ return unsupportedAudioStreamClass;
+ }
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioConsoleCommandExtension.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioConsoleCommandExtension.java
new file mode 100644
index 00000000000..09cd2a0ab45
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioConsoleCommandExtension.java
@@ -0,0 +1,217 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio.internal;
+
+import static java.util.Comparator.comparing;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.eclipse.smarthome.core.audio.AudioException;
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.eclipse.smarthome.core.audio.AudioSink;
+import org.eclipse.smarthome.core.audio.AudioSource;
+import org.eclipse.smarthome.core.i18n.LocaleProvider;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.io.console.Console;
+import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension;
+import org.eclipse.smarthome.io.console.extensions.ConsoleCommandExtension;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Console command extension for all audio features.
+ *
+ * @author Karel Goderis - Initial contribution and API
+ * @author Kai Kreuzer - refactored to match AudioManager implementation
+ * @author Christoph Weitkamp - Added parameter to adjust the volume
+ * @author Wouter Born - Sort audio sink and source options
+ */
+@Component(service = ConsoleCommandExtension.class)
+public class AudioConsoleCommandExtension extends AbstractConsoleCommandExtension {
+
+ static final String SUBCMD_PLAY = "play";
+ static final String SUBCMD_STREAM = "stream";
+ static final String SUBCMD_SOURCES = "sources";
+ static final String SUBCMD_SINKS = "sinks";
+
+ private AudioManager audioManager;
+ private LocaleProvider localeProvider;
+
+ public AudioConsoleCommandExtension() {
+ super("audio", "Commands around audio enablement features.");
+ }
+
+ @Override
+ public List getUsages() {
+ return Arrays.asList(new String[] {
+ buildCommandUsage(SUBCMD_PLAY + " [] ",
+ "plays a sound file from the sounds folder through the optionally specified audio sink(s)"),
+ buildCommandUsage(SUBCMD_PLAY + " ",
+ "plays a sound file from the sounds folder through the specified audio sink(s) with the specified volume"),
+ buildCommandUsage(SUBCMD_STREAM + " [] ",
+ "streams the sound from the url through the optionally specified audio sink(s)"),
+ buildCommandUsage(SUBCMD_SOURCES, "lists the audio sources"),
+ buildCommandUsage(SUBCMD_SINKS, "lists the audio sinks") });
+
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length > 0) {
+ String subCommand = args[0];
+ switch (subCommand) {
+ case SUBCMD_PLAY:
+ if (args.length > 1) {
+ play((String[]) ArrayUtils.subarray(args, 1, args.length), console);
+ } else {
+ console.println(
+ "Specify file to play, and optionally the sink(s) to use (e.g. 'play javasound hello.mp3')");
+ }
+ return;
+ case SUBCMD_STREAM:
+ if (args.length > 1) {
+ stream((String[]) ArrayUtils.subarray(args, 1, args.length), console);
+ } else {
+ console.println("Specify url to stream from, and optionally the sink(s) to use");
+ }
+ return;
+ case SUBCMD_SOURCES:
+ listSources(console);
+ return;
+ case SUBCMD_SINKS:
+ listSinks(console);
+ return;
+ default:
+ break;
+ }
+ } else {
+ printUsage(console);
+ }
+ }
+
+ private void listSources(Console console) {
+ Set sources = audioManager.getAllSources();
+ if (sources.size() > 0) {
+ AudioSource defaultSource = audioManager.getSource();
+ Locale locale = localeProvider.getLocale();
+ sources.stream().sorted(comparing(s -> s.getLabel(locale))).forEach(source -> {
+ console.println(String.format("%s %s (%s)", source.equals(defaultSource) ? "*" : " ",
+ source.getLabel(locale), source.getId()));
+ });
+ } else {
+ console.println("No audio sources found.");
+ }
+ }
+
+ private void listSinks(Console console) {
+ Set sinks = audioManager.getAllSinks();
+ if (sinks.size() > 0) {
+ AudioSink defaultSink = audioManager.getSink();
+ Locale locale = localeProvider.getLocale();
+ sinks.stream().sorted(comparing(s -> s.getLabel(locale))).forEach(sink -> {
+ console.println(String.format("%s %s (%s)", sink.equals(defaultSink) ? "*" : " ", sink.getLabel(locale),
+ sink.getId()));
+ });
+ } else {
+ console.println("No audio sinks found.");
+ }
+ }
+
+ private void play(String[] args, Console console) {
+ switch (args.length) {
+ case 1:
+ playOnSink(null, args[0], null, console);
+ break;
+ case 2:
+ playOnSinks(args[0], args[1], null, console);
+ break;
+ case 3:
+ PercentType volume = null;
+ try {
+ volume = PercentType.valueOf(args[2]);
+ } catch (Exception e) {
+ console.println(e.getMessage());
+ break;
+ }
+ playOnSinks(args[0], args[1], volume, console);
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ private void playOnSinks(String pattern, String fileName, PercentType volume, Console console) {
+ for (String sinkId : audioManager.getSinkIds(pattern)) {
+ playOnSink(sinkId, fileName, volume, console);
+ }
+ }
+
+ private void playOnSink(String sinkId, String fileName, PercentType volume, Console console) {
+ try {
+ audioManager.playFile(fileName, sinkId, volume);
+ } catch (AudioException e) {
+ console.println(e.getMessage());
+ }
+ }
+
+ private void stream(String[] args, Console console) {
+ switch (args.length) {
+ case 1:
+ streamOnSink(null, args[0], console);
+ break;
+ case 2:
+ streamOnSinks(args[0], args[1], console);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void streamOnSinks(String pattern, String url, Console console) {
+ for (String sinkId : audioManager.getSinkIds(pattern)) {
+ streamOnSink(sinkId, url, console);
+ }
+ }
+
+ private void streamOnSink(String sinkId, String url, Console console) {
+ try {
+ audioManager.stream(url, sinkId);
+ } catch (AudioException e) {
+ console.println(e.getMessage());
+ }
+ }
+
+ @Reference
+ protected void setAudioManager(AudioManager audioManager) {
+ this.audioManager = audioManager;
+ }
+
+ protected void unsetAudioManager(AudioManager audioManager) {
+ this.audioManager = null;
+ }
+
+ @Reference
+ protected void setLocaleProvider(LocaleProvider localeProvider) {
+ this.localeProvider = localeProvider;
+ }
+
+ protected void unsetLocaleProvider(LocaleProvider localeProvider) {
+ this.localeProvider = null;
+ }
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioManagerImpl.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioManagerImpl.java
new file mode 100644
index 00000000000..a7b2d84d1b4
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioManagerImpl.java
@@ -0,0 +1,322 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio.internal;
+
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.smarthome.config.core.ConfigConstants;
+import org.eclipse.smarthome.config.core.ConfigOptionProvider;
+import org.eclipse.smarthome.config.core.ConfigurableService;
+import org.eclipse.smarthome.config.core.ParameterOption;
+import org.eclipse.smarthome.core.audio.AudioException;
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.eclipse.smarthome.core.audio.AudioSink;
+import org.eclipse.smarthome.core.audio.AudioSource;
+import org.eclipse.smarthome.core.audio.AudioStream;
+import org.eclipse.smarthome.core.audio.FileAudioStream;
+import org.eclipse.smarthome.core.audio.URLAudioStream;
+import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException;
+import org.eclipse.smarthome.core.audio.UnsupportedAudioStreamException;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This service provides functionality around audio services and is the central service to be used directly by others.
+ *
+ * @author Karel Goderis - Initial contribution and API
+ * @author Kai Kreuzer - removed unwanted dependencies
+ * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
+ * @author Christoph Weitkamp - Added parameter to adjust the volume
+ * @author Wouter Born - Sort audio sink and source options
+ */
+@Component(immediate = true, configurationPid = "org.eclipse.smarthome.audio", property = { //
+ Constants.SERVICE_PID + "=org.eclipse.smarthome.audio", //
+ ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=system", //
+ ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=" + AudioManagerImpl.CONFIG_URI, //
+ ConfigurableService.SERVICE_PROPERTY_LABEL + "=Audio" //
+})
+public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
+
+ // constants for the configuration properties
+ static final String CONFIG_URI = "system:audio";
+ static final String CONFIG_DEFAULT_SINK = "defaultSink";
+ static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
+
+ private final Logger logger = LoggerFactory.getLogger(AudioManagerImpl.class);
+
+ // service maps
+ private final Map audioSources = new ConcurrentHashMap<>();
+ private final Map audioSinks = new ConcurrentHashMap<>();
+
+ /**
+ * default settings filled through the service configuration
+ */
+ private String defaultSource;
+ private String defaultSink;
+
+ @Activate
+ protected void activate(Map config) {
+ modified(config);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ }
+
+ @Modified
+ void modified(Map config) {
+ if (config != null) {
+ this.defaultSource = config.containsKey(CONFIG_DEFAULT_SOURCE)
+ ? config.get(CONFIG_DEFAULT_SOURCE).toString()
+ : null;
+ this.defaultSink = config.containsKey(CONFIG_DEFAULT_SINK) ? config.get(CONFIG_DEFAULT_SINK).toString()
+ : null;
+ }
+ }
+
+ @Override
+ public void play(AudioStream audioStream) {
+ play(audioStream, null);
+ }
+
+ @Override
+ public void play(AudioStream audioStream, String sinkId) {
+ play(audioStream, sinkId, null);
+ }
+
+ @Override
+ public void play(AudioStream audioStream, String sinkId, PercentType volume) {
+ AudioSink sink = getSink(sinkId);
+ if (sink != null) {
+ PercentType oldVolume = null;
+ try {
+ // get current volume
+ oldVolume = getVolume(sinkId);
+ } catch (IOException e) {
+ logger.debug("An exception occurred while getting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+ // set notification sound volume
+ if (volume != null) {
+ try {
+ setVolume(volume, sinkId);
+ } catch (IOException e) {
+ logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+ }
+ try {
+ sink.process(audioStream);
+ } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
+ logger.warn("Error playing '{}': {}", audioStream, e.getMessage(), e);
+ } finally {
+ if (volume != null && oldVolume != null) {
+ // restore volume only if it was set before
+ try {
+ setVolume(oldVolume, sinkId);
+ } catch (IOException e) {
+ logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(),
+ e.getMessage(), e);
+ }
+ }
+ }
+ } else {
+ logger.warn("Failed playing audio stream '{}' as no audio sink was found.", audioStream);
+ }
+ }
+
+ @Override
+ public void playFile(String fileName) throws AudioException {
+ playFile(fileName, null, null);
+ }
+
+ @Override
+ public void playFile(String fileName, PercentType volume) throws AudioException {
+ playFile(fileName, null, volume);
+ }
+
+ @Override
+ public void playFile(String fileName, String sinkId) throws AudioException {
+ playFile(fileName, sinkId, null);
+ }
+
+ @Override
+ public void playFile(String fileName, String sinkId, PercentType volume) throws AudioException {
+ Objects.requireNonNull(fileName, "File cannot be played as fileName is null.");
+
+ File file = new File(
+ ConfigConstants.getConfigFolder() + File.separator + SOUND_DIR + File.separator + fileName);
+ FileAudioStream is = new FileAudioStream(file);
+ play(is, sinkId, volume);
+ }
+
+ @Override
+ public void stream(String url) throws AudioException {
+ stream(url, null);
+ }
+
+ @Override
+ public void stream(String url, String sinkId) throws AudioException {
+ AudioStream audioStream = url != null ? new URLAudioStream(url) : null;
+ play(audioStream, sinkId, null);
+ }
+
+ @Override
+ public PercentType getVolume(String sinkId) throws IOException {
+ AudioSink sink = getSink(sinkId);
+
+ if (sink != null) {
+ return sink.getVolume();
+ }
+ return PercentType.ZERO;
+ }
+
+ @Override
+ public void setVolume(PercentType volume, String sinkId) throws IOException {
+ AudioSink sink = getSink(sinkId);
+
+ if (sink != null) {
+ sink.setVolume(volume);
+ }
+ }
+
+ @Override
+ public AudioSource getSource() {
+ AudioSource source = null;
+ if (defaultSource != null) {
+ source = audioSources.get(defaultSource);
+ if (source == null) {
+ logger.warn("Default AudioSource service '{}' not available!", defaultSource);
+ }
+ } else if (!audioSources.isEmpty()) {
+ source = audioSources.values().iterator().next();
+ } else {
+ logger.debug("No AudioSource service available!");
+ }
+ return source;
+ }
+
+ @Override
+ public Set getAllSources() {
+ return new HashSet<>(audioSources.values());
+ }
+
+ @Override
+ public AudioSink getSink() {
+ AudioSink sink = null;
+ if (defaultSink != null) {
+ sink = audioSinks.get(defaultSink);
+ if (sink == null) {
+ logger.warn("Default AudioSink service '{}' not available!", defaultSink);
+ }
+ } else if (!audioSinks.isEmpty()) {
+ sink = audioSinks.values().iterator().next();
+ } else {
+ logger.debug("No AudioSink service available!");
+ }
+ return sink;
+ }
+
+ @Override
+ public Set getAllSinks() {
+ return new HashSet<>(audioSinks.values());
+ }
+
+ @Override
+ public Set getSourceIds(String pattern) {
+ String regex = pattern.replace("?", ".?").replace("*", ".*?");
+ Set matchedSources = new HashSet<>();
+
+ for (String aSource : audioSources.keySet()) {
+ if (aSource.matches(regex)) {
+ matchedSources.add(aSource);
+ }
+ }
+
+ return matchedSources;
+ }
+
+ @Override
+ public AudioSink getSink(String sinkId) {
+ return (sinkId == null) ? getSink() : audioSinks.get(sinkId);
+ }
+
+ @Override
+ public Set getSinkIds(String pattern) {
+ String regex = pattern.replace("?", ".?").replace("*", ".*?");
+ Set matchedSinkIds = new HashSet<>();
+
+ for (String sinkId : audioSinks.keySet()) {
+ if (sinkId.matches(regex)) {
+ matchedSinkIds.add(sinkId);
+ }
+ }
+
+ return matchedSinkIds;
+ }
+
+ @Override
+ public Collection getParameterOptions(URI uri, String param, Locale locale) {
+ if (uri.toString().equals(CONFIG_URI)) {
+ final Locale safeLocale = locale != null ? locale : Locale.getDefault();
+ if (CONFIG_DEFAULT_SOURCE.equals(param)) {
+ return audioSources.values().stream().sorted(comparing(s -> s.getLabel(safeLocale)))
+ .map(s -> new ParameterOption(s.getId(), s.getLabel(safeLocale))).collect(toList());
+ } else if (CONFIG_DEFAULT_SINK.equals(param)) {
+ return audioSinks.values().stream().sorted(comparing(s -> s.getLabel(safeLocale)))
+ .map(s -> new ParameterOption(s.getId(), s.getLabel(safeLocale))).collect(toList());
+ }
+ }
+ return null;
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ protected void addAudioSource(AudioSource audioSource) {
+ this.audioSources.put(audioSource.getId(), audioSource);
+ }
+
+ protected void removeAudioSource(AudioSource audioSource) {
+ this.audioSources.remove(audioSource.getId());
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ protected void addAudioSink(AudioSink audioSink) {
+ this.audioSinks.put(audioSink.getId(), audioSink);
+ }
+
+ protected void removeAudioSink(AudioSink audioSink) {
+ this.audioSinks.remove(audioSink.getId());
+ }
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioServlet.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioServlet.java
new file mode 100644
index 00000000000..61635125b0e
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/internal/AudioServlet.java
@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.eclipse.smarthome.core.audio.AudioException;
+import org.eclipse.smarthome.core.audio.AudioFormat;
+import org.eclipse.smarthome.core.audio.AudioHTTPServer;
+import org.eclipse.smarthome.core.audio.AudioStream;
+import org.eclipse.smarthome.core.audio.FixedLengthAudioStream;
+import org.eclipse.smarthome.io.http.servlet.SmartHomeServlet;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpService;
+
+/**
+ * A servlet that serves audio streams via HTTP.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+@Component
+public class AudioServlet extends SmartHomeServlet implements AudioHTTPServer {
+
+ private static final long serialVersionUID = -3364664035854567854L;
+
+ private static final String SERVLET_NAME = "/audio";
+
+ private final Map oneTimeStreams = new ConcurrentHashMap<>();
+ private final Map multiTimeStreams = new ConcurrentHashMap<>();
+
+ private final Map streamTimeouts = new ConcurrentHashMap<>();
+
+ @Activate
+ protected void activate() {
+ super.activate(SERVLET_NAME);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate(SERVLET_NAME);
+ }
+
+ @Override
+ @Reference
+ protected void setHttpService(HttpService httpService) {
+ super.setHttpService(httpService);
+ }
+
+ @Override
+ public void unsetHttpService(HttpService httpService) {
+ super.unsetHttpService(httpService);
+ }
+
+ private InputStream prepareInputStream(final String streamId, final HttpServletResponse resp)
+ throws AudioException {
+ final AudioStream stream;
+ final boolean multiAccess;
+ if (oneTimeStreams.containsKey(streamId)) {
+ stream = oneTimeStreams.remove(streamId);
+ multiAccess = false;
+ } else if (multiTimeStreams.containsKey(streamId)) {
+ stream = multiTimeStreams.get(streamId);
+ multiAccess = true;
+ } else {
+ return null;
+ }
+
+ logger.debug("Stream to serve is {}", streamId);
+
+ // try to set the content-type, if possible
+ final String mimeType;
+ if (stream.getFormat().getCodec() == AudioFormat.CODEC_MP3) {
+ mimeType = "audio/mpeg";
+ } else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_WAVE) {
+ mimeType = "audio/wav";
+ } else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_OGG) {
+ mimeType = "audio/ogg";
+ } else {
+ mimeType = null;
+ }
+ if (mimeType != null) {
+ resp.setContentType(mimeType);
+ }
+
+ // try to set the content-length, if possible
+ if (stream instanceof FixedLengthAudioStream) {
+ final Long size = ((FixedLengthAudioStream) stream).length();
+ resp.setContentLength(size.intValue());
+ }
+
+ if (multiAccess) {
+ // we need to care about concurrent access and have a separate stream for each thread
+ return ((FixedLengthAudioStream) stream).getClonedStream();
+ } else {
+ return stream;
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ removeTimedOutStreams();
+
+ final String streamId = StringUtils.substringBefore(StringUtils.substringAfterLast(req.getRequestURI(), "/"),
+ ".");
+
+ try (final InputStream stream = prepareInputStream(streamId, resp)) {
+ if (stream == null) {
+ logger.debug("Received request for invalid stream id at {}", req.getRequestURI());
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+ } else {
+ IOUtils.copy(stream, resp.getOutputStream());
+ resp.flushBuffer();
+ }
+ } catch (final AudioException ex) {
+ resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
+ }
+ }
+
+ private synchronized void removeTimedOutStreams() {
+ for (String streamId : multiTimeStreams.keySet()) {
+ if (streamTimeouts.get(streamId) < System.nanoTime()) {
+ // the stream has expired, we need to remove it!
+ FixedLengthAudioStream stream = multiTimeStreams.remove(streamId);
+ streamTimeouts.remove(streamId);
+ IOUtils.closeQuietly(stream);
+ stream = null;
+ logger.debug("Removed timed out stream {}", streamId);
+ }
+ }
+ }
+
+ @Override
+ public String serve(AudioStream stream) {
+ String streamId = UUID.randomUUID().toString();
+ oneTimeStreams.put(streamId, stream);
+ return getRelativeURL(streamId);
+ }
+
+ @Override
+ public String serve(FixedLengthAudioStream stream, int seconds) {
+ String streamId = UUID.randomUUID().toString();
+ multiTimeStreams.put(streamId, stream);
+ streamTimeouts.put(streamId, System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds));
+ return getRelativeURL(streamId);
+ }
+
+ Map getMultiTimeStreams() {
+ return Collections.unmodifiableMap(multiTimeStreams);
+ }
+
+ private String getRelativeURL(String streamId) {
+ return SERVLET_NAME + "/" + streamId;
+ }
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/utils/AudioStreamUtils.java b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/utils/AudioStreamUtils.java
new file mode 100644
index 00000000000..549c1006442
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/java/org/eclipse/smarthome/core/audio/utils/AudioStreamUtils.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.core.audio.utils;
+
+/**
+ * Some general filename and extension utilities.
+ *
+ * @author Christoph Weitkamp - Initial contribution and API
+ *
+ */
+public class AudioStreamUtils {
+
+ public static final String EXTENSION_SEPARATOR = ".";
+
+ /**
+ * Gets the base name of a filename.
+ *
+ * @param filename the filename to query
+ * @return the base name of the file or an empty string if none exists or {@code null} if the filename is
+ * {@code null}
+ */
+ public static String getBaseName(String filename) {
+ if (filename == null) {
+ return null;
+ }
+ final int index = filename.lastIndexOf(EXTENSION_SEPARATOR);
+ if (index == -1) {
+ return filename;
+ } else {
+ return filename.substring(0, index);
+ }
+ }
+
+ /**
+ * Gets the extension of a filename.
+ *
+ * @param filename the filename to retrieve the extension of
+ * @return the extension of the file or an empty string if none exists or {@code null} if the filename is
+ * {@code null}
+ */
+ public static String getExtension(String filename) {
+ if (filename == null) {
+ return null;
+ }
+ final int index = filename.lastIndexOf(EXTENSION_SEPARATOR);
+ if (index == -1) {
+ return "";
+ } else {
+ return filename.substring(index + 1);
+ }
+ }
+
+ /**
+ * Checks if the extension of a filename matches the given.
+ *
+ * @param filename the filename to check the extension of
+ * @param extension the extension to check for
+ * @return {@code true} if the filename has the specified extension
+ */
+ public static boolean isExtension(String filename, String extension) {
+ if (filename == null) {
+ return false;
+ }
+ if (extension == null || extension.isEmpty()) {
+ return false;
+ }
+ return getExtension(filename).equals(extension);
+ }
+
+}
diff --git a/bundles/org.openhab.core.audio/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.core.audio/src/main/resources/ESH-INF/config/config.xml
new file mode 100644
index 00000000000..6ae64f590c9
--- /dev/null
+++ b/bundles/org.openhab.core.audio/src/main/resources/ESH-INF/config/config.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ The default audio source to use if no other is specified.
+
+
+
+ The default audio sink to use if no other is specified.
+
+
+
+
diff --git a/bundles/org.openhab.core.auth.jaas/.classpath b/bundles/org.openhab.core.auth.jaas/.classpath
new file mode 100644
index 00000000000..3c5e7d17553
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.auth.jaas/.project b/bundles/org.openhab.core.auth.jaas/.project
new file mode 100644
index 00000000000..984b7ddd251
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.auth.jaas
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..0c4313356dd
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding/=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.auth.jaas/NOTICE b/bundles/org.openhab.core.auth.jaas/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/org.openhab.core.auth.jaas/pom.xml b/bundles/org.openhab.core.auth.jaas/pom.xml
new file mode 100644
index 00000000000..f2417166155
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.auth.jaas
+
+ openHAB Core :: Bundles :: JAAS Authentication
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.auth.jaas/src/main/java/org/eclipse/smarthome/auth/jaas/internal/JaasAuthenticationProvider.java b/bundles/org.openhab.core.auth.jaas/src/main/java/org/eclipse/smarthome/auth/jaas/internal/JaasAuthenticationProvider.java
new file mode 100644
index 00000000000..36ef01516c7
--- /dev/null
+++ b/bundles/org.openhab.core.auth.jaas/src/main/java/org/eclipse/smarthome/auth/jaas/internal/JaasAuthenticationProvider.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.jaas.internal;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.eclipse.smarthome.core.auth.Authentication;
+import org.eclipse.smarthome.core.auth.AuthenticationException;
+import org.eclipse.smarthome.core.auth.AuthenticationProvider;
+import org.eclipse.smarthome.core.auth.Credentials;
+import org.eclipse.smarthome.core.auth.UsernamePasswordCredentials;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+
+/**
+ * Implementation of authentication provider which is backed by JAAS realm.
+ *
+ * Real authentication logic is embedded in login modules implemented by 3rd party, this code is just for bridging it to
+ * smarthome platform.
+ *
+ * @author Łukasz Dywicki - Initial contribution and API
+ * @author Kai Kreuzer - Removed ManagedService and used DS configuration instead
+ */
+@Component(configurationPid = "org.eclipse.smarthome.jaas")
+public class JaasAuthenticationProvider implements AuthenticationProvider {
+
+ private String realmName;
+
+ @Override
+ public Authentication authenticate(final Credentials credentials) throws AuthenticationException {
+ if (realmName == null) { // configuration is not yet ready or set
+ return null;
+ }
+
+ if (!(credentials instanceof UsernamePasswordCredentials)) {
+ throw new AuthenticationException("Unsupported credentials passed to provider.");
+ }
+
+ UsernamePasswordCredentials userCredentials = (UsernamePasswordCredentials) credentials;
+ final String name = userCredentials.getUsername();
+ final char[] password = userCredentials.getPassword().toCharArray();
+
+ try {
+ LoginContext loginContext = new LoginContext(realmName, new CallbackHandler() {
+ @Override
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof PasswordCallback) {
+ ((PasswordCallback) callback).setPassword(password);
+ } else if (callback instanceof NameCallback) {
+ ((NameCallback) callback).setName(name);
+ } else {
+ throw new UnsupportedCallbackException(callback);
+ }
+ }
+ }
+ });
+ loginContext.login();
+
+ return getAuthentication(name, loginContext.getSubject());
+ } catch (LoginException e) {
+ throw new AuthenticationException("Could not obtain authentication over login context", e);
+ }
+ }
+
+ private Authentication getAuthentication(String name, Subject subject) {
+ return new Authentication(name, getRoles(subject.getPrincipals()));
+ }
+
+ private String[] getRoles(Set principals) {
+ String[] roles = new String[principals.size()];
+ int i = 0;
+ for (Principal principal : principals) {
+ roles[i++] = principal.getName();
+ }
+ return roles;
+ }
+
+ @Activate
+ protected void activate(Map properties) {
+ modified(properties);
+ }
+
+ @Deactivate
+ protected void deactivate(Map properties) {
+ }
+
+ @Modified
+ protected void modified(Map properties) {
+ if (properties == null) {
+ realmName = null;
+ return;
+ }
+
+ Object propertyValue = properties.get("realmName");
+ if (propertyValue != null) {
+ if (propertyValue instanceof String) {
+ realmName = (String) propertyValue;
+ } else {
+ realmName = propertyValue.toString();
+ }
+ } else {
+ // value could be unset, we should reset it value
+ realmName = null;
+ }
+ }
+
+ @Override
+ public boolean supports(Class extends Credentials> type) {
+ return UsernamePasswordCredentials.class.isAssignableFrom(type);
+ }
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/.classpath b/bundles/org.openhab.core.auth.oauth2client/.classpath
new file mode 100644
index 00000000000..3c5e7d17553
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.auth.oauth2client/.project b/bundles/org.openhab.core.auth.oauth2client/.project
new file mode 100644
index 00000000000..5b062cf91d5
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.auth.oauth2client
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..0c4313356dd
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding/=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.auth.oauth2client/pom.xml b/bundles/org.openhab.core.auth.oauth2client/pom.xml
new file mode 100644
index 00000000000..41d150f94d7
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/pom.xml
@@ -0,0 +1,29 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.auth.oauth2client
+
+ openHAB Core :: Bundles :: OAuth2Client
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.net
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/Keyword.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/Keyword.java
new file mode 100644
index 00000000000..ba0113a327a
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/Keyword.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+/**
+ * Just a place to store all the important, reused keywords.
+ *
+ * @author Gary Tse - Initial contribution
+ *
+ */
+public interface Keyword {
+
+ String CLIENT_ID = "client_id";
+ String CLIENT_SECRET = "client_secret";
+
+ String GRANT_TYPE = "grant_type";
+ String USERNAME = "username";
+ String PASSWORD = "password";
+ String CLIENT_CREDENTIALS = "client_credentials";
+ String AUTHORIZATION_CODE = "authorization_code";
+
+ String SCOPE = "scope";
+
+ String REFRESH_TOKEN = "refresh_token";
+ String REDIRECT_URI = "redirect_uri";
+
+ String CODE = "code"; // https://tools.ietf.org/html/rfc6749#section-4.1
+
+ String STATE = "state"; // https://tools.ietf.org/html/rfc6749#section-4.1
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthClientServiceImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthClientServiceImpl.java
new file mode 100644
index 00000000000..4c0fa2f33fb
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthClientServiceImpl.java
@@ -0,0 +1,407 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import static org.eclipse.smarthome.auth.oauth2client.internal.Keyword.*;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthClientService;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthResponseException;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of OAuthClientService.
+ *
+ * It requires the following services:
+ *
+ * org.eclipse.smarthome.core.storage.Storage (mandatory; for storing grant tokens, access tokens and refresh tokens)
+ *
+ * HttpClientFactory for http connections with Jetty
+ *
+ * The OAuthTokens, request parameters are stored and persisted using the "Storage" service.
+ * This allows the token to be automatically refreshed when needed.
+ *
+ * @author Michael Bock - Initial contribution
+ * @author Gary Tse - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class OAuthClientServiceImpl implements OAuthClientService {
+
+ public static final int DEFAULT_TOKEN_EXPIRES_IN_BUFFER_SECOND = 10;
+
+ private static final String EXCEPTION_MESSAGE_CLOSED = "Client service is closed";
+
+ private transient final Logger logger = LoggerFactory.getLogger(OAuthClientServiceImpl.class);
+
+ private @NonNullByDefault({}) OAuthStoreHandler storeHandler;
+
+ // Constructor params - static
+ private final String handle;
+ private final int tokenExpiresInSeconds;
+ private final HttpClientFactory httpClientFactory;
+ private final List accessTokenRefreshListeners = new ArrayList<>();
+
+ private PersistedParams persistedParams = new PersistedParams();
+
+ private volatile boolean closed = false;
+
+ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpClientFactory httpClientFactory) {
+ this.handle = handle;
+ this.tokenExpiresInSeconds = tokenExpiresInSeconds;
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ /**
+ * It should only be used internally, thus the access is package level
+ *
+ * @param bundleContext Bundle Context
+ * @param handle The handle produced previously from
+ * {@link org.eclipse.smarthome.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}
+ * @param storeHandler Storage handler
+ * @param tokenExpiresInSeconds Positive integer; a small time buffer in seconds. It is used to calculate the expiry
+ * of the access tokens. This allows the access token to expire earlier than the
+ * official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke,
+ * only to find the token immediately expired.
+ * @param httpClientFactory Http client factory
+ * @return new instance of OAuthClientServiceImpl or null if it doesn't exist
+ * @throws IllegalStateException if store is not available.
+ */
+ static @Nullable OAuthClientServiceImpl getInstance(String handle, OAuthStoreHandler storeHandler,
+ int tokenExpiresInSeconds, HttpClientFactory httpClientFactory) {
+ // Load parameters from Store
+ PersistedParams persistedParamsFromStore = storeHandler.loadPersistedParams(handle);
+ if (persistedParamsFromStore == null) {
+ return null;
+ }
+ OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds,
+ httpClientFactory);
+ clientService.storeHandler = storeHandler;
+ clientService.persistedParams = persistedParamsFromStore;
+
+ return clientService;
+ }
+
+ /**
+ * It should only be used internally, thus the access is package level
+ *
+ * @param bundleContext Bundle Context*
+ * @param handle The handle produced previously from
+ * {@link org.eclipse.smarthome.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}*
+ * @param storeHandler Storage handler
+ * @param httpClientFactory Http client factory
+ * @param persistedParams These parameters are static with respect to the oauth provider and thus can be persisted.
+ * @return OAuthClientServiceImpl an instance
+ */
+ static OAuthClientServiceImpl createInstance(String handle, OAuthStoreHandler storeHandler,
+ HttpClientFactory httpClientFactory, PersistedParams params) {
+ OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, params.tokenExpiresInSeconds,
+ httpClientFactory);
+
+ clientService.storeHandler = storeHandler;
+ clientService.persistedParams = params;
+ storeHandler.savePersistedParams(handle, clientService.persistedParams);
+
+ return clientService;
+ }
+
+ @Override
+ public String getAuthorizationUrl(@Nullable String redirectURI, @Nullable String scope, @Nullable String state)
+ throws OAuthException {
+ if (state == null) {
+ persistedParams.state = createNewState();
+ } else {
+ persistedParams.state = state;
+ }
+ String scopeToUse = scope == null ? persistedParams.scope : scope;
+ // keep it to check against redirectUri in #getAccessTokenResponseByAuthorizationCode
+ persistedParams.redirectUri = redirectURI;
+ String authorizationUrl = persistedParams.authorizationUrl;
+ if (authorizationUrl == null) {
+ throw new OAuthException("Missing authorization url");
+ }
+ String clientId = persistedParams.clientId;
+ if (clientId == null) {
+ throw new OAuthException("Missing client ID");
+ }
+
+ OAuthConnector connector = new OAuthConnector(httpClientFactory);
+ return connector.getAuthorizationUrl(authorizationUrl, clientId, redirectURI, persistedParams.state,
+ scopeToUse);
+ }
+
+ @Override
+ public String extractAuthCodeFromAuthResponse(@NonNull String redirectURLwithParams) throws OAuthException {
+ // parse the redirectURL
+ try {
+ URL redirectURLObject = new URL(redirectURLwithParams);
+ UrlEncoded urlEncoded = new UrlEncoded(redirectURLObject.getQuery());
+
+ String stateFromRedirectURL = urlEncoded.getValue(STATE, 0); // may contain multiple...
+ if (stateFromRedirectURL == null) {
+ if (persistedParams.state == null) {
+ // This should not happen as the state is usually set
+ return urlEncoded.getValue(CODE, 0);
+ } // else
+ throw new OAuthException(String.format("state from redirectURL is incorrect. Expected: %s Found: %s",
+ persistedParams.state, stateFromRedirectURL));
+ } else {
+ if (stateFromRedirectURL.equals(persistedParams.state)) {
+ return urlEncoded.getValue(CODE, 0);
+ } // else
+ throw new OAuthException(String.format("state from redirectURL is incorrect. Expected: %s Found: %s",
+ persistedParams.state, stateFromRedirectURL));
+ }
+ } catch (MalformedURLException e) {
+ throw new OAuthException("Redirect URL is malformed", e);
+ }
+ }
+
+ @Override
+ public AccessTokenResponse getAccessTokenResponseByAuthorizationCode(String authorizationCode, String redirectURI)
+ throws OAuthException, IOException, OAuthResponseException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+
+ if (persistedParams.redirectUri != null && !persistedParams.redirectUri.equals(redirectURI)) {
+ // check parameter redirectURI in #getAuthorizationUrl are the same as given
+ throw new OAuthException(String.format(
+ "redirectURI should be the same from previous call #getAuthorizationUrl. Expected: %s Found: %s",
+ persistedParams.redirectUri, redirectURI));
+ }
+ String tokenUrl = persistedParams.tokenUrl;
+ if (tokenUrl == null) {
+ throw new OAuthException("Missing token url");
+ }
+ String clientId = persistedParams.clientId;
+ if (clientId == null) {
+ throw new OAuthException("Missing client ID");
+ }
+
+ OAuthConnector connector = new OAuthConnector(httpClientFactory);
+ AccessTokenResponse accessTokenResponse = connector.grantTypeAuthorizationCode(tokenUrl, authorizationCode,
+ clientId, persistedParams.clientSecret, redirectURI,
+ Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
+
+ // store it
+ storeHandler.saveAccessTokenResponse(handle, accessTokenResponse);
+ return accessTokenResponse;
+ }
+
+ /**
+ * Implicit Grant (RFC 6749 section 4.2) is not implemented. It is directly interacting with user-agent
+ * The implicit grant is not implemented. It usually involves browser/javascript redirection flows
+ * and is out of Eclipse SmartHome scope.
+ */
+ @Override
+ public AccessTokenResponse getAccessTokenByImplicit(@Nullable String redirectURI, @Nullable String scope,
+ @Nullable String state) throws OAuthException, IOException, OAuthResponseException {
+ throw new UnsupportedOperationException("Implicit Grant is not implemented");
+ }
+
+ @Override
+ public AccessTokenResponse getAccessTokenByResourceOwnerPasswordCredentials(String username, String password,
+ @Nullable String scope) throws OAuthException, IOException, OAuthResponseException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+ String tokenUrl = persistedParams.tokenUrl;
+ if (tokenUrl == null) {
+ throw new OAuthException("Missing token url");
+ }
+
+ OAuthConnector connector = new OAuthConnector(httpClientFactory);
+ AccessTokenResponse accessTokenResponse = connector.grantTypePassword(tokenUrl, username, password,
+ persistedParams.clientId, persistedParams.clientSecret, scope,
+ Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
+
+ // store it
+ storeHandler.saveAccessTokenResponse(handle, accessTokenResponse);
+ return accessTokenResponse;
+ }
+
+ @Override
+ public AccessTokenResponse getAccessTokenByClientCredentials(@Nullable String scope)
+ throws OAuthException, IOException, OAuthResponseException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+ String tokenUrl = persistedParams.tokenUrl;
+ if (tokenUrl == null) {
+ throw new OAuthException("Missing token url");
+ }
+ String clientId = persistedParams.clientId;
+ if (clientId == null) {
+ throw new OAuthException("Missing client ID");
+ }
+
+ OAuthConnector connector = new OAuthConnector(httpClientFactory);
+ // depending on usage, cannot guarantee every parameter is not null at the beginning
+ AccessTokenResponse accessTokenResponse = connector.grantTypeClientCredentials(tokenUrl, clientId,
+ persistedParams.clientSecret, scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
+
+ // store it
+ storeHandler.saveAccessTokenResponse(handle, accessTokenResponse);
+ return accessTokenResponse;
+ }
+
+ @Override
+ public AccessTokenResponse refreshToken() throws OAuthException, IOException, OAuthResponseException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+
+ AccessTokenResponse lastAccessToken;
+ try {
+ lastAccessToken = storeHandler.loadAccessTokenResponse(handle);
+ } catch (GeneralSecurityException e) {
+ throw new OAuthException("Cannot decrypt access token from store", e);
+ }
+ if (lastAccessToken == null) {
+ throw new OAuthException(
+ "Cannot refresh token because last access token is not available from handle: " + handle);
+ }
+ if (lastAccessToken.getRefreshToken() == null) {
+ throw new OAuthException("Cannot refresh token because last access token did not have a refresh token");
+ }
+ String tokenUrl = persistedParams.tokenUrl;
+ if (tokenUrl == null) {
+ throw new OAuthException("tokenUrl is required but null");
+ }
+
+ OAuthConnector connector = new OAuthConnector(httpClientFactory);
+ AccessTokenResponse accessTokenResponse = connector.grantTypeRefreshToken(tokenUrl,
+ lastAccessToken.getRefreshToken(), persistedParams.clientId, persistedParams.clientSecret,
+ persistedParams.scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
+
+ // The service may not return the refresh token so use the last refresh token otherwise it's not stored.
+ if (StringUtil.isBlank(accessTokenResponse.getRefreshToken())) {
+ accessTokenResponse.setRefreshToken(lastAccessToken.getRefreshToken());
+ }
+ // store it
+ storeHandler.saveAccessTokenResponse(handle, accessTokenResponse);
+ accessTokenRefreshListeners.forEach(l -> l.onAccessTokenResponse(accessTokenResponse));
+ return accessTokenResponse;
+ }
+
+ @Override
+ public @Nullable AccessTokenResponse getAccessTokenResponse()
+ throws OAuthException, IOException, OAuthResponseException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+
+ AccessTokenResponse lastAccessToken;
+ try {
+ lastAccessToken = storeHandler.loadAccessTokenResponse(handle);
+ } catch (GeneralSecurityException e) {
+ throw new OAuthException("Cannot decrypt access token from store", e);
+ }
+ if (lastAccessToken == null) {
+ return null;
+ }
+
+ if (lastAccessToken.isExpired(LocalDateTime.now(), tokenExpiresInSeconds)
+ && lastAccessToken.getRefreshToken() != null) {
+ return refreshToken();
+ }
+ return lastAccessToken;
+ }
+
+ @Override
+ public void importAccessTokenResponse(AccessTokenResponse accessTokenResponse) throws OAuthException {
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+ storeHandler.saveAccessTokenResponse(handle, accessTokenResponse);
+ }
+
+ /**
+ * Access tokens have expiry times. They are given by authorization server.
+ * This parameter introduces a buffer in seconds that deduct from the expires-in.
+ * For example, if the expires-in = 3600 seconds ( 1hour ), then setExpiresInBuffer(60)
+ * will remove 60 seconds from the expiry time. In other words, the expires-in
+ * becomes 3540 ( 59 mins ) effectively.
+ *
+ * Calls to protected resources can reasonably assume that the token is not expired.
+ *
+ * @param tokenExpiresInBuffer The number of seconds to remove the expires-in. Default 0 seconds.
+ */
+ public void setTokenExpiresInBuffer(int tokenExpiresInBuffer) {
+ this.persistedParams.tokenExpiresInSeconds = tokenExpiresInBuffer;
+ }
+
+ @Override
+ public void remove() throws OAuthException {
+
+ if (isClosed()) {
+ throw new OAuthException(EXCEPTION_MESSAGE_CLOSED);
+ }
+
+ logger.debug("removing handle: {}", handle);
+ storeHandler.remove(handle);
+ close();
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ storeHandler = null;
+
+ logger.debug("closing oauth client, handle: {}", handle);
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public void addAccessTokenRefreshListener(AccessTokenRefreshListener listener) {
+ accessTokenRefreshListeners.add(listener);
+ }
+
+ @Override
+ public boolean removeAccessTokenRefreshListener(AccessTokenRefreshListener listener) {
+ return accessTokenRefreshListeners.remove(listener);
+ }
+
+ private String createNewState() {
+ return UUID.randomUUID().toString();
+ }
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthConnector.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthConnector.java
new file mode 100644
index 00000000000..79abb1c3536
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthConnector.java
@@ -0,0 +1,383 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import static org.eclipse.smarthome.auth.oauth2client.internal.Keyword.*;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.time.LocalDateTime;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.FormContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.util.Fields;
+import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthResponseException;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.eclipse.smarthome.io.net.http.TrustManagerProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Implementation of the OAuthConnector. It directly deals with the underlying http connections (using Jetty).
+ * This is meant for internal use. OAuth2client's clients should look into {@code OAuthClientService} or
+ * {@code OAuthFactory}
+ *
+ * @author Michael Bock - Initial contribution
+ * @author Gary Tse - ESH adaptation
+ *
+ */
+@NonNullByDefault
+public class OAuthConnector {
+
+ private static final String HTTP_CLIENT_CONSUMER_NAME = "OAuthConnector";
+
+ private final HttpClientFactory httpClientFactory;
+
+ private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class);
+ private final Gson gson;
+
+ public OAuthConnector(HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = httpClientFactory;
+ gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ }
+
+ /**
+ * Authorization Code Grant
+ *
+ * @param authorizationEndpoint The end point of the authorization provider that performs authorization of the
+ * resource owner
+ * @param clientId Client identifier (will be URL-encoded)
+ * @param redirectURI RFC 6749 section 3.1.2 (will be URL-encoded)
+ * @param state Recommended to enhance security (will be URL-encoded)
+ * @param scope Optional space separated list of scope (will be URL-encoded)
+ *
+ * @return A URL based on the authorizationEndpoint, with query parameters added.
+ * @see rfc6749 section-4.1.1
+ */
+ public String getAuthorizationUrl(String authorizationEndpoint, String clientId, @Nullable String redirectURI,
+ @Nullable String state, @Nullable String scope) {
+ StringBuilder authorizationUrl = new StringBuilder(authorizationEndpoint);
+
+ if (authorizationUrl.indexOf("?") == -1) {
+ authorizationUrl.append('?');
+ } else {
+ authorizationUrl.append('&');
+ }
+
+ try {
+ authorizationUrl.append("response_type=code");
+ authorizationUrl.append("&client_id=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()));
+ if (state != null) {
+ authorizationUrl.append("&state=").append(URLEncoder.encode(state, StandardCharsets.UTF_8.name()));
+ }
+ if (redirectURI != null) {
+ authorizationUrl.append("&redirect_uri=")
+ .append(URLEncoder.encode(redirectURI, StandardCharsets.UTF_8.name()));
+ }
+ if (scope != null) {
+ authorizationUrl.append("&scope=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8.name()));
+ }
+ } catch (UnsupportedEncodingException e) {
+ // never happens
+ logger.error("Unknown encoding {}", e.getMessage(), e);
+ }
+ return authorizationUrl.toString();
+ }
+
+ /**
+ * Resource Owner Password Credentials Grant
+ *
+ * @see rfc6749 section-4.3
+ *
+ * @param tokenUrl URL of the oauth provider that accepts access token requests.
+ * @param username The resource owner username.
+ * @param password The resource owner password.
+ * @param clientId The client identifier issued to the client during the registration process
+ * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
+ * @param scope Access Token Scope.
+ * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
+ * provider.
+ * @return Access Token
+ * @throws IOException IO/ network exceptions
+ * @throws OAuthException Other exceptions
+ * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
+ * Response
+ */
+ public AccessTokenResponse grantTypePassword(String tokenUrl, String username, String password,
+ @Nullable String clientId, @Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
+ throws OAuthResponseException, OAuthException, IOException {
+
+ HttpClient httpClient = null;
+ try {
+ httpClient = createHttpClient(tokenUrl);
+ Request request = getMethod(httpClient, tokenUrl);
+ Fields fields = initFields(GRANT_TYPE, PASSWORD, USERNAME, username, PASSWORD, password, SCOPE, scope);
+
+ setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
+ return doRequest(PASSWORD, httpClient, request, fields);
+ } finally {
+ shutdownQuietly(httpClient);
+ }
+ }
+
+ /**
+ * Refresh Token
+ *
+ * @see rfc6749 section-6
+ *
+ * @param tokenUrl URL of the oauth provider that accepts access token requests.
+ * @param refreshToken The refresh token, which can be used to obtain new access tokens using authorization grant
+ * @param clientId The client identifier issued to the client during the registration process
+ * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
+ * @param scope Access Token Scope.
+ * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
+ * provider.
+ * @return Access Token
+ * @throws IOException IO/ network exceptions
+ * @throws OAuthException Other exceptions
+ * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
+ * Response
+ */
+ public AccessTokenResponse grantTypeRefreshToken(String tokenUrl, String refreshToken, @Nullable String clientId,
+ @Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
+ throws OAuthResponseException, OAuthException, IOException {
+
+ HttpClient httpClient = null;
+ try {
+ httpClient = createHttpClient(tokenUrl);
+ Request request = getMethod(httpClient, tokenUrl);
+ Fields fields = initFields(GRANT_TYPE, REFRESH_TOKEN, REFRESH_TOKEN, refreshToken, SCOPE, scope);
+
+ setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
+ return doRequest(REFRESH_TOKEN, httpClient, request, fields);
+ } finally {
+ shutdownQuietly(httpClient);
+ }
+ }
+
+ /**
+ * Authorization Code Grant - part (E)
+ *
+ * @see rfc6749 section-4.1.3
+ *
+ * @param tokenUrl URL of the oauth provider that accepts access token requests.
+ * @param authorizationCode to be used to trade with the oauth provider for access token
+ * @param clientId The client identifier issued to the client during the registration process
+ * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
+ * @param redirectUrl is the http request parameter which tells the oauth provider the URI to redirect the
+ * user-agent. This may/ may not be present as per agreement with the oauth provider.
+ * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
+ * provider
+ * @return Access Token
+ * @throws IOException IO/ network exceptions
+ * @throws OAuthException Other exceptions
+ * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
+ * Response
+ */
+ public AccessTokenResponse grantTypeAuthorizationCode(String tokenUrl, String authorizationCode, String clientId,
+ @Nullable String clientSecret, String redirectUrl, boolean supportsBasicAuth)
+ throws OAuthResponseException, OAuthException, IOException {
+ HttpClient httpClient = null;
+ try {
+ httpClient = createHttpClient(tokenUrl);
+ Request request = getMethod(httpClient, tokenUrl);
+ Fields fields = initFields(GRANT_TYPE, AUTHORIZATION_CODE, CODE, authorizationCode, REDIRECT_URI,
+ redirectUrl);
+
+ setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
+ return doRequest(AUTHORIZATION_CODE, httpClient, request, fields);
+ } finally {
+ shutdownQuietly(httpClient);
+ }
+ }
+
+ /**
+ * Client Credentials Grant
+ *
+ * @see rfc6749 section-4.4
+ *
+ * @param tokenUrl URL of the oauth provider that accepts access token requests.
+ * @param clientId The client identifier issued to the client during the registration process
+ * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
+ * @param scope Access Token Scope.
+ * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
+ * provider
+ * @return Access Token
+ * @throws IOException IO/ network exceptions
+ * @throws OAuthException Other exceptions
+ * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
+ * Response
+ */
+ public AccessTokenResponse grantTypeClientCredentials(String tokenUrl, String clientId,
+ @Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
+ throws OAuthResponseException, OAuthException, IOException {
+
+ HttpClient httpClient = null;
+ try {
+ httpClient = createHttpClient(tokenUrl);
+ Request request = getMethod(httpClient, tokenUrl);
+ Fields fields = initFields(GRANT_TYPE, CLIENT_CREDENTIALS, SCOPE, scope);
+
+ setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
+ return doRequest(CLIENT_CREDENTIALS, httpClient, request, fields);
+ } finally {
+ shutdownQuietly(httpClient);
+ }
+ }
+
+ private Request getMethod(HttpClient httpClient, String tokenUrl) {
+ Request request = httpClient.newRequest(tokenUrl).method(HttpMethod.POST);
+ request.header(HttpHeader.ACCEPT, "application/json");
+ request.header(HttpHeader.ACCEPT_CHARSET, "UTF-8");
+ return request;
+ }
+
+ private void setAuthentication(@Nullable String clientId, @Nullable String clientSecret, Request request,
+ Fields fields, boolean supportsBasicAuth) {
+ logger.debug("Setting authentication for clientId {}. Using basic auth {}", clientId, supportsBasicAuth);
+ if (supportsBasicAuth && clientSecret != null) {
+ String authString = clientId + ":" + clientSecret;
+ request.header(HttpHeader.AUTHORIZATION,
+ "Basic " + Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8)));
+ } else {
+ if (clientId != null) {
+ fields.add(CLIENT_ID, clientId);
+ }
+ if (clientSecret != null) {
+ fields.add(CLIENT_SECRET, clientSecret);
+ }
+ }
+ }
+
+ private Fields initFields(String... parameters) {
+ Fields fields = new Fields();
+
+ for (int i = 0; i < parameters.length; i += 2) {
+ if (i + 1 < parameters.length && parameters[i] != null && parameters[i + 1] != null) {
+ logger.debug("Oauth request parameter {}, value {}", parameters[i], parameters[i + 1]);
+ fields.add(parameters[i], parameters[i + 1]);
+ }
+ }
+ return fields;
+ }
+
+ private AccessTokenResponse doRequest(final String grantType, HttpClient httpClient, final Request request,
+ Fields fields) throws OAuthResponseException, OAuthException, IOException {
+
+ int statusCode = 0;
+ String content = "";
+ try {
+ final FormContentProvider entity = new FormContentProvider(fields);
+ final ContentResponse response = AccessController
+ .doPrivileged((PrivilegedExceptionAction) () -> {
+ Request requestWithContent = request.content(entity);
+ return requestWithContent.send();
+ });
+
+ statusCode = response.getStatus();
+ content = response.getContentAsString();
+
+ if (statusCode == HttpStatus.OK_200) {
+ AccessTokenResponse jsonResponse = gson.fromJson(content, AccessTokenResponse.class);
+ jsonResponse.setCreatedOn(LocalDateTime.now()); // this is not supplied by the response
+ logger.info("grant type {} to URL {} success", grantType, request.getURI());
+ return jsonResponse;
+ } else if (statusCode == HttpStatus.BAD_REQUEST_400) {
+ OAuthResponseException errorResponse = gson.fromJson(content, OAuthResponseException.class);
+ logger.error("grant type {} to URL {} failed with error code {}, description {}", grantType,
+ request.getURI(), errorResponse.getError(), errorResponse.getErrorDescription());
+
+ throw errorResponse;
+ } else {
+ logger.error("grant type {} to URL {} failed with HTTP response code {}", grantType, request.getURI(),
+ statusCode);
+ throw new OAuthException("Bad http response, http code " + statusCode);
+ }
+ } catch (PrivilegedActionException pae) {
+ Exception underlyingException = pae.getException();
+ if (underlyingException instanceof InterruptedException || underlyingException instanceof TimeoutException
+ || underlyingException instanceof ExecutionException) {
+ throw new IOException("Exception in oauth communication, grant type " + grantType, underlyingException);
+ }
+ // Dont know what exception it is, wrap it up and throw it out
+ throw new OAuthException("Exception in oauth communication, grant type " + grantType, underlyingException);
+ } catch (JsonSyntaxException e) {
+ throw new OAuthException(String.format(
+ "Unable to deserialize json into AccessTokenResponse/ OAuthResponseException. httpCode: %i json: %s",
+ statusCode, content), e);
+ }
+ }
+
+ /**
+ * This is a special case where the httpClient (jetty) is created due to the need for certificate pinning.
+ * If ceritificate pinning is needed, please refer to {@code TrustManagerProvider}. The http client is
+ * created, used and then shutdown immediately after use. There is little reason to cache the client/ connections
+ * because oauth requests are short; and it may take hours/ days before the next request is needed.
+ *
+ * @param tokenUrl access token url
+ * @return http client. This http client
+ * @throws OAuthException If any exception is thrown while starting the http client.
+ * @see TrustManagerProvider
+ */
+ private HttpClient createHttpClient(String tokenUrl) throws OAuthException {
+ HttpClient httpClient = httpClientFactory.createHttpClient(HTTP_CLIENT_CONSUMER_NAME, tokenUrl);
+ if (!httpClient.isStarted()) {
+ try {
+ AccessController.doPrivileged((PrivilegedExceptionAction<@Nullable Void>) () -> {
+ httpClient.start();
+ return null;
+ });
+ } catch (Exception e) {
+ throw new OAuthException("Exception while starting httpClient, tokenUrl: " + tokenUrl, e);
+ }
+ }
+ return httpClient;
+ }
+
+ private void shutdownQuietly(@Nullable HttpClient httpClient) {
+ try {
+ if (httpClient != null) {
+ AccessController.doPrivileged((PrivilegedExceptionAction<@Nullable Void>) () -> {
+ httpClient.stop();
+ return null;
+ });
+ }
+ } catch (Exception e) {
+ // there is nothing we can do here
+ logger.error("Exception while shutting down httpClient, {}", e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthFactoryImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthFactoryImpl.java
new file mode 100644
index 00000000000..2fe52a48f8c
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthFactoryImpl.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthClientService;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException;
+import org.eclipse.smarthome.core.auth.client.oauth2.OAuthFactory;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link OAuthFactory}.
+ *
+ * @author Michael Bock - Initial contribution
+ * @author Gary Tse - ESH adaptation
+ * @author Hilbrand Bouwkamp - Changed implementation of createOAuthClientService
+ */
+@NonNullByDefault
+@Component
+public class OAuthFactoryImpl implements OAuthFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(OAuthFactoryImpl.class);
+
+ @NonNullByDefault({})
+ private OAuthStoreHandler oAuthStoreHandler;
+ @NonNullByDefault({})
+ private HttpClientFactory httpClientFactory;
+
+ private int tokenExpiresInBuffer = OAuthClientServiceImpl.DEFAULT_TOKEN_EXPIRES_IN_BUFFER_SECOND;
+
+ private final Map oauthClientServiceCache = new ConcurrentHashMap<>();
+
+ @Deactivate
+ public void deactivate() {
+ // close each service
+ for (OAuthClientService clientServiceImpl : oauthClientServiceCache.values()) {
+ clientServiceImpl.close();
+ }
+ oauthClientServiceCache.clear();
+ }
+
+ @Override
+ public OAuthClientService createOAuthClientService(String handle, String tokenUrl,
+ @Nullable String authorizationUrl, String clientId, @Nullable String clientSecret, @Nullable String scope,
+ @Nullable Boolean supportsBasicAuth) {
+ PersistedParams params = oAuthStoreHandler.loadPersistedParams(handle);
+ PersistedParams newParams = new PersistedParams(handle, tokenUrl, authorizationUrl, clientId, clientSecret,
+ scope, supportsBasicAuth, tokenExpiresInBuffer);
+ OAuthClientService clientImpl = null;
+
+ // If parameters in storage and parameters are the same as arguments passed get the client from storage
+ if (params != null && params.equals(newParams)) {
+ clientImpl = getOAuthClientService(handle);
+ }
+ // If no client with parameters or with different parameters create or update (if parameters are different)
+ // client in storage.
+ if (clientImpl == null) {
+ clientImpl = OAuthClientServiceImpl.createInstance(handle, oAuthStoreHandler, httpClientFactory, newParams);
+ oauthClientServiceCache.put(handle, clientImpl);
+ }
+ return clientImpl;
+
+ }
+
+ @Override
+ @Nullable
+ public OAuthClientService getOAuthClientService(String handle) {
+ OAuthClientService clientImpl = oauthClientServiceCache.get(handle);
+
+ if (clientImpl == null || clientImpl.isClosed()) {
+ // This happens after reboot, or client was closed without factory knowing; create a new client
+ // the store has the handle/config data
+ clientImpl = OAuthClientServiceImpl.getInstance(handle, oAuthStoreHandler, tokenExpiresInBuffer,
+ httpClientFactory);
+ if (clientImpl == null) {
+ return null;
+ }
+ oauthClientServiceCache.put(handle, clientImpl);
+ }
+ return clientImpl;
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public void ungetOAuthService(String handle) {
+ OAuthClientService clientImpl = oauthClientServiceCache.get(handle);
+
+ if (clientImpl == null) {
+ logger.debug("{} handle not found. Cannot unregisterOAuthServie", handle);
+ return;
+ }
+ clientImpl.close();
+ oauthClientServiceCache.remove(handle);
+ }
+
+ @Override
+ public void deleteServiceAndAccessToken(String handle) {
+ OAuthClientService clientImpl = oauthClientServiceCache.get(handle);
+
+ if (clientImpl != null) {
+ try {
+ clientImpl.remove();
+ } catch (OAuthException e) {
+ // client was already closed, does not matter
+ }
+ oauthClientServiceCache.remove(handle);
+ }
+ oAuthStoreHandler.remove(handle);
+ }
+
+ /**
+ * The Store handler is mandatory, but the actual storage service is not.
+ * OAuthStoreHandler will handle when storage service is missing.
+ *
+ * Intended static mandatory 1..1 reference
+ *
+ * @param oAuthStoreHandler
+ */
+ @Reference
+ protected void setOAuthStoreHandler(OAuthStoreHandler oAuthStoreHandler) {
+ this.oAuthStoreHandler = oAuthStoreHandler;
+ }
+
+ protected void unsetOAuthStoreHandler(OAuthStoreHandler oAuthStoreHandler) {
+ this.oAuthStoreHandler = null;
+ }
+
+ /**
+ * HttpClientFactory is mandatory, and is used as a client for
+ * all http(s) communications to the OAuth providers
+ *
+ * @param httpClientFactory
+ */
+ @Reference
+ protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = null;
+ }
+
+ public int getTokenExpiresInBuffer() {
+ return tokenExpiresInBuffer;
+ }
+
+ public void setTokenExpiresInBuffer(int bufferInSeconds) {
+ tokenExpiresInBuffer = bufferInSeconds;
+ }
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandler.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandler.java
new file mode 100644
index 00000000000..577fff1f820
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandler.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import java.security.GeneralSecurityException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse;
+
+/**
+ * This is for OAuth client internal use.
+ *
+ * @author Gary Tse - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public interface OAuthStoreHandler {
+
+ /**
+ * Get an AccessTokenResponse from the store. The access token and refresh token are encrypted
+ * and therefore will be decrypted before returning.
+ *
+ * If the storage is not available, it is still possible to get the AccessTokenResponse from memory cache.
+ * However, the last-used statistics will be broken. It is a measured risk to take.
+ *
+ * @param handle the handle given by the call
+ * {@code OAuthFactory#createOAuthClientService(String, String, String, String, String, Boolean)}
+ * @return AccessTokenResponse if available, null if not.
+ * @throws GeneralSecurityException when the token cannot be decrypted.
+ */
+ @Nullable
+ AccessTokenResponse loadAccessTokenResponse(String handle) throws GeneralSecurityException;
+
+ /**
+ * Save the {@code AccessTokenResponse} by the handle
+ *
+ * @param handle unique string used as a handle/ reference to the OAuth client service, and the underlying
+ * access tokens, configs.
+ * @param accessTokenResponse This can be null, which explicitly removes the AccessTokenResponse from store.
+ */
+ void saveAccessTokenResponse(String handle, @Nullable AccessTokenResponse accessTokenResponse);
+
+ /**
+ * Remove the token for the given handler. No exception is thrown in all cases
+ *
+ * @param handle unique string used as a handle/ reference to the OAuth client service, and the underlying
+ * access tokens, configs.
+ */
+ void remove(String handle);
+
+ /**
+ * Remove all data in the oauth store, !!!use with caution!!!
+ */
+ void removeAll();
+
+ /**
+ * Save the {@code PersistedParams} into the store
+ *
+ * @param handle unique string used as a handle/ reference to the OAuth client service, and the underlying
+ * access tokens, configs.
+ * @param persistedParams These parameters are static with respect to the oauth provider and thus can be persisted.
+ */
+ void savePersistedParams(String handle, @Nullable PersistedParams persistedParams);
+
+ /**
+ * Load the {@code PersistedParams} from the store
+ *
+ * @param handle unique string used as a handle/ reference to the OAuth client service, and the underlying
+ * access tokens, configs.
+ * @return PersistedParams when available, null if not exist
+ */
+ @Nullable
+ PersistedParams loadPersistedParams(String handle);
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandlerImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandlerImpl.java
new file mode 100644
index 00000000000..d2b75e27b96
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/OAuthStoreHandlerImpl.java
@@ -0,0 +1,439 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import static org.eclipse.smarthome.auth.oauth2client.internal.StorageRecordType.*;
+
+import java.security.GeneralSecurityException;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.auth.oauth2client.internal.cipher.SymmetricKeyCipher;
+import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse;
+import org.eclipse.smarthome.core.auth.client.oauth2.StorageCipher;
+import org.eclipse.smarthome.core.storage.Storage;
+import org.eclipse.smarthome.core.storage.StorageService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializer;
+
+/**
+ * This class handles the storage directly. It is internal to the OAuthClientService and there is
+ * little need to study this.
+ *
+ * The first role of this handler storing and caching the access token response, and persisted parameters.
+ *
+ * The storage contains these:
+ * 1. INDEX_HANDLES = json string-set of all handles
+ * 2. .LastUsed = system-time-milliseconds
+ * 3. .AccessTokenResponse = Json of AccessTokenResponse
+ * 4. .ServiceConfiguration = Json of PersistedParameters
+ *
+ * If at any time, the storage is not available, it is still possible to read existing access tokens from store.
+ * The last-used statistics for this access token is broken. It is a measured risk to take.
+ *
+ * If at any time, the storage is not available, it is not able to write any new access tokens into store.
+ *
+ * All entries are subject to removal if they have not been used for 183 days or more (half year).
+ * The recycle is performed when then instance is deactivated
+ *
+ * @author Gary Tse - Initial Contribution
+ *
+ */
+@NonNullByDefault
+@Component(property = "CIPHER_TARGET=SymmetricKeyCipher")
+public class OAuthStoreHandlerImpl implements OAuthStoreHandler {
+
+ // easy mocking with protected access
+ protected static final int EXPIRE_DAYS = 183;
+ protected static final int ACCESS_TOKEN_CACHE_SIZE = 50;
+ private static final String STORE_NAME = "StorageHandler.For.OAuthClientService";
+ private static final String STORE_KEY_INDEX_OF_HANDLES = "INDEX_HANDLES";
+
+ private final Set allHandles = new HashSet<>(); // must be initialized
+ private @NonNullByDefault({}) StorageFacade storageFacade;
+
+ private final Set allAvailableStorageCiphers = new LinkedHashSet<>();
+ private Optional storageCipher = Optional.empty();
+
+ private final Logger logger = LoggerFactory.getLogger(OAuthStoreHandlerImpl.class);
+
+ @Activate
+ public void activate(Map properties) throws GeneralSecurityException {
+ // this allows future implementations to change cipher by just setting the CIPHER_TARGET
+ String cipherTarget = (String) properties.getOrDefault("CIPHER_TARGET", SymmetricKeyCipher.CIPHER_ID);
+
+ // choose the cipher by the cipherTarget
+ storageCipher = allAvailableStorageCiphers.stream()
+ .filter(cipher -> cipher.getUniqueCipherId().equals(cipherTarget)).findFirst();
+
+ logger.debug("Using Cipher: {}", storageCipher
+ .orElseThrow(() -> new GeneralSecurityException("No StorageCipher with target=" + cipherTarget)));
+ }
+
+ /**
+ * Deactivate and free resources.
+ */
+ @Deactivate
+ public void deactivate() {
+ storageFacade.close(); // this removes old entries
+ // DS will take care of other references
+ }
+
+ @Override
+ public @Nullable AccessTokenResponse loadAccessTokenResponse(String handle) throws GeneralSecurityException {
+ AccessTokenResponse accessTokenResponseFromStore = (AccessTokenResponse) storageFacade.get(handle,
+ ACCESS_TOKEN_RESPONSE);
+
+ if (accessTokenResponseFromStore == null) {
+ // token does not exist
+ return null;
+ }
+
+ AccessTokenResponse decryptedAccessToken = decryptToken(accessTokenResponseFromStore);
+ return decryptedAccessToken;
+ }
+
+ @Override
+ public void saveAccessTokenResponse(@NonNull String handle, @Nullable AccessTokenResponse pAccessTokenResponse) {
+ AccessTokenResponse accessTokenResponse = pAccessTokenResponse;
+ if (accessTokenResponse == null) {
+ accessTokenResponse = new AccessTokenResponse(); // put empty
+ }
+
+ AccessTokenResponse encryptedToken;
+ try {
+ encryptedToken = encryptToken(accessTokenResponse);
+ } catch (GeneralSecurityException e) {
+ logger.warn("Unable to encrypt token, storing as-is", e);
+ encryptedToken = accessTokenResponse;
+ }
+ storageFacade.put(handle, encryptedToken);
+ }
+
+ @Override
+ public void remove(String handle) {
+ storageFacade.removeByHandle(handle);
+ }
+
+ @Override
+ public void removeAll() {
+ storageFacade.removeAll();
+ allHandles.clear();
+ }
+
+ @Override
+ public void savePersistedParams(String handle, @Nullable PersistedParams persistedParams) {
+ storageFacade.put(handle, persistedParams);
+ }
+
+ @Override
+ public @Nullable PersistedParams loadPersistedParams(String handle) {
+ PersistedParams persistedParams = (PersistedParams) storageFacade.get(handle, SERVICE_CONFIGURATION);
+ return persistedParams;
+ }
+
+ private AccessTokenResponse encryptToken(AccessTokenResponse accessTokenResponse) throws GeneralSecurityException {
+ AccessTokenResponse encryptedAccessToken = (AccessTokenResponse) accessTokenResponse.clone();
+
+ if (accessTokenResponse.getAccessToken() != null) {
+ encryptedAccessToken.setAccessToken(encrypt(accessTokenResponse.getAccessToken()));
+ }
+ if (accessTokenResponse.getRefreshToken() != null) {
+ encryptedAccessToken.setRefreshToken(encrypt(accessTokenResponse.getRefreshToken()));
+ }
+ return encryptedAccessToken;
+ }
+
+ private AccessTokenResponse decryptToken(AccessTokenResponse accessTokenResponse) throws GeneralSecurityException {
+ AccessTokenResponse decryptedToken = (AccessTokenResponse) accessTokenResponse.clone();
+ if (!storageCipher.isPresent()) {
+ return decryptedToken; // do nothing if no cipher
+ }
+ logger.debug("Decrypting token: {}", accessTokenResponse);
+ decryptedToken.setAccessToken(storageCipher.get().decrypt(accessTokenResponse.getAccessToken()));
+ decryptedToken.setRefreshToken(storageCipher.get().decrypt(accessTokenResponse.getRefreshToken()));
+ return decryptedToken;
+ }
+
+ private @Nullable String encrypt(String token) throws GeneralSecurityException {
+ if (!storageCipher.isPresent()) {
+ return token; // do nothing if no cipher
+ } else {
+ StorageCipher cipher = storageCipher.get();
+ return cipher.encrypt(token);
+ }
+ }
+
+ @Reference
+ protected synchronized void setStorageService(StorageService storageService) {
+ storageFacade = new StorageFacade(storageService.getStorage(STORE_NAME));
+ }
+
+ protected synchronized void unsetStorageService(StorageService storageService) {
+ storageFacade.close();
+ storageFacade = null;
+ }
+
+ /**
+ * Static policy -- don't want to change cipher on the fly!
+ * There may be multiple storage ciphers, choose the one that matches the target (done at activate)
+ *
+ */
+ @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE)
+ protected synchronized void setStorageCipher(StorageCipher storageCipher) {
+ // keep all ciphers
+ allAvailableStorageCiphers.add(storageCipher);
+ }
+
+ protected synchronized void unsetStorageCipher(StorageCipher storageCipher) {
+ allAvailableStorageCiphers.remove(storageCipher);
+ if (this.storageCipher.isPresent() && this.storageCipher.get() == storageCipher) {
+ this.storageCipher = Optional.empty();
+ }
+ }
+
+ private boolean isExpired(@Nullable LocalDateTime lastUsed) {
+ if (lastUsed == null) {
+ return false;
+ }
+ // (last used + 183 days < now) then it is expired
+ return lastUsed.plusDays(EXPIRE_DAYS).isBefore(LocalDateTime.now());
+ }
+
+ /**
+ * This is designed to simplify all the locking required for the store.
+ */
+ private class StorageFacade implements AutoCloseable {
+ private final Storage storage;
+ private final Lock storageLock = new ReentrantLock(); // for all operations on the storage
+ private final Gson gson;
+
+ public StorageFacade(Storage storage) {
+ this.storage = storage;
+ // Add adapters for LocalDateTime
+ gson = new GsonBuilder()
+ .registerTypeAdapter(LocalDateTime.class,
+ (JsonDeserializer) (json, typeOfT, context) -> LocalDateTime
+ .parse(json.getAsString()))
+ .registerTypeAdapter(LocalDateTime.class,
+ (JsonSerializer) (date, type,
+ jsonSerializationContext) -> new JsonPrimitive(date.toString()))
+ .setPrettyPrinting().create();
+ }
+
+ public Set getAllHandlesFromIndex() {
+ Set handlesFromStoreageIndex = new HashSet<>();
+ try {
+ String allHandlesStr = get(STORE_KEY_INDEX_OF_HANDLES);
+ logger.debug("All available handles: {}", allHandlesStr);
+ if (allHandlesStr == null) {
+ return handlesFromStoreageIndex;
+ }
+ return gson.fromJson(allHandlesStr, HashSet.class);
+ } catch (RuntimeException storeNotAvailable) {
+ return handlesFromStoreageIndex; // empty
+ }
+ }
+
+ public @Nullable String get(String key) {
+ storageLock.lock();
+ try {
+ return storage.get(key);
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public @Nullable Object get(String handle, StorageRecordType recordType) {
+ storageLock.lock();
+ try {
+ String value = storage.get(recordType.getKey(handle));
+ if (value == null) {
+ return null;
+ }
+
+ // update last used when it is an access token
+ if (recordType.equals(ACCESS_TOKEN_RESPONSE)) {
+ try {
+ AccessTokenResponse accessTokenResponse = gson.fromJson(value, AccessTokenResponse.class);
+ return accessTokenResponse;
+ } catch (Exception e) {
+ logger.error(
+ "Unable to deserialize json, discarding AccessTokenResponse. "
+ + "Please check json against standard or with oauth provider. json:\n{}",
+ value, e);
+ return null;
+ }
+ } else if (recordType.equals(SERVICE_CONFIGURATION)) {
+ try {
+ PersistedParams params = gson.fromJson(value, PersistedParams.class);
+ return params;
+ } catch (Exception e) {
+ logger.error("Unable to deserialize json, discarding PersistedParams. json:\n{}", value, e);
+ return null;
+ }
+ } else if (recordType.equals(LAST_USED)) {
+ try {
+ LocalDateTime lastUsedDate = gson.fromJson(value, LocalDateTime.class);
+ return lastUsedDate;
+ } catch (Exception e) {
+ logger.info("Unable to deserialize json, reset LAST_USED to now. json:\n{}", value);
+ return LocalDateTime.now();
+ }
+ }
+ return null;
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public void put(String handle, @Nullable LocalDateTime lastUsed) {
+ storageLock.lock();
+ try {
+ if (lastUsed == null) {
+ storage.put(LAST_USED.getKey(handle), (String) null);
+ } else {
+ String gsonStr = gson.toJson(lastUsed);
+ storage.put(LAST_USED.getKey(handle), gsonStr);
+ }
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public void put(String handle, @Nullable AccessTokenResponse accessTokenResponse) {
+ storageLock.lock();
+ try {
+ if (accessTokenResponse == null) {
+ storage.put(ACCESS_TOKEN_RESPONSE.getKey(handle), (String) null);
+ } else {
+ String gsonAccessTokenStr = gson.toJson(accessTokenResponse);
+ storage.put(ACCESS_TOKEN_RESPONSE.getKey(handle), gsonAccessTokenStr);
+ String gsonDateStr = gson.toJson(LocalDateTime.now());
+ storage.put(LAST_USED.getKey(handle), gsonDateStr);
+
+ if (!allHandles.contains(handle)) {
+ // update all handles index
+ allHandles.add(handle);
+ storage.put(STORE_KEY_INDEX_OF_HANDLES, gson.toJson(allHandles));
+ }
+ }
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public void put(String handle, @Nullable PersistedParams persistedParams) {
+ storageLock.lock();
+ try {
+ if (persistedParams == null) {
+ storage.put(SERVICE_CONFIGURATION.getKey(handle), (String) null);
+ } else {
+ String gsonPersistedParamsStr = gson.toJson(persistedParams);
+ storage.put(SERVICE_CONFIGURATION.getKey(handle), gsonPersistedParamsStr);
+ String gsonDateStr = gson.toJson(LocalDateTime.now());
+ storage.put(LAST_USED.getKey(handle), gsonDateStr);
+ if (!allHandles.contains(handle)) {
+ // update all handles index
+ allHandles.add(handle);
+ storage.put(STORE_KEY_INDEX_OF_HANDLES, gson.toJson(allHandles));
+ }
+ }
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public void removeByHandle(String handle) {
+ logger.debug("Removing handle {} from storage", handle);
+ storageLock.lock();
+ try {
+ if (allHandles.remove(handle)) { // entry exists and successfully removed
+ storage.remove(ACCESS_TOKEN_RESPONSE.getKey(handle));
+ storage.remove(LAST_USED.getKey(handle));
+ storage.remove(SERVICE_CONFIGURATION.getKey(handle));
+ storage.put(STORE_KEY_INDEX_OF_HANDLES, gson.toJson(allHandles)); // update all handles
+ }
+ } finally {
+ storageLock.unlock();
+ }
+ }
+
+ public void removeAll() {
+ // no need any locks, the other methods will take care of this
+ Set allHandlesFromStore = getAllHandlesFromIndex();
+ for (String handle : allHandlesFromStore) {
+ removeByHandle(handle);
+ }
+ }
+
+ @Override
+ public void close() {
+ boolean lockGained = false;
+ try {
+ // dont want to wait too long during shutdown or update
+ lockGained = storageLock.tryLock(15, TimeUnit.SECONDS);
+
+ // if lockGained within timeout, then try to remove old entries
+ if (lockGained) {
+ String handlesSSV = this.storage.get(STORE_KEY_INDEX_OF_HANDLES);
+ if (handlesSSV != null) {
+ String[] handles = handlesSSV.trim().split(" ");
+ for (String handle : handles) {
+ LocalDateTime lastUsed = (LocalDateTime) get(handle, LAST_USED);
+ if (isExpired(lastUsed)) {
+ removeByHandle(handle);
+ }
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ // if lock is not acquired within the timeout or thread is interruted
+ // then forget about the old entries, do not try to delete them.
+ // re-setting thread state to interrupted
+ Thread.currentThread().interrupt();
+ } finally {
+ if (lockGained) {
+ try {
+ storageLock.unlock();
+ } catch (IllegalMonitorStateException e) {
+ // never reach here normally
+ logger.error("Unexpected attempt to unlock without lock", e);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/PersistedParams.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/PersistedParams.java
new file mode 100644
index 00000000000..ec9d4d15dcd
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/PersistedParams.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Params that need to be persisted.
+ *
+ * @author Michael Bock - Initial contribution
+ * @author Gary Tse - Initial contribution
+ * @author Hilbrand Bouwkamp - Moved class to it's own file and added hashCode and equals methods
+ */
+class PersistedParams {
+ String handle;
+ String tokenUrl;
+ String authorizationUrl;
+ String clientId;
+ String clientSecret;
+ String scope;
+ Boolean supportsBasicAuth;
+ String state;
+ String redirectUri;
+ int tokenExpiresInSeconds = 60;
+
+ /**
+ * Default constructor needed for json serialization.
+ */
+ public PersistedParams() {
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param handle the handle to the oauth service
+ * @param tokenUrl the token url of the oauth provider. This is used for getting access token.
+ * @param authorizationUrl the authorization url of the oauth provider. This is used purely for generating
+ * authorization code/ url.
+ * @param clientId the client id
+ * @param clientSecret the client secret (optional)
+ * @param scope the desired scope
+ * @param supportsBasicAuth whether the OAuth provider supports basic authorization or the client id and client
+ * secret should be passed as form params. true - use http basic authentication, false - do not use http
+ * basic authentication, null - unknown (default to do not use)
+ * @param tokenExpiresInSeconds Positive integer; a small time buffer in seconds. It is used to calculate the expiry
+ * of the access tokens. This allows the access token to expire earlier than the
+ * official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke,
+ * only to find the token immediately expired.
+ */
+ public PersistedParams(String handle, String tokenUrl, String authorizationUrl, String clientId,
+ String clientSecret, String scope, Boolean supportsBasicAuth, int tokenExpiresInSeconds) {
+ this.handle = handle;
+ this.tokenUrl = tokenUrl;
+ this.authorizationUrl = authorizationUrl;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.scope = scope;
+ this.supportsBasicAuth = supportsBasicAuth;
+ this.tokenExpiresInSeconds = tokenExpiresInSeconds;
+
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((authorizationUrl == null) ? 0 : authorizationUrl.hashCode());
+ result = prime * result + ((clientId == null) ? 0 : clientId.hashCode());
+ result = prime * result + ((clientSecret == null) ? 0 : clientSecret.hashCode());
+ result = prime * result + ((handle == null) ? 0 : handle.hashCode());
+ result = prime * result + ((redirectUri == null) ? 0 : redirectUri.hashCode());
+ result = prime * result + ((scope == null) ? 0 : scope.hashCode());
+ result = prime * result + ((state == null) ? 0 : state.hashCode());
+ result = prime * result + ((supportsBasicAuth == null) ? 0 : supportsBasicAuth.hashCode());
+ result = prime * result + tokenExpiresInSeconds;
+ result = prime * result + ((tokenUrl == null) ? 0 : tokenUrl.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PersistedParams other = (PersistedParams) obj;
+ if (authorizationUrl == null) {
+ if (other.authorizationUrl != null) {
+ return false;
+ }
+ } else if (!authorizationUrl.equals(other.authorizationUrl)) {
+ return false;
+ }
+ if (clientId == null) {
+ if (other.clientId != null) {
+ return false;
+ }
+ } else if (!clientId.equals(other.clientId)) {
+ return false;
+ }
+ if (clientSecret == null) {
+ if (other.clientSecret != null) {
+ return false;
+ }
+ } else if (!clientSecret.equals(other.clientSecret)) {
+ return false;
+ }
+ if (handle == null) {
+ if (other.handle != null) {
+ return false;
+ }
+ } else if (!handle.equals(other.handle)) {
+ return false;
+ }
+ if (redirectUri == null) {
+ if (other.redirectUri != null) {
+ return false;
+ }
+ } else if (!redirectUri.equals(other.redirectUri)) {
+ return false;
+ }
+ if (scope == null) {
+ if (other.scope != null) {
+ return false;
+ }
+ } else if (!scope.equals(other.scope)) {
+ return false;
+ }
+ if (state == null) {
+ if (other.state != null) {
+ return false;
+ }
+ } else if (!state.equals(other.state)) {
+ return false;
+ }
+ if (supportsBasicAuth == null) {
+ if (other.supportsBasicAuth != null) {
+ return false;
+ }
+ } else if (!supportsBasicAuth.equals(other.supportsBasicAuth)) {
+ return false;
+ }
+ if (tokenExpiresInSeconds != other.tokenExpiresInSeconds) {
+ return false;
+ }
+ if (tokenUrl == null) {
+ if (other.tokenUrl != null) {
+ return false;
+ }
+ } else if (!tokenUrl.equals(other.tokenUrl)) {
+ return false;
+ }
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/StorageRecordType.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/StorageRecordType.java
new file mode 100644
index 00000000000..ac37ebb3d6a
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/StorageRecordType.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Enum of types being used in the store
+ *
+ * @author Gary Tse - Initial Contribution
+ *
+ */
+public enum StorageRecordType {
+
+ LAST_USED(".LastUsed"),
+ ACCESS_TOKEN_RESPONSE(".AccessTokenResponse"),
+ SERVICE_CONFIGURATION(".ServiceConfiguration");
+
+ private String suffix;
+
+ private StorageRecordType(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public @NonNull String getKey(String handle) {
+ return (handle == null) ? this.suffix : (handle + this.suffix);
+ }
+
+}
diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java
new file mode 100644
index 00000000000..0b547d92fa4
--- /dev/null
+++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/eclipse/smarthome/auth/oauth2client/internal/cipher/SymmetricKeyCipher.java
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.smarthome.auth.oauth2client.internal.cipher;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.auth.client.oauth2.StorageCipher;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a symmetric key encryption service for encrypting the OAuth tokens.
+ *
+ * @author Gary Tse - Initial Contribution
+ *
+ */
+@NonNullByDefault
+@Component
+public class SymmetricKeyCipher implements StorageCipher {
+
+ public static final String CIPHER_ID = "SymmetricKeyCipher";
+ public static final String PID = CIPHER_ID;
+ private final Logger logger = LoggerFactory.getLogger(SymmetricKeyCipher.class);
+
+ private static final String ENCRYPTION_ALGO = "AES";
+ private static final String ENCRYPTION_ALGO_MODE_WITH_PADDING = "AES/CBC/PKCS5Padding";
+ private static final String PROPERTY_KEY_ENCRYPTION_KEY_BASE64 = "ENCRYPTION_KEY";
+ private static final int ENCRYPTION_KEY_SIZE_BITS = 128; // do not use high grade encryption due to export limit
+ private static final int IV_BYTE_SIZE = 16;
+
+ @NonNullByDefault({})
+ private ConfigurationAdmin configurationAdmin;
+ @NonNullByDefault({})
+ private SecretKey encryptionKey;
+
+ private final SecureRandom random = new SecureRandom();
+
+ /**
+ * Activate will try to load the encryption key. If an existing encryption key does not exists,
+ * it will generate a new one and save to {@code org.osgi.service.cm.ConfigurationAdmin}
+ *
+ * @throws NoSuchAlgorithmException When encryption algorithm is not available {@code #ENCRYPTION_ALGO}
+ * @throws IOException if access to persistent storage fails (@code org.osgi.service.cm.ConfigurationAdmin)
+ */
+ @Activate
+ public void activate() throws NoSuchAlgorithmException, IOException {
+ // load or generate the encryption key
+ encryptionKey = getOrGenerateEncryptionKey();
+ }
+
+ @Override
+ public String getUniqueCipherId() {
+ return CIPHER_ID;
+ }
+
+ @Nullable
+ @Override
+ public String encrypt(@Nullable String plainText) throws GeneralSecurityException {
+ if (plainText == null) {
+ return null;
+ }
+
+ // Generate IV
+ byte iv[] = new byte[IV_BYTE_SIZE];
+ random.nextBytes(iv);
+ Cipher cipherEnc = Cipher.getInstance(ENCRYPTION_ALGO_MODE_WITH_PADDING);
+ cipherEnc.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
+ byte[] encryptedBytes = cipherEnc.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+ byte[] encryptedBytesWithIV = new byte[encryptedBytes.length + IV_BYTE_SIZE];
+
+ // copy iv to the start of array
+ System.arraycopy(iv, 0, encryptedBytesWithIV, 0, IV_BYTE_SIZE);
+ // append encrypted text to tail
+ System.arraycopy(encryptedBytes, 0, encryptedBytesWithIV, IV_BYTE_SIZE, encryptedBytes.length);
+ String encryptedBase64String = Base64.getEncoder().encodeToString(encryptedBytesWithIV);
+
+ return encryptedBase64String;
+ }
+
+ @Nullable
+ @Override
+ public String decrypt(@Nullable String base64CipherText) throws GeneralSecurityException {
+ if (base64CipherText == null) {
+ return null;
+ }
+ // base64 decode the base64CipherText
+ byte[] decodedCipherTextWithIV = Base64.getDecoder().decode(base64CipherText);
+ // Read IV
+ byte[] iv = new byte[IV_BYTE_SIZE];
+ System.arraycopy(decodedCipherTextWithIV, 0, iv, 0, IV_BYTE_SIZE);
+
+ byte[] cipherTextBytes = new byte[decodedCipherTextWithIV.length - IV_BYTE_SIZE];
+ System.arraycopy(decodedCipherTextWithIV, IV_BYTE_SIZE, cipherTextBytes, 0, cipherTextBytes.length);
+
+ Cipher cipherDec = Cipher.getInstance(ENCRYPTION_ALGO_MODE_WITH_PADDING);
+ cipherDec.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
+ byte[] decryptedBytes = cipherDec.doFinal(cipherTextBytes);
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
+ }
+
+ private static SecretKey generateEncryptionKey() throws NoSuchAlgorithmException {
+ KeyGenerator keygen = KeyGenerator.getInstance(ENCRYPTION_ALGO);
+ keygen.init(ENCRYPTION_KEY_SIZE_BITS);
+ SecretKey secretKey = keygen.generateKey();
+ return secretKey;
+ }
+
+ private SecretKey getOrGenerateEncryptionKey() throws NoSuchAlgorithmException, IOException {
+ Configuration configuration = configurationAdmin.getConfiguration(PID);
+ String encryptionKeyInBase64 = null;
+ Dictionary properties = configuration.getProperties();
+ if (properties == null) {
+ properties = new Hashtable<>();
+ }
+
+ if (properties.get(PROPERTY_KEY_ENCRYPTION_KEY_BASE64) == null) {
+ encryptionKey = generateEncryptionKey();
+ encryptionKeyInBase64 = new String(Base64.getEncoder().encode(encryptionKey.getEncoded()));
+
+ // Put encryption key back into config
+ properties.put(PROPERTY_KEY_ENCRYPTION_KEY_BASE64, encryptionKeyInBase64);
+ configuration.update(properties);
+
+ logger.debug("Encryption key generated");
+ } else {
+ // encryption key already present in config
+ encryptionKeyInBase64 = (String) properties.get(PROPERTY_KEY_ENCRYPTION_KEY_BASE64);
+ byte[] encKeyBytes = Base64.getDecoder().decode(encryptionKeyInBase64);
+ // 128 bit key/ 8 bit = 16 bytes length
+ encryptionKey = new SecretKeySpec(encKeyBytes, 0, ENCRYPTION_KEY_SIZE_BITS / 8, ENCRYPTION_ALGO);
+
+ logger.debug("Encryption key loaded");
+ }
+ return encryptionKey;
+ }
+
+ @Reference
+ public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+ this.configurationAdmin = configurationAdmin;
+ }
+
+ public void unsetConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+ this.configurationAdmin = configurationAdmin;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.media/.classpath b/bundles/org.openhab.core.automation.module.media/.classpath
new file mode 100644
index 00000000000..3c5e7d17553
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.media/.project b/bundles/org.openhab.core.automation.module.media/.project
new file mode 100644
index 00000000000..5ff68533733
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.automation.module.media
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..0c4313356dd
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding/=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.automation.module.media/NOTICE b/bundles/org.openhab.core.automation.module.media/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/org.openhab.core.automation.module.media/pom.xml b/bundles/org.openhab.core.automation.module.media/pom.xml
new file mode 100644
index 00000000000..9e862a1c02a
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/pom.xml
@@ -0,0 +1,29 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.automation.module.media
+
+ openHAB Core :: Bundles :: Automation Media Modules
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.module.script
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.voice
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java
new file mode 100644
index 00000000000..4666b48ae26
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaActionTypeProvider.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.media.internal;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang.StringUtils;
+import org.eclipse.smarthome.config.core.ConfigConstants;
+import org.eclipse.smarthome.config.core.ConfigDescriptionParameter;
+import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type;
+import org.eclipse.smarthome.config.core.ConfigDescriptionParameterBuilder;
+import org.eclipse.smarthome.config.core.ParameterOption;
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.eclipse.smarthome.core.audio.AudioSink;
+import org.eclipse.smarthome.core.common.registry.ProviderChangeListener;
+import org.openhab.core.automation.Visibility;
+import org.openhab.core.automation.type.ActionType;
+import org.openhab.core.automation.type.ModuleType;
+import org.openhab.core.automation.type.ModuleTypeProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * This class dynamically provides the Play action type.
+ * This is necessary since there is no other way to provide dynamic config param options for module types.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ * @author Simon Kaufmann - added "say" action
+ *
+ */
+@Component(immediate = true)
+public class MediaActionTypeProvider implements ModuleTypeProvider {
+
+ private AudioManager audioManager;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ModuleType getModuleType(String UID, Locale locale) {
+ if (PlayActionHandler.TYPE_ID.equals(UID)) {
+ return getPlayActionType(locale);
+ } else if (SayActionHandler.TYPE_ID.equals(UID)) {
+ return getSayActionType(locale);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Collection getModuleTypes(Locale locale) {
+ return Stream.of(getPlayActionType(locale), getSayActionType(locale)).collect(Collectors.toList());
+ }
+
+ private ModuleType getPlayActionType(Locale locale) {
+ return new ActionType(PlayActionHandler.TYPE_ID, getConfigPlayDesc(locale), "play a sound",
+ "Plays a sound file.", null, Visibility.VISIBLE, new ArrayList<>(), new ArrayList<>());
+ }
+
+ private ModuleType getSayActionType(Locale locale) {
+ return new ActionType(SayActionHandler.TYPE_ID, getConfigSayDesc(locale), "say something",
+ "Speaks a given text through a natural voice.", null, Visibility.VISIBLE, new ArrayList<>(),
+ new ArrayList<>());
+ }
+
+ private List getConfigPlayDesc(Locale locale) {
+ ConfigDescriptionParameter param1 = ConfigDescriptionParameterBuilder
+ .create(PlayActionHandler.PARAM_SOUND, Type.TEXT).withRequired(true).withLabel("Sound")
+ .withDescription("the sound to play").withOptions(getSoundOptions()).withLimitToOptions(true).build();
+ return Stream.of(param1, getAudioSinkConfigDescParam(locale)).collect(Collectors.toList());
+ }
+
+ private List getConfigSayDesc(Locale locale) {
+ ConfigDescriptionParameter param1 = ConfigDescriptionParameterBuilder
+ .create(SayActionHandler.PARAM_TEXT, Type.TEXT).withRequired(true).withLabel("Text")
+ .withDescription("the text to speak").build();
+ return Stream.of(param1, getAudioSinkConfigDescParam(locale)).collect(Collectors.toList());
+ }
+
+ private ConfigDescriptionParameter getAudioSinkConfigDescParam(Locale locale) {
+ ConfigDescriptionParameter param2 = ConfigDescriptionParameterBuilder
+ .create(SayActionHandler.PARAM_SINK, Type.TEXT).withRequired(false).withLabel("Sink")
+ .withDescription("the audio sink id").withOptions(getSinkOptions(locale)).withLimitToOptions(true)
+ .build();
+ return param2;
+ }
+
+ /**
+ * This method creates one option for every file that is found in the sounds directory.
+ * As a label, the file extension is removed and the string is capitalized.
+ *
+ * @return a list of parameter options representing the sound files
+ */
+ private List getSoundOptions() {
+ List options = new ArrayList<>();
+ File soundsDir = Paths.get(ConfigConstants.getConfigFolder(), AudioManager.SOUND_DIR).toFile();
+ if (soundsDir.isDirectory()) {
+ for (String fileName : soundsDir.list()) {
+ if (fileName.contains(".") && !fileName.startsWith(".")) {
+ String soundName = StringUtils.capitalize(fileName.substring(0, fileName.lastIndexOf(".")));
+ options.add(new ParameterOption(fileName, soundName));
+ }
+ }
+ }
+ return options;
+ }
+
+ /**
+ * This method creates one option for every sink that is found in the system.
+ *
+ * @return a list of parameter options representing the audio sinks
+ */
+ private List getSinkOptions(Locale locale) {
+ List options = new ArrayList<>();
+
+ for (AudioSink sink : audioManager.getAllSinks()) {
+ options.add(new ParameterOption(sink.getId(), sink.getLabel(locale)));
+ }
+ return options;
+ }
+
+ @Override
+ public void addProviderChangeListener(ProviderChangeListener listener) {
+ // does nothing because this provider does not change
+ }
+
+ @Override
+ public Collection getAll() {
+ return getModuleTypes(null);
+ }
+
+ @Override
+ public void removeProviderChangeListener(ProviderChangeListener listener) {
+ // does nothing because this provider does not change
+ }
+
+ @Reference
+ protected void setAudioManager(AudioManager audioManager) {
+ this.audioManager = audioManager;
+ }
+
+ protected void unsetAudioManager(AudioManager audioManager) {
+ this.audioManager = null;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java
new file mode 100644
index 00000000000..9561e5f0e8d
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaModuleHandlerFactory.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.media.internal;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+
+import java.util.Collection;
+
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.eclipse.smarthome.core.voice.VoiceManager;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
+import org.openhab.core.automation.handler.ModuleHandler;
+import org.openhab.core.automation.handler.ModuleHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@Component(service = ModuleHandlerFactory.class)
+public class MediaModuleHandlerFactory extends BaseModuleHandlerFactory {
+
+ private static final Collection TYPES = unmodifiableList(
+ asList(SayActionHandler.TYPE_ID, PlayActionHandler.TYPE_ID));
+ private VoiceManager voiceManager;
+ private AudioManager audioManager;
+
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public Collection getTypes() {
+ return TYPES;
+ }
+
+ @Override
+ protected ModuleHandler internalCreate(Module module, String ruleUID) {
+ if (module instanceof Action) {
+ switch (module.getTypeUID()) {
+ case SayActionHandler.TYPE_ID:
+ return new SayActionHandler((Action) module, voiceManager);
+ case PlayActionHandler.TYPE_ID:
+ return new PlayActionHandler((Action) module, audioManager);
+ default:
+ break;
+ }
+ }
+ return null;
+ }
+
+ @Reference
+ protected void setAudioManager(AudioManager audioManager) {
+ this.audioManager = audioManager;
+ }
+
+ protected void unsetAudioManager(AudioManager audioManager) {
+ this.audioManager = null;
+ }
+
+ @Reference
+ protected void setVoiceManager(VoiceManager voiceManager) {
+ this.voiceManager = voiceManager;
+ }
+
+ protected void unsetVoiceManager(VoiceManager voiceManager) {
+ this.voiceManager = null;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaScriptScopeProvider.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaScriptScopeProvider.java
new file mode 100644
index 00000000000..4ac970b0fc1
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/MediaScriptScopeProvider.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.media.internal;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.eclipse.smarthome.core.voice.VoiceManager;
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * This is a scope provider for features that are related to audio and voice support.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ *
+ */
+@Component
+public class MediaScriptScopeProvider implements ScriptExtensionProvider {
+ Map elements = new HashMap<>();
+
+ @Reference
+ protected void setAudioManager(AudioManager audioManager) {
+ elements.put("audio", audioManager);
+ }
+
+ protected void unsetAudioManager(AudioManager audioManager) {
+ elements.remove("audio");
+ }
+
+ @Reference
+ protected void setVoiceManager(VoiceManager voiceManager) {
+ elements.put("voice", voiceManager);
+ }
+
+ protected void unsetVoiceManager(VoiceManager voiceManager) {
+ elements.remove("voice");
+ }
+
+ @Override
+ public Collection getDefaultPresets() {
+ return Collections.singleton("media");
+ }
+
+ @Override
+ public Collection getPresets() {
+ return Collections.singleton("media");
+ }
+
+ @Override
+ public Collection getTypes() {
+ return elements.keySet();
+ }
+
+ @Override
+ public Object get(String scriptIdentifier, String type) {
+ return elements.get(type);
+ }
+
+ @Override
+ public Map importPreset(String scriptIdentifier, String preset) {
+ return elements;
+ }
+
+ @Override
+ public void unload(String scriptIdentifier) {
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java
new file mode 100644
index 00000000000..a2d1a308890
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/PlayActionHandler.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.media.internal;
+
+import java.util.Map;
+
+import org.eclipse.smarthome.core.audio.AudioException;
+import org.eclipse.smarthome.core.audio.AudioManager;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.handler.ActionHandler;
+import org.openhab.core.automation.handler.BaseModuleHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is an ModuleHandler implementation for Actions that play a sound file from the file system.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class PlayActionHandler extends BaseModuleHandler implements ActionHandler {
+
+ public static final String TYPE_ID = "media.PlayAction";
+ public static final String PARAM_SOUND = "sound";
+ public static final String PARAM_SINK = "sink";
+
+ private final Logger logger = LoggerFactory.getLogger(PlayActionHandler.class);
+
+ private final AudioManager audioManager;
+
+ public PlayActionHandler(Action module, AudioManager audioManager) {
+ super(module);
+ this.audioManager = audioManager;
+ }
+
+ @Override
+ public Map execute(Map context) {
+ String sound = module.getConfiguration().get(PARAM_SOUND).toString();
+ String sink = (String) module.getConfiguration().get(PARAM_SINK);
+ try {
+ audioManager.playFile(sound, sink);
+ } catch (AudioException e) {
+ logger.error("Error playing sound '{}': {}", sound, e.getMessage());
+ }
+ return null;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java
new file mode 100644
index 00000000000..4b9158e9bd4
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.media/src/main/java/org/openhab/core/automation/module/media/internal/SayActionHandler.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.media.internal;
+
+import java.util.Map;
+
+import org.eclipse.smarthome.core.voice.VoiceManager;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.handler.ActionHandler;
+import org.openhab.core.automation.handler.BaseModuleHandler;
+
+/**
+ * This is an ModuleHandler implementation for Actions that trigger a TTS output through "say".
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class SayActionHandler extends BaseModuleHandler implements ActionHandler {
+
+ public static final String TYPE_ID = "media.SayAction";
+ public static final String PARAM_TEXT = "text";
+ public static final String PARAM_SINK = "sink";
+
+ private final VoiceManager voiceManager;
+
+ public SayActionHandler(Action module, VoiceManager voiceManager) {
+ super(module);
+ this.voiceManager = voiceManager;
+ }
+
+ @Override
+ public Map execute(Map context) {
+ String text = module.getConfiguration().get(PARAM_TEXT).toString();
+ String sink = (String) module.getConfiguration().get(PARAM_SINK);
+ voiceManager.say(text, null, sink);
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/.classpath b/bundles/org.openhab.core.automation.module.script.rulesupport/.classpath
new file mode 100644
index 00000000000..372f1810d64
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/.project b/bundles/org.openhab.core.automation.module.script.rulesupport/.project
new file mode 100644
index 00000000000..a0b5f4ea703
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.automation.module.script.rulesupport
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..00777dc9bed
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/=UTF-8
+encoding/ESH-INF=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/NOTICE b/bundles/org.openhab.core.automation.module.script.rulesupport/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml b/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml
new file mode 100644
index 00000000000..71954bfe1ef
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml
@@ -0,0 +1,29 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.automation.module.script.rulesupport
+
+ openHAB Core :: Bundles :: Automation Script RuleSupport
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation.module.script
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java
new file mode 100644
index 00000000000..73de464cf0c
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/AbstractScriptedModuleHandlerFactory.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal;
+
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
+import org.openhab.core.automation.handler.ModuleHandler;
+import org.openhab.core.automation.module.script.rulesupport.internal.delegates.SimpleActionHandlerDelegate;
+import org.openhab.core.automation.module.script.rulesupport.internal.delegates.SimpleConditionHandlerDelegate;
+import org.openhab.core.automation.module.script.rulesupport.internal.delegates.SimpleTriggerHandlerDelegate;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedActionHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedConditionHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedTriggerHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AbstractScriptedModuleHandlerFactory} wrappes the ScriptedHandler based on the underlying type.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public abstract class AbstractScriptedModuleHandlerFactory extends BaseModuleHandlerFactory {
+ Logger logger = LoggerFactory.getLogger(AbstractScriptedModuleHandlerFactory.class);
+
+ protected ModuleHandler getModuleHandler(Module module, ScriptedHandler scriptedHandler) {
+ ModuleHandler moduleHandler = null;
+
+ if (scriptedHandler != null) {
+ if (scriptedHandler instanceof SimpleActionHandler) {
+ moduleHandler = new SimpleActionHandlerDelegate((Action) module, (SimpleActionHandler) scriptedHandler);
+ } else if (scriptedHandler instanceof SimpleConditionHandler) {
+ moduleHandler = new SimpleConditionHandlerDelegate((Condition) module,
+ (SimpleConditionHandler) scriptedHandler);
+ } else if (scriptedHandler instanceof SimpleTriggerHandler) {
+ moduleHandler = new SimpleTriggerHandlerDelegate((Trigger) module,
+ (SimpleTriggerHandler) scriptedHandler);
+ } else if (scriptedHandler instanceof ScriptedActionHandlerFactory) {
+ moduleHandler = ((ScriptedActionHandlerFactory) scriptedHandler).get((Action) module);
+ } else if (scriptedHandler instanceof ScriptedTriggerHandlerFactory) {
+ moduleHandler = ((ScriptedTriggerHandlerFactory) scriptedHandler).get((Trigger) module);
+ } else if (scriptedHandler instanceof ScriptedConditionHandlerFactory) {
+ moduleHandler = ((ScriptedConditionHandlerFactory) scriptedHandler).get((Condition) module);
+ } else {
+ logger.error("Not supported moduleHandler: {}", module.getTypeUID());
+ }
+ }
+
+ return moduleHandler;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/RuleSupportScriptExtension.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/RuleSupportScriptExtension.java
new file mode 100644
index 00000000000..579342ee30f
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/RuleSupportScriptExtension.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.smarthome.config.core.ConfigDescriptionParameter;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.Rule;
+import org.openhab.core.automation.RuleRegistry;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.Visibility;
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.openhab.core.automation.module.script.rulesupport.shared.RuleSupportRuleRegistryDelegate;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedRuleProvider;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedActionHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedConditionHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.factories.ScriptedTriggerHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler;
+import org.openhab.core.automation.type.ActionType;
+import org.openhab.core.automation.type.ModuleType;
+import org.openhab.core.automation.type.TriggerType;
+import org.openhab.core.automation.util.ActionBuilder;
+import org.openhab.core.automation.util.ConditionBuilder;
+import org.openhab.core.automation.util.ModuleBuilder;
+import org.openhab.core.automation.util.TriggerBuilder;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * This Script-Extension provides types and presets to support writing Rules using a ScriptEngine.
+ * One can write and register Rules and Modules by adding them through the HandlerRegistry and/or RuleRegistry
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@Component(immediate = true)
+public class RuleSupportScriptExtension implements ScriptExtensionProvider {
+ private static final String RULE_SUPPORT = "RuleSupport";
+ private static final String RULE_REGISTRY = "ruleRegistry";
+ private static final String AUTOMATION_MANAGER = "automationManager";
+
+ private static HashMap> presets = new HashMap<>();
+ private static HashMap staticTypes = new HashMap<>();
+ private static HashSet types = new HashSet();
+ private final ConcurrentHashMap> objectCache = new ConcurrentHashMap<>();
+
+ private RuleRegistry ruleRegistry;
+ private ScriptedRuleProvider ruleProvider;
+ private ScriptedCustomModuleHandlerFactory scriptedCustomModuleHandlerFactory;
+ private ScriptedCustomModuleTypeProvider scriptedCustomModuleTypeProvider;
+ private ScriptedPrivateModuleHandlerFactory scriptedPrivateModuleHandlerFactory;
+
+ static {
+ staticTypes.put("SimpleActionHandler", SimpleActionHandler.class);
+ staticTypes.put("SimpleConditionHandler", SimpleConditionHandler.class);
+ staticTypes.put("SimpleTriggerHandler", SimpleTriggerHandler.class);
+ staticTypes.put("SimpleRule", SimpleRule.class);
+
+ staticTypes.put("ActionHandlerFactory", ScriptedActionHandlerFactory.class);
+ staticTypes.put("ConditionHandlerFactory", ScriptedConditionHandlerFactory.class);
+ staticTypes.put("TriggerHandlerFactory", ScriptedTriggerHandlerFactory.class);
+
+ staticTypes.put("ModuleBuilder", ModuleBuilder.class);
+ staticTypes.put("ActionBuilder", ActionBuilder.class);
+ staticTypes.put("ConditionBuilder", ConditionBuilder.class);
+ staticTypes.put("TriggerBuilder", TriggerBuilder.class);
+
+ staticTypes.put("Configuration", Configuration.class);
+ staticTypes.put("Action", Action.class);
+ staticTypes.put("Condition", Condition.class);
+ staticTypes.put("Trigger", Trigger.class);
+ staticTypes.put("Rule", Rule.class);
+ staticTypes.put("ModuleType", ModuleType.class);
+ staticTypes.put("ActionType", ActionType.class);
+ staticTypes.put("TriggerType", TriggerType.class);
+ staticTypes.put("Visibility", Visibility.class);
+ staticTypes.put("ConfigDescriptionParameter", ConfigDescriptionParameter.class);
+
+ types.addAll(staticTypes.keySet());
+
+ types.add(AUTOMATION_MANAGER);
+ types.add(RULE_REGISTRY);
+
+ presets.put(RULE_SUPPORT, Arrays.asList("Configuration", "Action", "Condition", "Trigger", "Rule",
+ "ModuleBuilder", "ActionBuilder", "ConditionBuilder", "TriggerBuilder"));
+ presets.put("RuleSimple", Arrays.asList("SimpleActionHandler", "SimpleConditionHandler", "SimpleTriggerHandler",
+ "SimpleRule", "TriggerType", "ConfigDescriptionParameter", "ModuleType", "ActionType", "Visibility"));
+ presets.put("RuleFactories",
+ Arrays.asList("ActionHandlerFactory", "ConditionHandlerFactory", "TriggerHandlerFactory", "TriggerType",
+ "ConfigDescriptionParameter", "ModuleType", "ActionType", "Visibility"));
+ }
+
+ @Reference
+ public void setRuleRegistry(RuleRegistry ruleRegistry) {
+ this.ruleRegistry = ruleRegistry;
+ }
+
+ public void unsetRuleRegistry(RuleRegistry ruleRegistry) {
+ this.ruleRegistry = null;
+ }
+
+ @Reference
+ public void setRuleProvider(ScriptedRuleProvider ruleProvider) {
+ this.ruleProvider = ruleProvider;
+ }
+
+ public void unsetRuleProvider(ScriptedRuleProvider ruleProvider) {
+ this.ruleProvider = null;
+ }
+
+ @Reference
+ public void setScriptedCustomModuleHandlerFactory(ScriptedCustomModuleHandlerFactory factory) {
+ this.scriptedCustomModuleHandlerFactory = factory;
+ }
+
+ public void unsetScriptedCustomModuleHandlerFactory(ScriptedCustomModuleHandlerFactory factory) {
+ this.scriptedCustomModuleHandlerFactory = null;
+ }
+
+ @Reference
+ public void setScriptedCustomModuleTypeProvider(ScriptedCustomModuleTypeProvider scriptedCustomModuleTypeProvider) {
+ this.scriptedCustomModuleTypeProvider = scriptedCustomModuleTypeProvider;
+ }
+
+ public void unsetScriptedCustomModuleTypeProvider(
+ ScriptedCustomModuleTypeProvider scriptedCustomModuleTypeProvider) {
+ this.scriptedCustomModuleTypeProvider = null;
+ }
+
+ @Reference
+ public void setScriptedPrivateModuleHandlerFactory(ScriptedPrivateModuleHandlerFactory factory) {
+ this.scriptedPrivateModuleHandlerFactory = factory;
+ }
+
+ public void unsetScriptedPrivateModuleHandlerFactory(ScriptedPrivateModuleHandlerFactory factory) {
+ this.scriptedPrivateModuleHandlerFactory = null;
+ }
+
+ @Override
+ public Collection getDefaultPresets() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Collection getPresets() {
+ return presets.keySet();
+ }
+
+ @Override
+ public Collection getTypes() {
+ return types;
+ }
+
+ @Override
+ public Object get(String scriptIdentifier, String type) {
+ Object obj = staticTypes.get(type);
+ if (obj != null) {
+ return obj;
+ }
+
+ HashMap objects = objectCache.get(scriptIdentifier);
+
+ if (objects == null) {
+ objects = new HashMap<>();
+ objectCache.put(scriptIdentifier, objects);
+ }
+
+ obj = objects.get(type);
+ if (obj != null) {
+ return obj;
+ }
+
+ if (type.equals(AUTOMATION_MANAGER) || type.equals(RULE_REGISTRY)) {
+ RuleSupportRuleRegistryDelegate ruleRegistryDelegate = new RuleSupportRuleRegistryDelegate(ruleRegistry,
+ ruleProvider);
+ ScriptedAutomationManager automationManager = new ScriptedAutomationManager(ruleRegistryDelegate,
+ scriptedCustomModuleHandlerFactory, scriptedCustomModuleTypeProvider,
+ scriptedPrivateModuleHandlerFactory);
+
+ objects.put(AUTOMATION_MANAGER, automationManager);
+ objects.put(RULE_REGISTRY, ruleRegistryDelegate);
+
+ obj = objects.get(type);
+ }
+
+ return obj;
+ }
+
+ @Override
+ public Map importPreset(String scriptIdentifier, String preset) {
+ Map scopeValues = new HashMap<>();
+
+ Collection values = presets.get(preset);
+
+ for (String value : values) {
+ scopeValues.put(value, staticTypes.get(value));
+ }
+
+ if (preset.equals(RULE_SUPPORT)) {
+ scopeValues.put(AUTOMATION_MANAGER, get(scriptIdentifier, AUTOMATION_MANAGER));
+
+ Object ruleRegistry = get(scriptIdentifier, RULE_REGISTRY);
+ scopeValues.put(RULE_REGISTRY, ruleRegistry);
+ }
+
+ return scopeValues;
+ }
+
+ @Override
+ public void unload(String scriptIdentifier) {
+ HashMap objects = objectCache.remove(scriptIdentifier);
+
+ if (objects != null) {
+ Object hr = objects.get(AUTOMATION_MANAGER);
+ if (hr != null) {
+ ScriptedAutomationManager automationManager = (ScriptedAutomationManager) hr;
+
+ automationManager.removeAll();
+ }
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleHandlerFactory.java
new file mode 100644
index 00000000000..16b27171f12
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleHandlerFactory.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.handler.ModuleHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link ScriptedCustomModuleHandlerFactory} is used in combination with the
+ * {@link ScriptedCustomModuleTypeProvider} to allow scripts to define custom types in the RuleManager. These
+ * registered types can then be used publicly from any Rule-Editor.
+ *
+ * This class provides the handlers from the script to the RuleManager. As Jsr223 languages have different needs, it
+ * allows these handlers to be defined in different ways.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@Component(immediate = true, service = ScriptedCustomModuleHandlerFactory.class)
+public class ScriptedCustomModuleHandlerFactory extends AbstractScriptedModuleHandlerFactory {
+ private final HashMap typesHandlers = new HashMap<>();
+
+ @Override
+ public Collection getTypes() {
+ return typesHandlers.keySet();
+ }
+
+ @Override
+ protected ModuleHandler internalCreate(Module module, String ruleUID) {
+ ScriptedHandler scriptedHandler = typesHandlers.get(module.getTypeUID());
+
+ return getModuleHandler(module, scriptedHandler);
+ }
+
+ public void addModuleHandler(String uid, ScriptedHandler scriptedHandler) {
+ typesHandlers.put(uid, scriptedHandler);
+ }
+
+ public void removeModuleHandler(String uid) {
+ typesHandlers.remove(uid);
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleTypeProvider.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleTypeProvider.java
new file mode 100644
index 00000000000..5a632476d0a
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedCustomModuleTypeProvider.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+import org.eclipse.smarthome.core.common.registry.ProviderChangeListener;
+import org.openhab.core.automation.type.ModuleType;
+import org.openhab.core.automation.type.ModuleTypeProvider;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link ScriptedCustomModuleTypeProvider} is used in combination with the
+ * {@link ScriptedCustomModuleHandlerFactory} to allow scripts to define custom types in the RuleManager. These
+ * registered types can then be used publicly from any Rule-Editor.
+ *
+ * @author Simon Merschjohann - initial contribution
+ *
+ */
+@Component(immediate = true, service = { ScriptedCustomModuleTypeProvider.class, ModuleTypeProvider.class })
+public class ScriptedCustomModuleTypeProvider implements ModuleTypeProvider {
+ private final HashMap modulesTypes = new HashMap<>();
+
+ private final HashSet> listeners = new HashSet<>();
+
+ @Override
+ public Collection getAll() {
+ return modulesTypes.values();
+ }
+
+ @Override
+ public void addProviderChangeListener(ProviderChangeListener listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ public void removeProviderChangeListener(ProviderChangeListener listener) {
+ this.listeners.remove(listener);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getModuleType(String UID, Locale locale) {
+ ModuleType handler = modulesTypes.get(UID);
+
+ return (T) handler;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection getModuleTypes(Locale locale) {
+ return (Collection) modulesTypes.values();
+ }
+
+ public void addModuleType(ModuleType moduleType) {
+ modulesTypes.put(moduleType.getUID(), moduleType);
+
+ for (ProviderChangeListener listener : listeners) {
+ listener.added(this, moduleType);
+ }
+ }
+
+ public void removeModuleType(ModuleType moduleType) {
+ removeModuleType(moduleType.getUID());
+ }
+
+ public void removeModuleType(String moduleTypeUID) {
+ ModuleType element = modulesTypes.remove(moduleTypeUID);
+
+ for (ProviderChangeListener listener : listeners) {
+ listener.removed(this, element);
+ }
+ }
+
+ public void updateModuleHandler(String uid) {
+ ModuleType modType = modulesTypes.get(uid);
+
+ if (modType != null) {
+ for (ProviderChangeListener listener : listeners) {
+ listener.updated(this, null, modType);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedPrivateModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedPrivateModuleHandlerFactory.java
new file mode 100644
index 00000000000..28f204f70d3
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/ScriptedPrivateModuleHandlerFactory.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.handler.ModuleHandler;
+import org.openhab.core.automation.handler.ModuleHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ScriptedPrivateModuleHandlerFactory} is used to provide types for "private" scripted Actions, Triggers and
+ * Conditions. These Module Types are meant to be only used inside scripts.
+ *
+ * This class provides the handlers from the script to the RuleManager. As Jsr223 languages have different needs, it
+ * allows these handlers to be defined in different ways.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@Component(immediate = true, service = { ScriptedPrivateModuleHandlerFactory.class, ModuleHandlerFactory.class })
+public class ScriptedPrivateModuleHandlerFactory extends AbstractScriptedModuleHandlerFactory {
+ private static final String PRIV_ID = "privId";
+ private static final Collection TYPES = Arrays.asList("jsr223.ScriptedAction", "jsr223.ScriptedCondition",
+ "jsr223.ScriptedTrigger");
+
+ private final Logger logger = LoggerFactory.getLogger(ScriptedPrivateModuleHandlerFactory.class);
+ private final HashMap privateTypes = new HashMap<>();
+
+ private int nextId = 0;
+
+ @Override
+ public Collection getTypes() {
+ return TYPES;
+ }
+
+ @Override
+ protected ModuleHandler internalCreate(Module module, String ruleUID) {
+ ModuleHandler moduleHandler = null;
+
+ ScriptedHandler scriptedHandler = null;
+ try {
+ scriptedHandler = privateTypes.get(module.getConfiguration().get(PRIV_ID));
+ } catch (Exception e) {
+ logger.warn("ScriptedHandler {} for ruleUID {} not found", module.getConfiguration().get(PRIV_ID), ruleUID);
+ }
+
+ if (scriptedHandler != null) {
+ moduleHandler = getModuleHandler(module, scriptedHandler);
+ }
+
+ return moduleHandler;
+ }
+
+ public String addHandler(String privId, ScriptedHandler scriptedHandler) {
+ privateTypes.put(privId, scriptedHandler);
+ return privId;
+ }
+
+ public String addHandler(ScriptedHandler scriptedHandler) {
+ String privId = "i" + (nextId++);
+ privateTypes.put(privId, scriptedHandler);
+ return privId;
+ }
+
+ public void removeHandler(String privId) {
+ privateTypes.remove(privId);
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleActionHandlerDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleActionHandlerDelegate.java
new file mode 100644
index 00000000000..18523300d28
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleActionHandlerDelegate.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal.delegates;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.handler.ActionHandler;
+import org.openhab.core.automation.handler.BaseModuleHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler;
+
+/**
+ * The SimpleActionHandlerDelegate allows the registration of {@link SimpleActionHandler}s to the RuleManager.
+ *
+ * @author Simon Merschjohann
+ */
+public class SimpleActionHandlerDelegate extends BaseModuleHandler implements ActionHandler {
+
+ private org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler actionHandler;
+
+ public SimpleActionHandlerDelegate(Action module,
+ org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler actionHandler) {
+ super(module);
+ this.actionHandler = actionHandler;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public Map execute(Map inputs) {
+ Set keys = new HashSet(inputs.keySet());
+
+ Map extendedInputs = new HashMap<>(inputs);
+ for (String key : keys) {
+ Object value = extendedInputs.get(key);
+ int dotIndex = key.indexOf('.');
+ if (dotIndex != -1) {
+ String moduleName = key.substring(0, dotIndex);
+ extendedInputs.put("module", moduleName);
+ String newKey = key.substring(dotIndex + 1);
+ extendedInputs.put(newKey, value);
+ }
+ }
+
+ Object result = actionHandler.execute(module, extendedInputs);
+ HashMap resultMap = new HashMap();
+ resultMap.put("result", result);
+ return resultMap;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleConditionHandlerDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleConditionHandlerDelegate.java
new file mode 100644
index 00000000000..d0991a6728f
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleConditionHandlerDelegate.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal.delegates;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.handler.BaseModuleHandler;
+import org.openhab.core.automation.handler.ConditionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler;
+
+/**
+ * The SimpleConditionHandlerDelegate allows the registration of {@link SimpleConditionHandler}s to the RuleManager.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public class SimpleConditionHandlerDelegate extends BaseModuleHandler implements ConditionHandler {
+
+ private SimpleConditionHandler conditionHandler;
+
+ public SimpleConditionHandlerDelegate(Condition condition, SimpleConditionHandler scriptedHandler) {
+ super(condition);
+ this.conditionHandler = scriptedHandler;
+ scriptedHandler.init(condition);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public boolean isSatisfied(Map inputs) {
+ return conditionHandler.isSatisfied(module, inputs);
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerCallbackDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerCallbackDelegate.java
new file mode 100644
index 00000000000..721a37e9cfc
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerCallbackDelegate.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal.delegates;
+
+import java.util.Map;
+
+import org.openhab.core.automation.RuleStatus;
+import org.openhab.core.automation.RuleStatusInfo;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.handler.TriggerHandlerCallback;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandlerCallback;
+
+/**
+ * The {@link SimpleTriggerHandlerCallbackDelegate} allows a script to define callbacks for triggers in different ways.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public class SimpleTriggerHandlerCallbackDelegate implements SimpleTriggerHandlerCallback {
+ private final Trigger trigger;
+ private final TriggerHandlerCallback callback;
+
+ public SimpleTriggerHandlerCallbackDelegate(Trigger trigger, TriggerHandlerCallback callback) {
+ this.trigger = trigger;
+ this.callback = callback;
+ }
+
+ @Override
+ public void triggered(Trigger trigger, Map context) {
+ callback.triggered(trigger, context);
+ }
+
+ @Override
+ public void triggered(Map context) {
+ callback.triggered(this.trigger, context);
+ }
+
+ @Override
+ public Boolean isEnabled(String ruleUID) {
+ return callback.isEnabled(ruleUID);
+ }
+
+ @Override
+ public void setEnabled(String uid, boolean isEnabled) {
+ callback.setEnabled(uid, isEnabled);
+ }
+
+ @Override
+ public RuleStatusInfo getStatusInfo(String ruleUID) {
+ return callback.getStatusInfo(ruleUID);
+ }
+
+ @Override
+ public RuleStatus getStatus(String ruleUID) {
+ return callback.getStatus(ruleUID);
+ }
+
+ @Override
+ public void runNow(String uid) {
+ callback.runNow(uid);
+ }
+
+ @Override
+ public void runNow(String uid, boolean considerConditions, Map context) {
+ callback.runNow(uid, considerConditions, context);
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerDelegate.java
new file mode 100644
index 00000000000..6394aa5a62c
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/delegates/SimpleTriggerHandlerDelegate.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal.delegates;
+
+import org.openhab.core.automation.ModuleHandlerCallback;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.handler.BaseModuleHandler;
+import org.openhab.core.automation.handler.TriggerHandler;
+import org.openhab.core.automation.handler.TriggerHandlerCallback;
+
+/**
+ * The {@link SimpleTriggerHandlerDelegate} allows to define triggers in a script language in different ways.
+ *
+ * @author Simon Merschjohann
+ */
+public class SimpleTriggerHandlerDelegate extends BaseModuleHandler implements TriggerHandler {
+ private final org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler triggerHandler;
+
+ public SimpleTriggerHandlerDelegate(Trigger module,
+ org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler triggerHandler) {
+ super(module);
+ this.triggerHandler = triggerHandler;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void setCallback(ModuleHandlerCallback callback) {
+ triggerHandler.setRuleEngineCallback(this.module,
+ new SimpleTriggerHandlerCallbackDelegate(this.module, (TriggerHandlerCallback) callback));
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java
new file mode 100644
index 00000000000..a06d33b1e45
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java
@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.internal.loader;
+
+import static java.nio.file.StandardWatchEventKinds.*;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchEvent.Kind;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.smarthome.config.core.ConfigConstants;
+import org.eclipse.smarthome.core.service.AbstractWatchService;
+import org.openhab.core.automation.module.script.ScriptEngineContainer;
+import org.openhab.core.automation.module.script.ScriptEngineManager;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link ScriptFileWatcher} watches the jsr223 directory for files. If a new/modified file is detected, the script
+ * is read and passed to the {@link ScriptEngineManager}.
+ *
+ * @author Simon Merschjohann - initial contribution
+ * @author Kai Kreuzer - improved logging and removed thread pool
+ *
+ */
+@Component(immediate = true)
+public class ScriptFileWatcher extends AbstractWatchService {
+ private static final String FILE_DIRECTORY = "automation" + File.separator + "jsr223";
+ private static final long INITIAL_DELAY = 25;
+ private static final long RECHECK_INTERVAL = 20;
+
+ private final long earliestStart = System.currentTimeMillis() + INITIAL_DELAY * 1000;
+
+ private ScriptEngineManager manager;
+ ScheduledExecutorService scheduler;
+
+ private final Map> urlsByScriptExtension = new ConcurrentHashMap<>();
+ private final Set loaded = new HashSet<>();
+
+ public ScriptFileWatcher() {
+ super(ConfigConstants.getConfigFolder() + File.separator + FILE_DIRECTORY);
+ }
+
+ @Reference
+ public void setScriptEngineManager(ScriptEngineManager manager) {
+ this.manager = manager;
+ }
+
+ public void unsetScriptEngineManager(ScriptEngineManager manager) {
+ this.manager = null;
+ }
+
+ @Override
+ public void activate() {
+ super.activate();
+ importResources(new File(pathToWatch));
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ scheduler.scheduleWithFixedDelay(this::checkFiles, INITIAL_DELAY, RECHECK_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void deactivate() {
+ if (scheduler != null) {
+ scheduler.shutdownNow();
+ scheduler = null;
+ }
+ super.deactivate();
+ }
+
+ /**
+ * Imports resources from the specified file or directory.
+ *
+ * @param file the file or directory to import resources from
+ */
+ private void importResources(File file) {
+ if (file.exists()) {
+ File[] files = file.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ if (!f.isHidden()) {
+ importResources(f);
+ }
+ }
+ } else {
+ try {
+ URL url = file.toURI().toURL();
+ importFile(url);
+ } catch (MalformedURLException e) {
+ // can't happen for the 'file' protocol handler with a correctly formatted URI
+ logger.debug("Can't create a URL", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected boolean watchSubDirectories() {
+ return true;
+ }
+
+ @Override
+ protected Kind>[] getWatchEventKinds(Path subDir) {
+ return new Kind>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
+ }
+
+ @Override
+ protected void processWatchEvent(WatchEvent> event, Kind> kind, Path path) {
+ File file = path.toFile();
+ if (!file.isHidden()) {
+ try {
+ URL fileUrl = file.toURI().toURL();
+ if (kind.equals(ENTRY_DELETE)) {
+ this.removeFile(fileUrl);
+ }
+
+ if (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))) {
+ this.importFile(fileUrl);
+ }
+ } catch (MalformedURLException e) {
+ logger.error("malformed", e);
+ }
+ }
+ }
+
+ private void removeFile(URL url) {
+ dequeueUrl(url);
+ manager.removeEngine(getScriptIdentifier(url));
+ loaded.remove(url);
+ }
+
+ private synchronized void importFile(URL url) {
+ String fileName = getFileName(url);
+ if (loaded.contains(url)) {
+ this.removeFile(url); // if already loaded, remove first
+ }
+
+ String scriptType = getScriptType(url);
+ if (scriptType != null) {
+ if (System.currentTimeMillis() < earliestStart) {
+ enqueueUrl(url, scriptType);
+ } else {
+ if (manager.isSupported(scriptType)) {
+ try (InputStreamReader reader = new InputStreamReader(new BufferedInputStream(url.openStream()))) {
+ logger.info("Loading script '{}'", fileName);
+
+ ScriptEngineContainer container = manager.createScriptEngine(scriptType,
+ getScriptIdentifier(url));
+
+ if (container != null) {
+ manager.loadScript(container.getIdentifier(), reader);
+ loaded.add(url);
+ logger.debug("Script loaded: {}", fileName);
+ } else {
+ logger.error("Script loading error, ignoring file: {}", fileName);
+ }
+ } catch (IOException e) {
+ logger.error("Failed to load file '{}': {}", url.getFile(), e.getMessage());
+ }
+ } else {
+ enqueueUrl(url, scriptType);
+
+ logger.info("ScriptEngine for {} not available", scriptType);
+ }
+ }
+ }
+ }
+
+ private String getFileName(URL url) {
+ String fileName = url.getFile();
+ String parentPath = FILE_DIRECTORY.replace('\\', '/');
+ if (fileName.contains(parentPath)) {
+ fileName = fileName.substring(fileName.lastIndexOf(parentPath) + parentPath.length() + 1);
+ }
+ return fileName;
+ }
+
+ private void enqueueUrl(URL url, String scriptType) {
+ synchronized (urlsByScriptExtension) {
+ Set set = urlsByScriptExtension.get(scriptType);
+ if (set == null) {
+ set = new HashSet();
+ urlsByScriptExtension.put(scriptType, set);
+ }
+ set.add(url);
+ logger.debug("in queue: {}", urlsByScriptExtension);
+ }
+ }
+
+ private void dequeueUrl(URL url) {
+ String scriptType = getScriptType(url);
+
+ if (scriptType != null) {
+ synchronized (urlsByScriptExtension) {
+ Set set = urlsByScriptExtension.get(scriptType);
+ if (set != null) {
+ set.remove(url);
+ if (set.isEmpty()) {
+ urlsByScriptExtension.remove(scriptType);
+ }
+ }
+ logger.debug("in queue: {}", urlsByScriptExtension);
+ }
+ }
+ }
+
+ private String getScriptType(URL url) {
+ String fileName = url.getPath();
+ int idx = fileName.lastIndexOf(".");
+ if (idx == -1) {
+ return null;
+ }
+ String fileExtension = fileName.substring(idx + 1);
+
+ // ignore known file extensions for "temp" files
+ if (fileExtension.equals("txt") || fileExtension.endsWith("~") || fileExtension.endsWith("swp")) {
+ return null;
+ }
+ return fileExtension;
+ }
+
+ private String getScriptIdentifier(URL url) {
+ return url.toString();
+ }
+
+ private void checkFiles() {
+ SortedSet reimportUrls = new TreeSet(new Comparator() {
+ @Override
+ public int compare(URL o1, URL o2) {
+ String f1 = o1.getPath();
+ String f2 = o2.getPath();
+ return String.CASE_INSENSITIVE_ORDER.compare(f1, f2);
+ }
+ });
+
+ synchronized (urlsByScriptExtension) {
+ HashSet newlySupported = new HashSet<>();
+ for (String key : urlsByScriptExtension.keySet()) {
+ if (manager.isSupported(key)) {
+ newlySupported.add(key);
+ }
+ }
+
+ for (String key : newlySupported) {
+ reimportUrls.addAll(urlsByScriptExtension.remove(key));
+ }
+ }
+
+ for (URL url : reimportUrls) {
+ importFile(url);
+ }
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java
new file mode 100644
index 00000000000..452188c9670
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/RuleSupportRuleRegistryDelegate.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.stream.Stream;
+
+import org.eclipse.smarthome.core.common.registry.RegistryChangeListener;
+import org.openhab.core.automation.Rule;
+import org.openhab.core.automation.RuleRegistry;
+
+/**
+ * The {@link RuleSupportRuleRegistryDelegate} is wrapping a {@link RuleRegistry} to provide a comfortable way to add
+ * rules to the RuleManager without worrying about the need to remove rules again. Nonetheless, using the addPermanent
+ * method it is still possible to add rules permanently.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public class RuleSupportRuleRegistryDelegate implements RuleRegistry {
+ private final RuleRegistry ruleRegistry;
+
+ private final HashSet rules = new HashSet<>();
+
+ private final ScriptedRuleProvider ruleProvider;
+
+ public RuleSupportRuleRegistryDelegate(RuleRegistry ruleRegistry, ScriptedRuleProvider ruleProvider) {
+ this.ruleRegistry = ruleRegistry;
+ this.ruleProvider = ruleProvider;
+ }
+
+ @Override
+ public void addRegistryChangeListener(RegistryChangeListener listener) {
+ ruleRegistry.addRegistryChangeListener(listener);
+ }
+
+ @Override
+ public Collection getAll() {
+ return ruleRegistry.getAll();
+ }
+
+ @Override
+ public Stream stream() {
+ return ruleRegistry.stream();
+ }
+
+ @Override
+ public Rule get(String key) {
+ return ruleRegistry.get(key);
+ }
+
+ @Override
+ public void removeRegistryChangeListener(RegistryChangeListener listener) {
+ ruleRegistry.removeRegistryChangeListener(listener);
+ }
+
+ @Override
+ public Rule add(Rule element) {
+ ruleProvider.addRule(element);
+ rules.add(element.getUID());
+
+ return element;
+ }
+
+ /**
+ * add a rule permanently to the RuleManager
+ *
+ * @param element the rule
+ */
+ public void addPermanent(Rule element) {
+ ruleRegistry.add(element);
+ }
+
+ @Override
+ public Rule update(Rule element) {
+ return ruleRegistry.update(element);
+ }
+
+ @Override
+ public Rule remove(String key) {
+ if (rules.remove(key)) {
+ ruleProvider.removeRule(key);
+ }
+
+ return ruleRegistry.remove(key);
+ }
+
+ @Override
+ public Collection getByTag(String tag) {
+ return ruleRegistry.getByTag(tag);
+ }
+
+ /**
+ * called when the script is unloaded or reloaded
+ */
+ public void removeAllAddedByScript() {
+ for (String rule : rules) {
+ try {
+ ruleProvider.removeRule(rule);
+ } catch (Exception ex) {
+ // ignore
+ }
+ }
+ rules.clear();
+ }
+
+ @Override
+ public Collection getByTags(String... tags) {
+ return ruleRegistry.getByTags(tags);
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java
new file mode 100644
index 00000000000..976a1598e0f
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedAutomationManager.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import org.eclipse.smarthome.config.core.Configuration;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.Rule;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.module.script.rulesupport.internal.ScriptedCustomModuleHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.internal.ScriptedCustomModuleTypeProvider;
+import org.openhab.core.automation.module.script.rulesupport.internal.ScriptedPrivateModuleHandlerFactory;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRuleActionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRuleActionHandlerDelegate;
+import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler;
+import org.openhab.core.automation.type.ActionType;
+import org.openhab.core.automation.type.ConditionType;
+import org.openhab.core.automation.type.TriggerType;
+import org.openhab.core.automation.util.ActionBuilder;
+import org.openhab.core.automation.util.ModuleBuilder;
+import org.openhab.core.automation.util.RuleBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This Registry is used for a single ScriptEngine instance. It allows the adding and removing of handlers.
+ * It allows the removal of previously added modules on unload.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public class ScriptedAutomationManager {
+ private final Logger logger = LoggerFactory.getLogger(ScriptedAutomationManager.class);
+
+ private final RuleSupportRuleRegistryDelegate ruleRegistryDelegate;
+
+ private final HashSet modules = new HashSet<>();
+ private final HashSet moduleHandlers = new HashSet<>();
+ private final HashSet privateHandlers = new HashSet<>();
+
+ private final ScriptedCustomModuleHandlerFactory scriptedCustomModuleHandlerFactory;
+ private final ScriptedCustomModuleTypeProvider scriptedCustomModuleTypeProvider;
+ private final ScriptedPrivateModuleHandlerFactory scriptedPrivateModuleHandlerFactory;
+
+ public ScriptedAutomationManager(RuleSupportRuleRegistryDelegate ruleRegistryDelegate,
+ ScriptedCustomModuleHandlerFactory scriptedCustomModuleHandlerFactory,
+ ScriptedCustomModuleTypeProvider scriptedCustomModuleTypeProvider,
+ ScriptedPrivateModuleHandlerFactory scriptedPrivateModuleHandlerFactory) {
+ this.ruleRegistryDelegate = ruleRegistryDelegate;
+ this.scriptedCustomModuleHandlerFactory = scriptedCustomModuleHandlerFactory;
+ this.scriptedCustomModuleTypeProvider = scriptedCustomModuleTypeProvider;
+ this.scriptedPrivateModuleHandlerFactory = scriptedPrivateModuleHandlerFactory;
+ }
+
+ public void removeModuleType(String UID) {
+ if (modules.remove(UID)) {
+ scriptedCustomModuleTypeProvider.removeModuleType(UID);
+ removeHandler(UID);
+ }
+ }
+
+ public void removeHandler(String typeUID) {
+ if (moduleHandlers.remove(typeUID)) {
+ scriptedCustomModuleHandlerFactory.removeModuleHandler(typeUID);
+ }
+ }
+
+ public void removePrivateHandler(String privId) {
+ if (privateHandlers.remove(privId)) {
+ scriptedPrivateModuleHandlerFactory.removeHandler(privId);
+ }
+ }
+
+ public void removeAll() {
+ logger.info("removeAll added handlers");
+
+ HashSet types = new HashSet<>(modules);
+ for (String moduleType : types) {
+ removeModuleType(moduleType);
+ }
+
+ HashSet moduleHandlers = new HashSet<>(this.moduleHandlers);
+ for (String uid : moduleHandlers) {
+ removeHandler(uid);
+ }
+
+ HashSet privateHandlers = new HashSet<>(this.privateHandlers);
+ for (String privId : privateHandlers) {
+ removePrivateHandler(privId);
+ }
+
+ ruleRegistryDelegate.removeAllAddedByScript();
+ }
+
+ public Rule addRule(Rule element) {
+ RuleBuilder builder = RuleBuilder.create(element.getUID());
+
+ String name = element.getName();
+ if (name == null || name.isEmpty()) {
+ name = element.getClass().getSimpleName();
+ if (name.contains("$")) {
+ name = name.substring(0, name.indexOf('$'));
+ }
+ }
+
+ builder.withName(name).withDescription(element.getDescription()).withTags(element.getTags());
+
+ // used for numbering the modules of the rule
+ int moduleIndex = 1;
+
+ try {
+ ArrayList conditions = new ArrayList<>();
+ for (Condition cond : element.getConditions()) {
+ Condition toAdd = cond;
+ if (cond.getId().isEmpty()) {
+ toAdd = ModuleBuilder.createCondition().withId(Integer.toString(moduleIndex++))
+ .withTypeUID(cond.getTypeUID()).withConfiguration(cond.getConfiguration())
+ .withInputs(cond.getInputs()).build();
+ }
+
+ conditions.add(toAdd);
+ }
+
+ builder.withConditions(conditions);
+ } catch (Exception ex) {
+ // conditions are optional
+ }
+
+ try {
+ ArrayList triggers = new ArrayList<>();
+ for (Trigger trigger : element.getTriggers()) {
+ Trigger toAdd = trigger;
+ if (trigger.getId().isEmpty()) {
+ toAdd = ModuleBuilder.createTrigger().withId(Integer.toString(moduleIndex++))
+ .withTypeUID(trigger.getTypeUID()).withConfiguration(trigger.getConfiguration()).build();
+ }
+
+ triggers.add(toAdd);
+ }
+
+ builder.withTriggers(triggers);
+ } catch (Exception ex) {
+ // triggers are optional
+ }
+
+ ArrayList actions = new ArrayList<>();
+ actions.addAll(element.getActions());
+
+ if (element instanceof SimpleRuleActionHandler) {
+ String privId = addPrivateActionHandler(
+ new SimpleRuleActionHandlerDelegate((SimpleRuleActionHandler) element));
+
+ Action scriptedAction = ActionBuilder.create().withId(Integer.toString(moduleIndex++))
+ .withTypeUID("jsr223.ScriptedAction").withConfiguration(new Configuration()).build();
+ scriptedAction.getConfiguration().put("privId", privId);
+ actions.add(scriptedAction);
+ }
+
+ builder.withActions(actions);
+
+ Rule rule = builder.build();
+
+ ruleRegistryDelegate.add(rule);
+ return rule;
+ }
+
+ public void addConditionType(ConditionType condititonType) {
+ modules.add(condititonType.getUID());
+ scriptedCustomModuleTypeProvider.addModuleType(condititonType);
+ }
+
+ public void addConditionHandler(String uid, ScriptedHandler conditionHandler) {
+ moduleHandlers.add(uid);
+ scriptedCustomModuleHandlerFactory.addModuleHandler(uid, conditionHandler);
+ scriptedCustomModuleTypeProvider.updateModuleHandler(uid);
+ }
+
+ public String addPrivateConditionHandler(SimpleConditionHandler conditionHandler) {
+ String uid = scriptedPrivateModuleHandlerFactory.addHandler(conditionHandler);
+ privateHandlers.add(uid);
+ return uid;
+ }
+
+ public void addActionType(ActionType actionType) {
+ modules.add(actionType.getUID());
+ scriptedCustomModuleTypeProvider.addModuleType(actionType);
+ }
+
+ public void addActionHandler(String uid, ScriptedHandler actionHandler) {
+ moduleHandlers.add(uid);
+ scriptedCustomModuleHandlerFactory.addModuleHandler(uid, actionHandler);
+ scriptedCustomModuleTypeProvider.updateModuleHandler(uid);
+ }
+
+ public String addPrivateActionHandler(SimpleActionHandler actionHandler) {
+ String uid = scriptedPrivateModuleHandlerFactory.addHandler(actionHandler);
+ privateHandlers.add(uid);
+ return uid;
+ }
+
+ public void addTriggerType(TriggerType triggerType) {
+ modules.add(triggerType.getUID());
+ scriptedCustomModuleTypeProvider.addModuleType(triggerType);
+ }
+
+ public void addTriggerHandler(String uid, ScriptedHandler triggerHandler) {
+ moduleHandlers.add(uid);
+ scriptedCustomModuleHandlerFactory.addModuleHandler(uid, triggerHandler);
+ scriptedCustomModuleTypeProvider.updateModuleHandler(uid);
+ }
+
+ public String addPrivateTriggerHandler(SimpleTriggerHandler triggerHandler) {
+ String uid = scriptedPrivateModuleHandlerFactory.addHandler(triggerHandler);
+ privateHandlers.add(uid);
+ return uid;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedHandler.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedHandler.java
new file mode 100644
index 00000000000..bfed0e9495a
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedHandler.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared;
+
+/**
+ * Empty interface to identify scripted handlers
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public interface ScriptedHandler {
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedRuleProvider.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedRuleProvider.java
new file mode 100644
index 00000000000..3fad5e38c55
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/ScriptedRuleProvider.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+import org.eclipse.smarthome.core.common.registry.ProviderChangeListener;
+import org.openhab.core.automation.Rule;
+import org.openhab.core.automation.RuleProvider;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * This RuleProvider keeps Rules at added by scripts during the runtime. This ensures that Rules are not kept on reboot,
+ * but have to be added by the scripts again.
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@Component(immediate = true, service = { ScriptedRuleProvider.class, RuleProvider.class })
+public class ScriptedRuleProvider implements RuleProvider {
+ private final Collection> listeners = new ArrayList>();
+
+ HashMap rules = new HashMap<>();
+
+ @Override
+ public void addProviderChangeListener(ProviderChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public Collection getAll() {
+ return rules.values();
+ }
+
+ @Override
+ public void removeProviderChangeListener(ProviderChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ public void addRule(Rule rule) {
+ rules.put(rule.getUID(), rule);
+
+ for (ProviderChangeListener providerChangeListener : listeners) {
+ providerChangeListener.added(this, rule);
+ }
+ }
+
+ public void removeRule(String ruleUID) {
+ removeRule(rules.get(ruleUID));
+ }
+
+ public void removeRule(Rule rule) {
+ for (ProviderChangeListener providerChangeListener : listeners) {
+ providerChangeListener.removed(this, rule);
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java
new file mode 100644
index 00000000000..c4cd92f78e0
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedActionHandlerFactory.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.factories;
+
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.handler.ActionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public interface ScriptedActionHandlerFactory extends ScriptedHandler {
+ public ActionHandler get(Action action);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java
new file mode 100644
index 00000000000..b490059f00b
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedConditionHandlerFactory.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.factories;
+
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.handler.ConditionHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public interface ScriptedConditionHandlerFactory extends ScriptedHandler {
+ public ConditionHandler get(Condition module);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java
new file mode 100644
index 00000000000..2bc82d72310
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/factories/ScriptedTriggerHandlerFactory.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.factories;
+
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.handler.TriggerHandler;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public interface ScriptedTriggerHandlerFactory extends ScriptedHandler {
+ public TriggerHandler get(Trigger module);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleActionHandler.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleActionHandler.java
new file mode 100644
index 00000000000..0dd5206210f
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleActionHandler.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public abstract class SimpleActionHandler implements ScriptedHandler {
+ public void init(Action module) {
+ }
+
+ public abstract Object execute(Action module, Map inputs);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleConditionHandler.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleConditionHandler.java
new file mode 100644
index 00000000000..7d80d0f4f20
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleConditionHandler.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public abstract class SimpleConditionHandler implements ScriptedHandler {
+ public void init(Condition condition) {
+ }
+
+ public abstract boolean isSatisfied(Condition condition, Map inputs);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRule.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRule.java
new file mode 100644
index 00000000000..34f2209a24f
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRule.java
@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.core.ConfigDescriptionParameter;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.Rule;
+import org.openhab.core.automation.RuleRegistry;
+import org.openhab.core.automation.RuleStatus;
+import org.openhab.core.automation.RuleStatusDetail;
+import org.openhab.core.automation.RuleStatusInfo;
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.Visibility;
+import org.openhab.core.automation.template.RuleTemplate;
+
+/**
+ * convenience Rule class with an action handler. This allows to define Rules which have a execution block.
+ *
+ * @author Simon Merschjohann - Initial contribution
+ * @author Kai Kreuzer - made it implement Rule
+ *
+ */
+@NonNullByDefault
+public abstract class SimpleRule implements Rule, SimpleRuleActionHandler {
+
+ @NonNullByDefault({})
+ protected List triggers;
+ @NonNullByDefault({})
+ protected List conditions;
+ @NonNullByDefault({})
+ protected List actions;
+ @NonNullByDefault({})
+ protected Configuration configuration;
+ @NonNullByDefault({})
+ protected List configDescriptions;
+ @Nullable
+ protected String templateUID;
+ @NonNullByDefault({})
+ protected String uid;
+ @Nullable
+ protected String name;
+ @NonNullByDefault({})
+ protected Set tags;
+ @NonNullByDefault({})
+ protected Visibility visibility;
+ @Nullable
+ protected String description;
+
+ protected transient volatile RuleStatusInfo status = new RuleStatusInfo(RuleStatus.UNINITIALIZED,
+ RuleStatusDetail.NONE);
+
+ public SimpleRule() {
+ }
+
+ @Override
+ public String getUID() {
+ return uid;
+ }
+
+ @Override
+ @Nullable
+ public String getTemplateUID() {
+ return templateUID;
+ }
+
+ /**
+ * This method is used to specify the {@link RuleTemplate} identifier of the template that will be used to
+ * by the {@link RuleRegistry} to resolve the {@link Rule}: to validate the {@link Rule}'s configuration, as
+ * well as to create and configure the {@link Rule}'s modules.
+ */
+ public void setTemplateUID(@Nullable String templateUID) {
+ this.templateUID = templateUID;
+ }
+
+ @Override
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * This method is used to specify the {@link Rule}'s human-readable name.
+ *
+ * @param ruleName the {@link Rule}'s human-readable name, or {@code null}.
+ */
+ public void setName(@Nullable String ruleName) {
+ name = ruleName;
+ }
+
+ @Override
+ public Set getTags() {
+ return tags;
+ }
+
+ /**
+ * This method is used to specify the {@link Rule}'s assigned tags.
+ *
+ * @param ruleTags the {@link Rule}'s assigned tags.
+ */
+ public void setTags(@Nullable Set ruleTags) {
+ tags = ruleTags != null ? ruleTags : new HashSet<>();
+ }
+
+ @Override
+ @Nullable
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * This method is used to specify human-readable description of the purpose and consequences of the
+ * {@link Rule}'s execution.
+ *
+ * @param ruleDescription the {@link Rule}'s human-readable description, or {@code null}.
+ */
+ public void setDescription(@Nullable String ruleDescription) {
+ description = ruleDescription;
+ }
+
+ @Override
+ public Visibility getVisibility() {
+ return visibility;
+ }
+
+ /**
+ * This method is used to specify the {@link Rule}'s {@link Visibility}.
+ *
+ * @param visibility the {@link Rule}'s {@link Visibility} value.
+ */
+ public void setVisibility(@Nullable Visibility visibility) {
+ this.visibility = visibility == null ? Visibility.VISIBLE : visibility;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * This method is used to specify the {@link Rule}'s {@link Configuration}.
+ *
+ * @param ruleConfiguration the new configuration values.
+ */
+ public void setConfiguration(@Nullable Configuration ruleConfiguration) {
+ this.configuration = ruleConfiguration == null ? new Configuration() : ruleConfiguration;
+ }
+
+ @Override
+ public List getConfigurationDescriptions() {
+ return configDescriptions;
+ }
+
+ /**
+ * This method is used to describe with {@link ConfigDescriptionParameter}s
+ * the meta info for configuration properties of the {@link Rule}.
+ */
+ public void setConfigurationDescriptions(@Nullable List configDescriptions) {
+ this.configDescriptions = configDescriptions == null ? new ArrayList<>() : configDescriptions;
+ }
+
+ @Override
+ public List getConditions() {
+ return conditions == null ? Collections.emptyList() : conditions;
+ }
+
+ /**
+ * This method is used to specify the conditions participating in {@link Rule}.
+ *
+ * @param conditions a list with the conditions that should belong to this {@link Rule}.
+ */
+ public void setConditions(@Nullable List conditions) {
+ this.conditions = conditions;
+ }
+
+ @Override
+ public List getActions() {
+ return actions == null ? Collections.emptyList() : actions;
+ }
+
+ @Override
+ public List getTriggers() {
+ return triggers == null ? Collections.emptyList() : triggers;
+ }
+
+ /**
+ * This method is used to specify the actions participating in {@link Rule}
+ *
+ * @param actions a list with the actions that should belong to this {@link Rule}.
+ */
+ public void setActions(@Nullable List actions) {
+ this.actions = actions;
+ }
+
+ /**
+ * This method is used to specify the triggers participating in {@link Rule}
+ *
+ * @param triggers a list with the triggers that should belong to this {@link Rule}.
+ */
+ public void setTriggers(@Nullable List triggers) {
+ this.triggers = triggers;
+ }
+
+ @Override
+ public List getModules() {
+ final List result;
+ List modules = new ArrayList();
+ modules.addAll(triggers);
+ modules.addAll(conditions);
+ modules.addAll(actions);
+ result = Collections.unmodifiableList(modules);
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ public List getModules(@Nullable Class moduleClazz) {
+ final List result;
+ if (Module.class == moduleClazz) {
+ result = (List) getModules();
+ } else if (Trigger.class == moduleClazz) {
+ result = (List) triggers;
+ } else if (Condition.class == moduleClazz) {
+ result = (List) conditions;
+ } else if (Action.class == moduleClazz) {
+ result = (List) actions;
+ } else {
+ result = Collections.emptyList();
+ }
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + uid.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof Rule)) {
+ return false;
+ }
+ Rule other = (Rule) obj;
+ if (!uid.equals(other.getUID())) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandler.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandler.java
new file mode 100644
index 00000000000..8815604a7ae
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandler.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Action;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public interface SimpleRuleActionHandler {
+ Object execute(Action module, Map inputs);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandlerDelegate.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandlerDelegate.java
new file mode 100644
index 00000000000..adabe028320
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleRuleActionHandlerDelegate.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Action;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public class SimpleRuleActionHandlerDelegate extends SimpleActionHandler {
+
+ private SimpleRuleActionHandler handler;
+
+ public SimpleRuleActionHandlerDelegate(SimpleRuleActionHandler handler) {
+ super();
+ this.handler = handler;
+ }
+
+ @Override
+ public Object execute(Action module, Map inputs) {
+ return handler.execute(module, inputs);
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandler.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandler.java
new file mode 100644
index 00000000000..ef5ee77f01d
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandler.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.Trigger;
+import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public abstract class SimpleTriggerHandler implements ScriptedHandler {
+ private SimpleTriggerHandlerCallback ruleCallback;
+
+ public void init(Trigger module) {
+ }
+
+ public void setRuleEngineCallback(Trigger module, SimpleTriggerHandlerCallback ruleCallback) {
+ this.ruleCallback = ruleCallback;
+ }
+
+ protected void trigger(Map context) {
+ this.ruleCallback.triggered(context);
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java
new file mode 100644
index 00000000000..7fe6470ee4d
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/shared/simple/SimpleTriggerHandlerCallback.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.rulesupport.shared.simple;
+
+import java.util.Map;
+
+import org.openhab.core.automation.handler.TriggerHandlerCallback;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public interface SimpleTriggerHandlerCallback extends TriggerHandlerCallback {
+ public void triggered(Map context);
+}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/ESH-INF/automation/moduletypes/PrivateScriptedTypes.json b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/ESH-INF/automation/moduletypes/PrivateScriptedTypes.json
new file mode 100644
index 00000000000..b799825ab36
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/resources/ESH-INF/automation/moduletypes/PrivateScriptedTypes.json
@@ -0,0 +1,61 @@
+{
+ "conditions":[
+ {
+ "uid":"jsr223.ScriptedCondition",
+ "label":"Scripted condition",
+ "description":"allows the definition of a condition by a script",
+ "visibility": "EXPERT",
+ "configDescriptions":[
+ {
+ "name":"privId",
+ "type":"TEXT",
+ "description":"the identifier of the private method",
+ "required":true
+ }
+ ]
+ }
+ ],
+ "actions":[
+ {
+ "uid":"jsr223.ScriptedAction",
+ "label":"Scripted action",
+ "description":"allows the execution of a method defined by a script",
+ "visibility": "EXPERT",
+ "configDescriptions":[
+ {
+ "name":"privId",
+ "type":"TEXT",
+ "description":"the identifier of the private method",
+ "required":true
+ }
+ ],
+ "outputs":[
+ {
+ "name":"result",
+ "type":"java.lang.Object",
+ "label":"result",
+ "description":"the script result.",
+ "reference":""
+ }
+ ]
+ }
+ ],
+ "triggers": [
+ {
+ "uid":"jsr223.ScriptedTrigger",
+ "label":"Scripted trigger",
+ "description":"allows the execution of a method defined by a script",
+ "visibility": "EXPERT",
+ "outputs":[
+ {
+ "name":"triggerOutput",
+ "type":"java.lang.String",
+ "label":"TriggerOutput label",
+ "description":"Text from the trigger",
+ "reference":"consoleInput",
+ "defaultValue":"dtag"
+ }
+ ]
+ }
+ ]
+}
diff --git a/bundles/org.openhab.core.automation.module.script/.classpath b/bundles/org.openhab.core.automation.module.script/.classpath
new file mode 100644
index 00000000000..372f1810d64
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.script/.project b/bundles/org.openhab.core.automation.module.script/.project
new file mode 100644
index 00000000000..8d22141ac91
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.automation.module.script
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..00777dc9bed
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/=UTF-8
+encoding/ESH-INF=UTF-8
+encoding/src=UTF-8
diff --git a/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b8947ec6fc0
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000000..f897a7f1cb2
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/bundles/org.openhab.core.automation.module.script/NOTICE b/bundles/org.openhab.core.automation.module.script/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/org.openhab.core.automation.module.script/pom.xml b/bundles/org.openhab.core.automation.module.script/pom.xml
new file mode 100644
index 00000000000..81a92cf7a23
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 2.5.0-SNAPSHOT
+
+
+ org.openhab.core.automation.module.script
+
+ openHAB Core :: Bundles :: Automation Script Modules
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.automation
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineContainer.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineContainer.java
new file mode 100644
index 00000000000..ae51a6e7ad9
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineContainer.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script;
+
+import javax.script.ScriptEngine;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+@NonNullByDefault
+public class ScriptEngineContainer {
+ private ScriptEngine scriptEngine;
+ private ScriptEngineFactory factory;
+ private String identifier;
+
+ public ScriptEngineContainer(ScriptEngine scriptEngine, ScriptEngineFactory factory, String identifier) {
+ super();
+ this.scriptEngine = scriptEngine;
+ this.factory = factory;
+ this.identifier = identifier;
+ }
+
+ public ScriptEngine getScriptEngine() {
+ return scriptEngine;
+ }
+
+ public ScriptEngineFactory getFactory() {
+ return factory;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java
new file mode 100644
index 00000000000..5f30aba9300
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.script.ScriptEngine;
+
+/**
+ * This class is used by the ScriptManager to load ScriptEngines.
+ * This is meant as a way to allow other OSGi bundles to provide custom Script-Languages with special needs (like
+ * Nashorn, Groovy, etc.)
+ *
+ * @author Simon Merschjohann
+ *
+ */
+public interface ScriptEngineFactory {
+
+ /**
+ * @return the list of supported language endings e.g. py, jy
+ */
+ List getLanguages();
+
+ /**
+ * "scopes" new values into the given ScriptEngine
+ *
+ * @param scriptEngine
+ * @param scopeValues
+ */
+ void scopeValues(ScriptEngine scriptEngine, Map scopeValues);
+
+ /**
+ * created a new ScriptEngine
+ *
+ * @param fileExtension
+ * @return
+ */
+ ScriptEngine createScriptEngine(String fileExtension);
+
+ /**
+ * checks if the script is supported. Does not necessarily be equal to getLanguages()
+ *
+ * @param fileExtension
+ * @return
+ */
+ boolean isSupported(String fileExtension);
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java
new file mode 100644
index 00000000000..078d421046c
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script;
+
+import java.io.InputStreamReader;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+@NonNullByDefault
+public interface ScriptEngineManager {
+
+ /**
+ * Checks if a given fileExtension is supported
+ *
+ * @param fileExtension
+ * @return true if supported
+ */
+ boolean isSupported(String fileExtension);
+
+ /**
+ * Creates a new ScriptEngine based on the given fileExtension
+ *
+ * @param fileExtension
+ * @param scriptIdentifier
+ * @return
+ */
+ @Nullable
+ ScriptEngineContainer createScriptEngine(String fileExtension, String scriptIdentifier);
+
+ /**
+ * Loads a script and initializes its scope variables
+ *
+ * @param fileExtension
+ * @param scriptIdentifier
+ * @param scriptData
+ * @return
+ */
+ void loadScript(String scriptIdentifier, InputStreamReader scriptData);
+
+ /**
+ * Unloads the ScriptEngine loaded with the scriptIdentifer
+ *
+ * @param scriptIdentifier
+ */
+ void removeEngine(String scriptIdentifier);
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptExtensionProvider.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptExtensionProvider.java
new file mode 100644
index 00000000000..53870f5dfbf
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptExtensionProvider.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * A {@link ScriptExtensionProvider} can provide variable and types on ScriptEngine instance basis.
+ *
+ * @author Simon Merschjohann- Initial contribution
+ *
+ */
+public interface ScriptExtensionProvider {
+
+ /**
+ * These presets will always get injected into the ScriptEngine on instance creation.
+ *
+ * @return collection of presets
+ */
+ public Collection getDefaultPresets();
+
+ /**
+ * Returns the provided Presets which are supported by this ScriptExtensionProvider.
+ * Presets define imports which will be injected into the ScriptEngine if called by "importPreset".
+ *
+ * @return provided presets
+ */
+ public Collection getPresets();
+
+ /**
+ * Returns the supported types which can be received by the given ScriptExtensionProvider
+ *
+ * @return provided types
+ */
+ public Collection getTypes();
+
+ /**
+ * This method should return an Object of the given type. Note: get can be called multiple times in the scripts use
+ * caching where appropriate.
+ *
+ * @param scriptIdentifier the identifier of the script that requests the given type
+ * @param type the type that is requested (must be part of the collection returned by the {@code #getTypes()} method
+ * @return the requested type (non-null)
+ * @throws IllegalArgumentException if the given type does not match to one returned by the {@code #getTypes()}
+ * method
+ */
+ public Object get(String scriptIdentifier, String type) throws IllegalArgumentException;
+
+ /**
+ * This method should return variables and types of the concrete type which will be injected into the ScriptEngines
+ * scope.
+ *
+ * @param scriptIdentifier the identifier of the script that receives the preset
+ * @return the presets, must be non-null (use an empty map instead)
+ */
+ public Map importPreset(String scriptIdentifier, String preset);
+
+ /**
+ * This will be called when the ScriptEngine will be unloaded (e.g. if the Script is deleted or updated).
+ * Every Context information stored in the ScriptExtensionProvider should be removed.
+ *
+ * @param scriptIdentifier the identifier of the script that is unloaded
+ */
+ public void unload(String scriptIdentifier);
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java
new file mode 100644
index 00000000000..84091392d85
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/GenericScriptEngineFactory.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+public class GenericScriptEngineFactory implements ScriptEngineFactory {
+ private ScriptEngineManager engineManager = new ScriptEngineManager();
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public GenericScriptEngineFactory() {
+ for (javax.script.ScriptEngineFactory f : engineManager.getEngineFactories()) {
+ logger.info("Activated scripting support for {}", f.getLanguageName());
+ logger.debug(
+ "Activated scripting support with engine {}({}) for {}({}) with mimetypes {} and file extensions {}",
+ f.getEngineName(), f.getEngineVersion(), f.getLanguageName(), f.getLanguageVersion(),
+ f.getMimeTypes(), f.getExtensions());
+ }
+ }
+
+ @Override
+ public List getLanguages() {
+ ArrayList languages = new ArrayList<>();
+
+ for (javax.script.ScriptEngineFactory f : engineManager.getEngineFactories()) {
+ languages.addAll(f.getExtensions());
+ }
+
+ return languages;
+ }
+
+ @Override
+ public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) {
+ for (Entry entry : scopeValues.entrySet()) {
+ scriptEngine.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public ScriptEngine createScriptEngine(String fileExtension) {
+ ScriptEngine engine = engineManager.getEngineByExtension(fileExtension);
+
+ if (engine == null) {
+ engine = engineManager.getEngineByName(fileExtension);
+ }
+
+ if (engine == null) {
+ engine = engineManager.getEngineByMimeType(fileExtension);
+ }
+
+ return engine;
+ }
+
+ @Override
+ public boolean isSupported(String fileExtension) {
+ for (javax.script.ScriptEngineFactory f : engineManager.getEngineFactories()) {
+ if (f.getExtensions().contains(fileExtension)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/NashornScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/NashornScriptEngineFactory.java
new file mode 100644
index 00000000000..735fb5668c2
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/NashornScriptEngineFactory.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+@Component(service = ScriptEngineFactory.class)
+public class NashornScriptEngineFactory implements ScriptEngineFactory {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private ScriptEngineManager engineManager = new ScriptEngineManager();
+
+ @Override
+ public List getLanguages() {
+ return Arrays.asList("js", "javascript", "application/javascript");
+ }
+
+ @Override
+ public void scopeValues(ScriptEngine engine, Map scopeValues) {
+ Set expressions = new HashSet();
+
+ for (Entry entry : scopeValues.entrySet()) {
+ engine.put(entry.getKey(), entry.getValue());
+
+ if (entry.getValue() instanceof Class) {
+ expressions.add(String.format("%s = %s.static;", entry.getKey(), entry.getKey()));
+ }
+ }
+ String scriptToEval = String.join("\n", expressions);
+ try {
+ engine.eval(scriptToEval);
+ } catch (ScriptException e) {
+ logger.error("ScriptException while importing scope: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ public ScriptEngine createScriptEngine(String fileExtension) {
+ ScriptEngine engine = engineManager.getEngineByExtension(fileExtension);
+
+ if (engine == null) {
+ engine = engineManager.getEngineByName(fileExtension);
+ }
+
+ if (engine == null) {
+ engine = engineManager.getEngineByMimeType(fileExtension);
+ }
+
+ return engine;
+ }
+
+ @Override
+ public boolean isSupported(String fileExtension) {
+ return getLanguages().contains(fileExtension);
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
new file mode 100644
index 00000000000..aac76212385
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal;
+
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.automation.module.script.ScriptEngineContainer;
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.automation.module.script.ScriptEngineManager;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The ScriptManager allows to load and unloading of script files using a script engines script type
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@NonNullByDefault
+@Component(service = ScriptEngineManager.class)
+public class ScriptEngineManagerImpl implements ScriptEngineManager {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private Set scriptEngineFactories = new HashSet<>();
+ private HashMap loadedScriptEngineInstances = new HashMap<>();
+ private HashMap supportedLanguages = new HashMap<>();
+ private GenericScriptEngineFactory genericScriptEngineFactory = new GenericScriptEngineFactory();
+
+ private @NonNullByDefault({}) ScriptExtensionManager scriptExtensionManager;
+
+ @Reference
+ public void setScriptExtensionManager(ScriptExtensionManager scriptExtensionManager) {
+ this.scriptExtensionManager = scriptExtensionManager;
+ }
+
+ public void unsetScriptExtensionManager(ScriptExtensionManager scriptExtensionManager) {
+ this.scriptExtensionManager = null;
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void addScriptEngineFactory(ScriptEngineFactory provider) {
+ this.scriptEngineFactories.add(provider);
+
+ for (String language : provider.getLanguages()) {
+ this.supportedLanguages.put(language, provider);
+ }
+ }
+
+ public void removeScriptEngineFactory(ScriptEngineFactory provider) {
+ this.scriptEngineFactories.remove(provider);
+
+ for (String language : provider.getLanguages()) {
+ this.supportedLanguages.remove(language, provider);
+ }
+ }
+
+ @Override
+ public boolean isSupported(String fileExtension) {
+ return findEngineFactory(fileExtension) != null;
+ }
+
+ @Override
+ public @Nullable ScriptEngineContainer createScriptEngine(String fileExtension, String scriptIdentifier) {
+ ScriptEngineContainer result = null;
+ ScriptEngineFactory engineProvider = findEngineFactory(fileExtension);
+
+ if (engineProvider == null) {
+ logger.error("loadScript(): scriptengine for language '{}' could not be found for identifier: {}",
+ fileExtension, scriptIdentifier);
+ } else {
+ try {
+ ScriptEngine engine = engineProvider.createScriptEngine(fileExtension);
+ HashMap scriptExManager = new HashMap<>();
+ result = new ScriptEngineContainer(engine, engineProvider, scriptIdentifier);
+ ScriptExtensionManagerWrapper wrapper = new ScriptExtensionManagerWrapper(scriptExtensionManager,
+ result);
+ scriptExManager.put("scriptExtension", wrapper);
+ scriptExManager.put("se", wrapper);
+ engineProvider.scopeValues(engine, scriptExManager);
+ scriptExtensionManager.importDefaultPresets(engineProvider, engine, scriptIdentifier);
+
+ loadedScriptEngineInstances.put(scriptIdentifier, result);
+ } catch (Exception ex) {
+ logger.error("Error while creating ScriptEngine", ex);
+ removeScriptExtensions(scriptIdentifier);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void loadScript(String scriptIdentifier, InputStreamReader scriptData) {
+ ScriptEngineContainer container = loadedScriptEngineInstances.get(scriptIdentifier);
+
+ if (container == null) {
+ logger.error("could not load script as no engine is created");
+ } else {
+ ScriptEngine engine = container.getScriptEngine();
+ try {
+ engine.eval(scriptData);
+
+ if (engine instanceof Invocable) {
+ Invocable inv = (Invocable) engine;
+ try {
+ inv.invokeFunction("scriptLoaded", scriptIdentifier);
+ } catch (NoSuchMethodException e) {
+ logger.trace("scriptLoaded() not defined in script: {}", scriptIdentifier);
+ }
+ } else {
+ logger.trace("engine does not support Invocable interface");
+ }
+ } catch (Exception ex) {
+ logger.error("Error during evaluation of script '{}': {}", scriptIdentifier, ex.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void removeEngine(String scriptIdentifier) {
+ ScriptEngineContainer container = loadedScriptEngineInstances.get(scriptIdentifier);
+
+ if (container != null) {
+ if (container.getScriptEngine() instanceof Invocable) {
+ Invocable inv = (Invocable) container.getScriptEngine();
+ try {
+ inv.invokeFunction("scriptUnloaded");
+ } catch (NoSuchMethodException e) {
+ logger.trace("scriptUnloaded() not defined in script");
+ } catch (ScriptException e) {
+ logger.error("Error while executing script", e);
+ }
+ } else {
+ logger.trace("engine does not support Invocable interface");
+ }
+
+ removeScriptExtensions(scriptIdentifier);
+ }
+ }
+
+ private void removeScriptExtensions(String pathIdentifier) {
+ try {
+ scriptExtensionManager.dispose(pathIdentifier);
+ } catch (Exception ex) {
+ logger.error("error removing engine", ex);
+ }
+ }
+
+ private @Nullable ScriptEngineFactory findEngineFactory(String fileExtension) {
+ ScriptEngineFactory engineProvider = supportedLanguages.get(fileExtension);
+
+ if (engineProvider != null) {
+ return engineProvider;
+ }
+
+ for (ScriptEngineFactory provider : supportedLanguages.values()) {
+ if (provider != null && provider.isSupported(fileExtension)) {
+ return provider;
+ }
+ }
+
+ if (genericScriptEngineFactory.isSupported(fileExtension)) {
+ return genericScriptEngineFactory;
+ }
+
+ return null;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManager.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManager.java
new file mode 100644
index 00000000000..186b72d3bd1
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManager.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.script.ScriptEngine;
+
+import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+
+/**
+ * This manager allows a script import extension providers
+ *
+ * @author Simon Merschjohann
+ *
+ */
+@Component(service = ScriptExtensionManager.class)
+public class ScriptExtensionManager {
+ private Set scriptExtensionProviders = new CopyOnWriteArraySet();
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ public void addScriptExtensionProvider(ScriptExtensionProvider provider) {
+ scriptExtensionProviders.add(provider);
+ }
+
+ public void removeScriptExtensionProvider(ScriptExtensionProvider provider) {
+ scriptExtensionProviders.remove(provider);
+ }
+
+ public void addExtension(ScriptExtensionProvider provider) {
+ scriptExtensionProviders.add(provider);
+ }
+
+ public void removeExtension(ScriptExtensionProvider provider) {
+ scriptExtensionProviders.remove(provider);
+ }
+
+ public List getTypes() {
+ ArrayList types = new ArrayList<>();
+
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ types.addAll(provider.getTypes());
+ }
+
+ return types;
+ }
+
+ public List getPresets() {
+ ArrayList presets = new ArrayList<>();
+
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ presets.addAll(provider.getPresets());
+ }
+
+ return presets;
+ }
+
+ public Object get(String type, String scriptIdentifier) {
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ if (provider.getTypes().contains(type)) {
+ return provider.get(scriptIdentifier, type);
+ }
+ }
+
+ return null;
+ }
+
+ public List getDefaultPresets() {
+ ArrayList defaultPresets = new ArrayList<>();
+
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ defaultPresets.addAll(provider.getDefaultPresets());
+ }
+
+ return defaultPresets;
+ }
+
+ public void importDefaultPresets(ScriptEngineFactory engineProvider, ScriptEngine scriptEngine,
+ String scriptIdentifier) {
+ for (String preset : getDefaultPresets()) {
+ importPreset(preset, engineProvider, scriptEngine, scriptIdentifier);
+ }
+ }
+
+ public void importPreset(String preset, ScriptEngineFactory engineProvider, ScriptEngine scriptEngine,
+ String scriptIdentifier) {
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ if (provider.getPresets().contains(preset)) {
+ Map scopeValues = provider.importPreset(scriptIdentifier, preset);
+
+ engineProvider.scopeValues(scriptEngine, scopeValues);
+ }
+ }
+ }
+
+ public void dispose(String scriptIdentifier) {
+ for (ScriptExtensionProvider provider : scriptExtensionProviders) {
+ provider.unload(scriptIdentifier);
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManagerWrapper.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManagerWrapper.java
new file mode 100644
index 00000000000..b8c7889954a
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptExtensionManagerWrapper.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.automation.module.script.ScriptEngineContainer;
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+
+/**
+ *
+ * @author Simon Merschjohann - Initial contribution
+ */
+@NonNullByDefault
+public class ScriptExtensionManagerWrapper {
+ private ScriptEngineContainer container;
+ private ScriptExtensionManager manager;
+
+ public ScriptExtensionManagerWrapper(ScriptExtensionManager manager, ScriptEngineContainer container) {
+ this.manager = manager;
+ this.container = container;
+ }
+
+ public void addScriptExtensionProvider(ScriptExtensionProvider provider) {
+ manager.addExtension(provider);
+ }
+
+ public void removeScriptExtensionProvider(ScriptExtensionProvider provider) {
+ manager.removeExtension(provider);
+ }
+
+ public List getTypes() {
+ return manager.getTypes();
+ }
+
+ public List getPresets() {
+ return manager.getPresets();
+ }
+
+ public Object get(String type) {
+ return manager.get(type, container.getIdentifier());
+ }
+
+ public List getDefaultPresets() {
+ return manager.getDefaultPresets();
+ }
+
+ public void importPreset(String preset) {
+ manager.importPreset(preset, container.getFactory(), container.getScriptEngine(), container.getIdentifier());
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/DefaultScriptScopeProvider.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/DefaultScriptScopeProvider.java
new file mode 100644
index 00000000000..316119f5397
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/DefaultScriptScopeProvider.java
@@ -0,0 +1,265 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal.defaultscope;
+
+import java.io.File;
+import java.net.URLEncoder;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.eclipse.smarthome.core.events.EventPublisher;
+import org.eclipse.smarthome.core.items.ItemRegistry;
+import org.eclipse.smarthome.core.library.types.DateTimeType;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.HSBType;
+import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
+import org.eclipse.smarthome.core.library.types.NextPreviousType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.OpenClosedType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.PlayPauseType;
+import org.eclipse.smarthome.core.library.types.PointType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.types.RawType;
+import org.eclipse.smarthome.core.library.types.RewindFastforwardType;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.StringListType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.eclipse.smarthome.core.library.unit.ImperialUnits;
+import org.eclipse.smarthome.core.library.unit.MetricPrefix;
+import org.eclipse.smarthome.core.library.unit.SIUnits;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.core.thing.ThingRegistry;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.UnDefType;
+import org.openhab.core.automation.RuleRegistry;
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+
+/**
+ * This is a default scope provider for stuff that is of general interest in an ESH-based solution.
+ * Nonetheless, solutions are free to remove it and have more specific scope providers for their own purposes.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ * @author Simon Merschjohann - refactored to be an ScriptExtensionProvider
+ *
+ */
+@Component(immediate = true)
+public class DefaultScriptScopeProvider implements ScriptExtensionProvider {
+
+ private final Queue queuedBeforeActivation = new LinkedList<>();
+
+ private Map elements;
+
+ private ItemRegistry itemRegistry;
+
+ private ThingRegistry thingRegistry;
+
+ private EventPublisher eventPublisher;
+
+ private ScriptBusEvent busEvent;
+
+ private ScriptThingActions thingActions;
+
+ private RuleRegistry ruleRegistry;
+
+ @Reference
+ protected void setRuleRegistry(RuleRegistry ruleRegistry) {
+ this.ruleRegistry = ruleRegistry;
+ }
+
+ protected void unsetRuleRegistry(RuleRegistry ruleRegistry) {
+ this.ruleRegistry = null;
+ }
+
+ @Reference
+ protected void setThingRegistry(ThingRegistry thingRegistry) {
+ this.thingRegistry = thingRegistry;
+ }
+
+ protected void unsetThingRegistry(ThingRegistry thingRegistry) {
+ this.thingRegistry = null;
+ }
+
+ @Reference
+ protected void setItemRegistry(ItemRegistry itemRegistry) {
+ this.itemRegistry = itemRegistry;
+ }
+
+ protected void unsetItemRegistry(ItemRegistry itemRegistry) {
+ this.itemRegistry = null;
+ }
+
+ @Reference
+ protected void setEventPublisher(EventPublisher eventPublisher) {
+ this.eventPublisher = eventPublisher;
+ }
+
+ protected void unsetEventPublisher(EventPublisher eventPublisher) {
+ this.eventPublisher = null;
+ }
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
+ synchronized void addThingActions(ThingActions thingActions) {
+ if (this.thingActions == null) { // bundle may not be active yet
+ queuedBeforeActivation.add(thingActions);
+ } else {
+ this.thingActions.addThingActions(thingActions);
+ elements.put(thingActions.getClass().getSimpleName(), thingActions.getClass());
+ }
+ }
+
+ protected void removeThingActions(ThingActions thingActions) {
+ elements.remove(thingActions.getClass().getSimpleName());
+ this.thingActions.removeThingActions(thingActions);
+ }
+
+ @Activate
+ protected synchronized void activate() {
+ busEvent = new ScriptBusEvent(itemRegistry, eventPublisher);
+ thingActions = new ScriptThingActions(thingRegistry);
+
+ elements = new HashMap<>();
+ elements.put("State", State.class);
+ elements.put("Command", Command.class);
+ elements.put("StringUtils", StringUtils.class);
+ elements.put("URLEncoder", URLEncoder.class);
+ elements.put("FileUtils", FileUtils.class);
+ elements.put("FilenameUtils", FilenameUtils.class);
+ elements.put("File", File.class);
+
+ // ESH types
+ elements.put("IncreaseDecreaseType", IncreaseDecreaseType.class);
+ elements.put("DECREASE", IncreaseDecreaseType.DECREASE);
+ elements.put("INCREASE", IncreaseDecreaseType.INCREASE);
+
+ elements.put("OnOffType", OnOffType.class);
+ elements.put("ON", OnOffType.ON);
+ elements.put("OFF", OnOffType.OFF);
+
+ elements.put("OpenClosedType", OpenClosedType.class);
+ elements.put("CLOSED", OpenClosedType.CLOSED);
+ elements.put("OPEN", OpenClosedType.OPEN);
+
+ elements.put("StopMoveType", StopMoveType.class);
+ elements.put("MOVE", StopMoveType.MOVE);
+ elements.put("STOP", StopMoveType.STOP);
+
+ elements.put("UpDownType", UpDownType.class);
+ elements.put("DOWN", UpDownType.DOWN);
+ elements.put("UP", UpDownType.UP);
+
+ elements.put("UnDefType", UnDefType.class);
+ elements.put("NULL", UnDefType.NULL);
+ elements.put("UNDEF", UnDefType.UNDEF);
+
+ elements.put("NextPreviousType", NextPreviousType.class);
+ elements.put("NEXT", NextPreviousType.NEXT);
+ elements.put("PREVIOUS", NextPreviousType.PREVIOUS);
+
+ elements.put("PlayPauseType", PlayPauseType.class);
+ elements.put("PLAY", PlayPauseType.PLAY);
+ elements.put("PAUSE", PlayPauseType.PAUSE);
+
+ elements.put("RewindFastforwardType", RewindFastforwardType.class);
+ elements.put("REWIND", RewindFastforwardType.REWIND);
+ elements.put("FASTFORWARD", RewindFastforwardType.FASTFORWARD);
+
+ elements.put("QuantityType", QuantityType.class);
+ elements.put("StringListType", StringListType.class);
+ elements.put("RawType", RawType.class);
+ elements.put("DateTimeType", DateTimeType.class);
+ elements.put("DecimalType", DecimalType.class);
+ elements.put("HSBType", HSBType.class);
+ elements.put("PercentType", PercentType.class);
+ elements.put("PointType", PointType.class);
+ elements.put("StringType", StringType.class);
+
+ elements.put("SIUnits", SIUnits.class);
+ elements.put("ImperialUnits", ImperialUnits.class);
+ elements.put("MetricPrefix", MetricPrefix.class);
+ elements.put("SmartHomeUnits", SmartHomeUnits.class);
+
+ // services
+ elements.put("items", new ItemRegistryDelegate(itemRegistry));
+ elements.put("ir", itemRegistry);
+ elements.put("itemRegistry", itemRegistry);
+ elements.put("things", thingRegistry);
+ elements.put("events", busEvent);
+ elements.put("rules", ruleRegistry);
+ elements.put("actions", thingActions);
+
+ // if any thingActions were queued before this got activated, add them now
+ queuedBeforeActivation.forEach(thingActions -> this.addThingActions(thingActions));
+ queuedBeforeActivation.clear();
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ busEvent.dispose();
+ busEvent = null;
+ thingActions.dispose();
+ thingActions = null;
+ elements = null;
+ }
+
+ @Override
+ public Collection getDefaultPresets() {
+ return Collections.singleton("default");
+ }
+
+ @Override
+ public Collection getPresets() {
+ return Collections.singleton("default");
+ }
+
+ @Override
+ public Collection getTypes() {
+ return elements.keySet();
+ }
+
+ @Override
+ public Object get(String scriptIdentifier, String type) {
+ return elements.get(type);
+ }
+
+ @Override
+ public Map importPreset(String scriptIdentifier, String preset) {
+ if (preset.equals("default")) {
+ return elements;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void unload(String scriptIdentifier) {
+ // nothing todo
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java
new file mode 100644
index 00000000000..51de958e6f1
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ItemRegistryDelegate.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal.defaultscope;
+
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.items.Item;
+import org.eclipse.smarthome.core.items.ItemNotFoundException;
+import org.eclipse.smarthome.core.items.ItemRegistry;
+import org.eclipse.smarthome.core.types.State;
+
+/**
+ * This is a helper class that can be added to script scopes. It provides easy access to the current item states.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ *
+ */
+public class ItemRegistryDelegate implements Map {
+
+ private final ItemRegistry itemRegistry;
+
+ public ItemRegistryDelegate(ItemRegistry itemRegistry) {
+ this.itemRegistry = itemRegistry;
+ }
+
+ @Override
+ public int size() {
+ return itemRegistry.getAll().size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ if (key instanceof String) {
+ try {
+ return itemRegistry.getItem((String) key) != null;
+ } catch (ItemNotFoundException e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return false;
+ }
+
+ @Override
+ public State get(Object key) {
+ final Item item = itemRegistry.get((String) key);
+ if (item == null) {
+ return null;
+ }
+ return item.getState();
+ }
+
+ @Override
+ public State put(String key, State value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public State remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(Map extends String, ? extends State> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set keySet() {
+ Set keys = new HashSet<>();
+ for (Item item : itemRegistry.getAll()) {
+ keys.add(item.getName());
+ }
+ return keys;
+ }
+
+ @Override
+ public Collection values() {
+ Set values = new HashSet<>();
+ for (Item item : itemRegistry.getAll()) {
+ values.add(item.getState());
+ }
+ return values;
+ }
+
+ @Override
+ public Set> entrySet() {
+ Set> entries = new HashSet>();
+ for (Item item : itemRegistry.getAll()) {
+ entries.add(new AbstractMap.SimpleEntry<>(item.getName(), item.getState()));
+ }
+ return entries;
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEvent.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEvent.java
new file mode 100644
index 00000000000..921a5379eba
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptBusEvent.java
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal.defaultscope;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.smarthome.core.events.EventPublisher;
+import org.eclipse.smarthome.core.items.GroupItem;
+import org.eclipse.smarthome.core.items.Item;
+import org.eclipse.smarthome.core.items.ItemNotFoundException;
+import org.eclipse.smarthome.core.items.ItemRegistry;
+import org.eclipse.smarthome.core.items.events.ItemEventFactory;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.TypeParser;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The static methods of this class are made available as functions in the scripts.
+ * This gives direct write access to the event bus from within scripts.
+ * Items should not be updated directly (setting the state property), but updates should
+ * be sent to the bus, so that all interested bundles are notified.
+ *
+ * Note: This class is a copy from the {@link BusEvent} class, which resides in the model.script bundle.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class ScriptBusEvent {
+
+ ScriptBusEvent(ItemRegistry itemRegistry, EventPublisher eventPublisher) {
+ this.itemRegistry = itemRegistry;
+ this.eventPublisher = eventPublisher;
+ }
+
+ private ItemRegistry itemRegistry;
+ private EventPublisher eventPublisher;
+
+ public void dispose() {
+ this.itemRegistry = null;
+ this.eventPublisher = null;
+ }
+
+ /**
+ * Sends a command for a specified item to the event bus.
+ *
+ * @param item the item to send the command to
+ * @param commandString the command to send
+ */
+ public Object sendCommand(Item item, String commandString) {
+ if (item != null) {
+ return sendCommand(item.getName(), commandString);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sends a number as a command for a specified item to the event bus.
+ *
+ * @param item the item to send the command to
+ * @param number the number to send as a command
+ */
+ public Object sendCommand(Item item, Number number) {
+ if (item != null && number != null) {
+ return sendCommand(item.getName(), number.toString());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sends a command for a specified item to the event bus.
+ *
+ * @param itemName the name of the item to send the command to
+ * @param commandString the command to send
+ */
+ public Object sendCommand(String itemName, String commandString) {
+ if (eventPublisher != null && itemRegistry != null) {
+ try {
+ Item item = itemRegistry.getItem(itemName);
+ Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
+ eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command));
+ } catch (ItemNotFoundException e) {
+ LoggerFactory.getLogger(ScriptBusEvent.class).warn("Item '{}' does not exist.", itemName);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sends a command for a specified item to the event bus.
+ *
+ * @param item the item to send the command to
+ * @param command the command to send
+ */
+ public Object sendCommand(Item item, Command command) {
+ if (eventPublisher != null && item != null) {
+ eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
+ }
+ return null;
+ }
+
+ /**
+ * Posts a status update for a specified item to the event bus.
+ *
+ * @param item the item to send the status update for
+ * @param state the new state of the item as a number
+ */
+ public Object postUpdate(Item item, Number state) {
+ if (item != null && state != null) {
+ return postUpdate(item.getName(), state.toString());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Posts a status update for a specified item to the event bus.
+ *
+ * @param item the item to send the status update for
+ * @param stateAsString the new state of the item
+ */
+ public Object postUpdate(Item item, String stateAsString) {
+ if (item != null) {
+ return postUpdate(item.getName(), stateAsString);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Posts a status update for a specified item to the event bus.
+ *
+ * @param itemName the name of the item to send the status update for
+ * @param stateAsString the new state of the item
+ */
+ public Object postUpdate(String itemName, String stateString) {
+ if (eventPublisher != null && itemRegistry != null) {
+ try {
+ Item item = itemRegistry.getItem(itemName);
+ State state = TypeParser.parseState(item.getAcceptedDataTypes(), stateString);
+ eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state));
+ } catch (ItemNotFoundException e) {
+ LoggerFactory.getLogger(ScriptBusEvent.class).warn("Item '{}' does not exist.", itemName);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Posts a status update for a specified item to the event bus.
+ * t
+ *
+ * @param item the item to send the status update for
+ * @param state the new state of the item
+ */
+ public Object postUpdate(Item item, State state) {
+ if (eventPublisher != null && item != null) {
+ eventPublisher.post(ItemEventFactory.createStateEvent(item.getName(), state));
+ }
+ return null;
+ }
+
+ /**
+ * Stores the current states for a list of items in a map.
+ * A group item is not itself put into the map, but instead all its members.
+ *
+ * @param items the items for which the state should be stored
+ * @return the map of items with their states
+ */
+ public Map- storeStates(Item... items) {
+ Map
- statesMap = new HashMap<>();
+ if (items != null) {
+ for (Item item : items) {
+ if (item instanceof GroupItem) {
+ GroupItem groupItem = (GroupItem) item;
+ for (Item member : groupItem.getAllMembers()) {
+ statesMap.put(member, member.getState());
+ }
+ } else {
+ statesMap.put(item, item.getState());
+ }
+ }
+ }
+ return statesMap;
+ }
+
+ /**
+ * Restores item states from a map.
+ * If the saved state can be interpreted as a command, a command is sent for the item
+ * (and the physical device can send a status update if occurred). If it is no valid
+ * command, the item state is directly updated to the saved value.
+ *
+ * @param statesMap a map with ({@link Item}, {@link State}) entries
+ * @return null
+ */
+ public Object restoreStates(Map
- statesMap) {
+ if (statesMap != null) {
+ for (Entry
- entry : statesMap.entrySet()) {
+ if (entry.getValue() instanceof Command) {
+ sendCommand(entry.getKey(), (Command) entry.getValue());
+ } else {
+ postUpdate(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptThingActions.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptThingActions.java
new file mode 100644
index 00000000000..0b1d77c79e6
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/defaultscope/ScriptThingActions.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal.defaultscope;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingRegistry;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+
+/**
+ * The methods of this class are made available as functions in the scripts.
+ *
+ * Note: This class is a copy from the {@link ThingActions} class, which resides in the model.script bundle.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class ScriptThingActions {
+
+ private static final Map thingActionsMap = new HashMap<>();
+
+ ScriptThingActions(ThingRegistry thingRegistry) {
+ this.thingRegistry = thingRegistry;
+ }
+
+ private ThingRegistry thingRegistry;
+
+ public void dispose() {
+ this.thingRegistry = null;
+ }
+
+ /**
+ * Gets an actions instance of a certain scope for a given thing UID
+ *
+ * @param scope the action scope
+ * @param thingUid the UID of the thing
+ *
+ * @return actions the actions instance or null, if not available
+ */
+ public ThingActions get(String scope, String thingUid) {
+ ThingUID uid = new ThingUID(thingUid);
+ Thing thing = thingRegistry.get(uid);
+ if (thing != null) {
+ ThingHandler handler = thing.getHandler();
+ if (handler != null) {
+ ThingActions thingActions = thingActionsMap.get(getKey(scope, thingUid));
+ return thingActions;
+ }
+ }
+ return null;
+ }
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
+ void addThingActions(ThingActions thingActions) {
+ String key = getKey(thingActions);
+ thingActionsMap.put(key, thingActions);
+ }
+
+ void removeThingActions(ThingActions thingActions) {
+ String key = getKey(thingActions);
+ thingActionsMap.remove(key);
+ }
+
+ private static String getKey(ThingActions thingActions) {
+ String scope = getScope(thingActions);
+ String thingUID = getThingUID(thingActions);
+ return getKey(scope, thingUID);
+ }
+
+ private static String getKey(String scope, String thingUID) {
+ return scope + "-" + thingUID;
+ }
+
+ private static String getThingUID(ThingActions actions) {
+ return actions.getThingHandler().getThing().getUID().getAsString();
+ }
+
+ private static String getScope(ThingActions actions) {
+ ThingActionsScope scopeAnnotation = actions.getClass().getAnnotation(ThingActionsScope.class);
+ return scopeAnnotation.name();
+ }
+
+}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java
new file mode 100644
index 00000000000..3af6682b6dd
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/factory/ScriptModuleHandlerFactory.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.automation.module.script.internal.factory;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.automation.Action;
+import org.openhab.core.automation.Condition;
+import org.openhab.core.automation.Module;
+import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
+import org.openhab.core.automation.handler.ModuleHandler;
+import org.openhab.core.automation.handler.ModuleHandlerFactory;
+import org.openhab.core.automation.module.script.ScriptEngineManager;
+import org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler;
+import org.openhab.core.automation.module.script.internal.handler.ScriptConditionHandler;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This HandlerFactory creates ModuleHandlers for scripts.
+ *
+ * @author Kai Kreuzer
+ *
+ */
+@NonNullByDefault
+@Component(service = ModuleHandlerFactory.class)
+public class ScriptModuleHandlerFactory extends BaseModuleHandlerFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(ScriptModuleHandlerFactory.class);
+
+ private @NonNullByDefault({}) ScriptEngineManager scriptEngineManager;
+
+ private static final Collection TYPES = Arrays
+ .asList(new String[] { ScriptActionHandler.SCRIPT_ACTION_ID, ScriptConditionHandler.SCRIPT_CONDITION });
+
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public Collection