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 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 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 unsupportedAudioStreamClass) { + this(message, unsupportedAudioStreamClass, null); + } + + /** + * Gets the unsupported audio stream class. + * + * @return The unsupported audio stream class + */ + public Class 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 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 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 getTypes() { + return TYPES; + } + + @Reference(policy = ReferencePolicy.DYNAMIC) + public void setScriptEngineManager(ScriptEngineManager scriptEngineManager) { + this.scriptEngineManager = scriptEngineManager; + } + + public void unsetScriptEngineManager(ScriptEngineManager scriptEngineManager) { + this.scriptEngineManager = null; + } + + @Override + protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) { + logger.trace("create {} -> {}", module.getId(), module.getTypeUID()); + String moduleTypeUID = module.getTypeUID(); + if (moduleTypeUID != null) { + if (ScriptConditionHandler.SCRIPT_CONDITION.equals(moduleTypeUID) && module instanceof Condition) { + ScriptConditionHandler handler = new ScriptConditionHandler((Condition) module, ruleUID, + scriptEngineManager); + return handler; + } else if (ScriptActionHandler.SCRIPT_ACTION_ID.equals(moduleTypeUID) && module instanceof Action) { + ScriptActionHandler handler = new ScriptActionHandler((Action) module, ruleUID, scriptEngineManager); + return handler; + } else { + logger.error("The ModuleHandler is not supported: {}", moduleTypeUID); + } + } else { + logger.error("ModuleType is not registered: {}", moduleTypeUID); + } + return null; + } + +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java new file mode 100644 index 00000000000..013cfc6d68d --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java @@ -0,0 +1,138 @@ +/** + * 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.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.UUID; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; + +import org.openhab.core.automation.Module; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.module.script.ScriptEngineContainer; +import org.openhab.core.automation.module.script.ScriptEngineManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an abstract class that can be used when implementing any module handler that handles scripts. + * + * @author Kai Kreuzer - Initial contribution + * @author Simon Merschjohann + * + * @param the type of module the concrete handler can handle + */ +public abstract class AbstractScriptModuleHandler extends BaseModuleHandler { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** Constant defining the configuration parameter of modules that specifies the mime type of a script */ + protected static final String SCRIPT_TYPE = "type"; + + /** Constant defining the configuration parameter of modules that specifies the script itself */ + protected static final String SCRIPT = "script"; + + protected ScriptEngineManager scriptEngineManager; + + private final String engineIdentifier; + + private Optional scriptEngine = Optional.empty(); + private String type; + protected String script; + + private final String ruleUID; + + public AbstractScriptModuleHandler(T module, String ruleUID, ScriptEngineManager scriptEngineManager) { + super(module); + this.scriptEngineManager = scriptEngineManager; + this.ruleUID = ruleUID; + engineIdentifier = UUID.randomUUID().toString(); + + loadConfig(); + } + + @Override + public void dispose() { + if (scriptEngine != null) { + scriptEngineManager.removeEngine(engineIdentifier); + } + } + + protected Optional getScriptEngine() { + return scriptEngine.isPresent() ? scriptEngine : createScriptEngine(); + } + + private Optional createScriptEngine() { + ScriptEngineContainer container = scriptEngineManager.createScriptEngine(type, engineIdentifier); + + if (container != null) { + scriptEngine = Optional.ofNullable(container.getScriptEngine()); + return scriptEngine; + } else { + logger.debug("No engine available for script type '{}' in action '{}'.", type, module.getId()); + return Optional.empty(); + } + } + + private void loadConfig() { + Object type = module.getConfiguration().get(SCRIPT_TYPE); + Object script = module.getConfiguration().get(SCRIPT); + if (!isValid(type)) { + throw new IllegalStateException(String.format("Type is missing in the configuration of module '%s'.", module.getId())); + } else if (!isValid(script)) { + throw new IllegalStateException(String.format("Script is missing in the configuration of module '%s'.", module.getId())); + } else { + this.type = (String) type; + this.script = (String) script; + } + } + + private boolean isValid(Object parameter) { + return parameter != null && parameter instanceof String && !((String) parameter).trim().isEmpty(); + } + + /** + * Adds the passed context variables of the rule engine to the context scope of the ScriptEngine, this should be + * updated each time the module is executed + * + * @param engine the script engine that is used + * @param context the variables and types to put into the execution context + */ + protected void setExecutionContext(ScriptEngine engine, Map context) { + ScriptContext executionContext = engine.getContext(); + + // Add the rule's UID to the context and make it available as "ctx". + // Note: We don't use "context" here as it doesn't work on all JVM versions! + final Map contextNew = new HashMap<>(context); + contextNew.put("ruleUID", this.ruleUID); + executionContext.setAttribute("ctx", contextNew, ScriptContext.ENGINE_SCOPE); + + // Add the rule's UID to the global namespace. + executionContext.setAttribute("ruleUID", this.ruleUID, ScriptContext.ENGINE_SCOPE); + + // add the single context entries without their prefix to the scope + for (Entry entry : context.entrySet()) { + Object value = entry.getValue(); + String key = entry.getKey(); + int dotIndex = key.indexOf('.'); + if (dotIndex != -1) { + key = key.substring(dotIndex + 1); + } + executionContext.setAttribute(key, value, ScriptContext.ENGINE_SCOPE); + } + } + +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java new file mode 100644 index 00000000000..7178465be70 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java @@ -0,0 +1,69 @@ +/** + * 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.handler; + +import java.util.HashMap; +import java.util.Map; + +import javax.script.ScriptException; + +import org.openhab.core.automation.Action; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.module.script.ScriptEngineManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This handler can execute script actions. + * + * @author Kai Kreuzer - Initial contribution and API + * @author Simon Merschjohann + * + */ +public class ScriptActionHandler extends AbstractScriptModuleHandler implements ActionHandler { + + public static final String SCRIPT_ACTION_ID = "script.ScriptAction"; + + private final Logger logger = LoggerFactory.getLogger(ScriptActionHandler.class); + + /** + * constructs a new ScriptActionHandler + * + * @param module + * @param ruleUid the UID of the rule this handler is used for + */ + public ScriptActionHandler(Action module, String ruleUID, ScriptEngineManager scriptEngineManager) { + super(module, ruleUID, scriptEngineManager); + } + + @Override + public void dispose() { + } + + @Override + public Map execute(final Map context) { + HashMap resultMap = new HashMap(); + + getScriptEngine().ifPresent(scriptEngine -> { + setExecutionContext(scriptEngine, context); + try { + Object result = scriptEngine.eval(script); + resultMap.put("result", result); + } catch (ScriptException e) { + logger.error("Script execution failed: {}", e.getMessage()); + } + }); + + return resultMap; + } +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java new file mode 100644 index 00000000000..acd73a80868 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java @@ -0,0 +1,67 @@ +/** + * 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.handler; + +import java.util.Map; +import java.util.Optional; + +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.ConditionHandler; +import org.openhab.core.automation.module.script.ScriptEngineManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This handler can evaluate a condition based on a script. + * + * @author Kai Kreuzer - Initial contribution + * @author Simon Merschjohann + * + */ +public class ScriptConditionHandler extends AbstractScriptModuleHandler implements ConditionHandler { + + public final Logger logger = LoggerFactory.getLogger(ScriptConditionHandler.class); + + public static final String SCRIPT_CONDITION = "script.ScriptCondition"; + + public ScriptConditionHandler(Condition module, String ruleUID, ScriptEngineManager scriptEngineManager) { + super(module, ruleUID, scriptEngineManager); + } + + @Override + public boolean isSatisfied(final Map context) { + boolean result = false; + Optional engine = getScriptEngine(); + + if (engine.isPresent()) { + ScriptEngine scriptEngine = engine.get(); + setExecutionContext(scriptEngine, context); + try { + Object returnVal = scriptEngine.eval(script); + if (returnVal instanceof Boolean) { + result = (boolean) returnVal; + } else { + logger.error("Script did not return a boolean value, but '{}'", returnVal.toString()); + } + } catch (ScriptException e) { + logger.error("Script execution failed: {}", e.getMessage()); + } + } + + return result; + } + +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/resources/ESH-INF/automation/moduletypes/ScriptTypes.json b/bundles/org.openhab.core.automation.module.script/src/main/resources/ESH-INF/automation/moduletypes/ScriptTypes.json new file mode 100644 index 00000000000..09814a89024 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/resources/ESH-INF/automation/moduletypes/ScriptTypes.json @@ -0,0 +1,67 @@ +{ + "conditions":[ + { + "uid":"script.ScriptCondition", + "label":"a given script evaluates to true", + "description":"Allows the definition of a condition through a script.", + "configDescriptions":[ + { + "name":"type", + "type":"TEXT", + "description":"the scripting language used", + "required":true, + "options":[ + { + "label": "Javascript", + "value": "application/javascript" + } + ] + }, + { + "name":"script", + "type":"TEXT", + "description":"the script to execute", + "required":true, + "context":"script" + } + ] + } + ], + "actions":[ + { + "uid":"script.ScriptAction", + "label":"execute a given script", + "description":"Allows the execution of a user-defined script.", + "configDescriptions":[ + { + "name":"type", + "type":"TEXT", + "description":"the scripting language used", + "required":true, + "options":[ + { + "label": "Javascript", + "value": "application/javascript" + } + ], + "defaultValue":"application/javascript" + }, + { + "name":"script", + "type":"TEXT", + "description":"the script to execute", + "required":true, + "context":"script" + } + ], + "outputs":[ + { + "name":"result", + "type":"java.lang.Object", + "label":"result", + "description":"the script result" + } + ] + } + ] +} diff --git a/bundles/org.openhab.core.automation.rest/.classpath b/bundles/org.openhab.core.automation.rest/.classpath new file mode 100644 index 00000000000..3c5e7d17553 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.automation.rest/.project b/bundles/org.openhab.core.automation.rest/.project new file mode 100644 index 00000000000..ee1b2f25bbc --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.automation.rest + + + + + + 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.rest/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.automation.rest/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..0c4313356dd --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/.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.rest/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.automation.rest/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..b8947ec6fc0 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/.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.rest/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.automation.rest/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000000..f897a7f1cb2 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/.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.rest/NOTICE b/bundles/org.openhab.core.automation.rest/NOTICE new file mode 100644 index 00000000000..b8675cd02e8 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/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.rest/pom.xml b/bundles/org.openhab.core.automation.rest/pom.xml new file mode 100644 index 00000000000..15ea0352dda --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/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.rest + + openHAB Core :: Bundles :: Automation REST API + + + + org.openhab.core.bundles + org.openhab.core.automation + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.io.rest + ${project.version} + + + + diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.java new file mode 100644 index 00000000000..9f17a67a5f5 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ModuleTypeResource.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.rest.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.eclipse.smarthome.io.rest.LocaleService; +import org.eclipse.smarthome.io.rest.RESTResource; +import org.openhab.core.automation.dto.ActionTypeDTOMapper; +import org.openhab.core.automation.dto.ConditionTypeDTOMapper; +import org.openhab.core.automation.dto.ModuleTypeDTO; +import org.openhab.core.automation.dto.TriggerTypeDTOMapper; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.type.TriggerType; +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 io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +/** + * This class acts as a REST resource for module types and is registered with the Jersey servlet. + * + * @author Kai Kreuzer - Initial contribution + * @author Markus Rathgeb - Use DTOs + * @author Ana Dimova - extends Module type DTOs with composites + */ +@Path("module-types") +@Api("module-types") +@Component +public class ModuleTypeResource implements RESTResource { + + private ModuleTypeRegistry moduleTypeRegistry; + private LocaleService localeService; + + @Context + private UriInfo uriInfo; + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = moduleTypeRegistry; + } + + protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = null; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setLocaleService(LocaleService localeService) { + this.localeService = localeService; + } + + protected void unsetLocaleService(LocaleService localeService) { + this.localeService = null; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Get all available module types.", response = ModuleTypeDTO.class, responseContainer = "List") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = ModuleTypeDTO.class, responseContainer = "List") }) + public Response getAll(@HeaderParam("Accept-Language") @ApiParam(value = "language") String language, + @QueryParam("tags") @ApiParam(value = "tags for filtering", required = false) String tagList, + @QueryParam("type") @ApiParam(value = "filtering by action, condition or trigger", required = false) String type) { + final Locale locale = localeService.getLocale(language); + final String[] tags = tagList != null ? tagList.split(",") : null; + final List modules = new ArrayList(); + + if (type == null || type.equals("trigger")) { + modules.addAll(TriggerTypeDTOMapper.map(moduleTypeRegistry.getTriggers(locale, tags))); + } + if (type == null || type.equals("condition")) { + modules.addAll(ConditionTypeDTOMapper.map(moduleTypeRegistry.getConditions(locale, tags))); + } + if (type == null || type.equals("action")) { + modules.addAll(ActionTypeDTOMapper.map(moduleTypeRegistry.getActions(locale, tags))); + } + return Response.ok(modules).build(); + } + + @GET + @Path("/{moduleTypeUID}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets a module type corresponding to the given UID.", response = ModuleTypeDTO.class) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ModuleTypeDTO.class), + @ApiResponse(code = 404, message = "Module Type corresponding to the given UID does not found.") }) + public Response getByUID(@HeaderParam("Accept-Language") @ApiParam(value = "language") String language, + @PathParam("moduleTypeUID") @ApiParam(value = "moduleTypeUID", required = true) String moduleTypeUID) { + Locale locale = localeService.getLocale(language); + final ModuleType moduleType = moduleTypeRegistry.get(moduleTypeUID, locale); + if (moduleType != null) { + return Response.ok(getModuleTypeDTO(moduleType)).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + private ModuleTypeDTO getModuleTypeDTO(final ModuleType moduleType) { + if (moduleType instanceof ActionType) { + if (moduleType instanceof CompositeActionType) { + return ActionTypeDTOMapper.map((CompositeActionType) moduleType); + } + return ActionTypeDTOMapper.map((ActionType) moduleType); + } else if (moduleType instanceof ConditionType) { + if (moduleType instanceof CompositeConditionType) { + return ConditionTypeDTOMapper.map((CompositeConditionType) moduleType); + } + return ConditionTypeDTOMapper.map((ConditionType) moduleType); + } else if (moduleType instanceof TriggerType) { + if (moduleType instanceof CompositeTriggerType) { + return TriggerTypeDTOMapper.map((CompositeTriggerType) moduleType); + } + return TriggerTypeDTOMapper.map((TriggerType) moduleType); + } else { + throw new IllegalArgumentException( + String.format("Cannot handle given module type class (%s)", moduleType.getClass())); + } + } + + @Override + public boolean isSatisfied() { + return moduleTypeRegistry != null && localeService != null; + } + +} diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java new file mode 100644 index 00000000000..62188f633b0 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java @@ -0,0 +1,482 @@ +/** + * 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.rest.internal; + +import static org.openhab.core.automation.RulePredicates.*; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.eclipse.smarthome.config.core.ConfigUtil; +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.io.rest.JSONResponse; +import org.eclipse.smarthome.io.rest.RESTResource; +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.RuleManager; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.dto.ActionDTO; +import org.openhab.core.automation.dto.ActionDTOMapper; +import org.openhab.core.automation.dto.ConditionDTO; +import org.openhab.core.automation.dto.ConditionDTOMapper; +import org.openhab.core.automation.dto.ModuleDTO; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleDTOMapper; +import org.openhab.core.automation.dto.TriggerDTO; +import org.openhab.core.automation.dto.TriggerDTOMapper; +import org.openhab.core.automation.rest.internal.dto.EnrichedRuleDTO; +import org.openhab.core.automation.rest.internal.dto.EnrichedRuleDTOMapper; +import org.openhab.core.automation.util.ModuleBuilder; +import org.openhab.core.automation.util.RuleBuilder; +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; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.ResponseHeader; + +/** + * This class acts as a REST resource for rules and is registered with the Jersey servlet. + * + * @author Kai Kreuzer - Initial contribution + * @author Markus Rathgeb - Use DTOs + */ +@Path("rules") +@Api("rules") +@Component +public class RuleResource implements RESTResource { + + private final Logger logger = LoggerFactory.getLogger(RuleResource.class); + + private RuleRegistry ruleRegistry; + private RuleManager ruleManager; + + @Context + private UriInfo uriInfo; + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setRuleRegistry(RuleRegistry ruleRegistry) { + this.ruleRegistry = ruleRegistry; + } + + protected void unsetRuleRegistry(RuleRegistry ruleRegistry) { + this.ruleRegistry = null; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setRuleManager(RuleManager ruleManager) { + this.ruleManager = ruleManager; + } + + protected void unsetRuleManager(RuleManager ruleManager) { + this.ruleManager = null; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Get available rules, optionally filtered by tags and/or prefix.", response = EnrichedRuleDTO.class, responseContainer = "Collection") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = EnrichedRuleDTO.class, responseContainer = "Collection") }) + public Response get(@QueryParam("prefix") final String prefix, @QueryParam("tags") final List tags) { + // match all + Predicate p = r -> true; + + // prefix parameter has been used + if (null != prefix) { + // works also for null prefix + // (empty prefix used if searching for rules without prefix) + p = p.and(hasPrefix(prefix)); + } + + // if tags is null or empty list returns all rules + p = p.and(hasAllTags(tags)); + + final Collection rules = ruleRegistry.stream().filter(p) // filter according to Predicates + .map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager)) // map matching rules + .collect(Collectors.toList()); + + return Response.ok(rules).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Creates a rule.") + @Produces(MediaType.APPLICATION_JSON) + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Created", responseHeaders = @ResponseHeader(name = "Location", description = "Newly created Rule", response = String.class)), + @ApiResponse(code = 409, message = "Creation of the rule is refused. Rule with the same UID already exists."), + @ApiResponse(code = 400, message = "Creation of the rule is refused. Missing required parameter.") }) + public Response create(@ApiParam(value = "rule data", required = true) RuleDTO rule) throws IOException { + try { + final Rule newRule = ruleRegistry.add(RuleDTOMapper.map(rule)); + return Response.status(Status.CREATED) + .header("Location", "rules/" + URLEncoder.encode(newRule.getUID(), "UTF-8")).build(); + } catch (IllegalArgumentException e) { + String errMessage = "Creation of the rule is refused: " + e.getMessage(); + logger.warn("{}", errMessage); + return JSONResponse.createErrorResponse(Status.CONFLICT, errMessage); + } catch (RuntimeException e) { + String errMessage = "Creation of the rule is refused: " + e.getMessage(); + logger.warn("{}", errMessage); + return JSONResponse.createErrorResponse(Status.BAD_REQUEST, errMessage); + } + } + + @GET + @Path("/{ruleUID}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule corresponding to the given UID.", response = EnrichedRuleDTO.class) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = EnrichedRuleDTO.class), + @ApiResponse(code = 404, message = "Rule not found") }) + public Response getByUID(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + return Response.ok(EnrichedRuleDTOMapper.map(rule, ruleManager)).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @DELETE + @Path("/{ruleUID}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Removes an existing rule corresponding to the given UID.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response remove(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) { + Rule removedRule = ruleRegistry.remove(ruleUID); + if (removedRule == null) { + logger.info("Received HTTP DELETE request at '{}' for the unknown rule '{}'.", uriInfo.getPath(), ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + @PUT + @Path("/{ruleUID}") + @Consumes(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Updates an existing rule corresponding to the given UID.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response update(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @ApiParam(value = "rule data", required = true) RuleDTO rule) throws IOException { + rule.uid = ruleUID; + final Rule oldRule = ruleRegistry.update(RuleDTOMapper.map(rule)); + if (oldRule == null) { + logger.info("Received HTTP PUT request for update at '{}' for the unknown rule '{}'.", uriInfo.getPath(), + ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + @GET + @Path("/{ruleUID}/config") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule configuration values.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response getConfiguration(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) + throws IOException { + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + logger.info("Received HTTP GET request for config at '{}' for the unknown rule '{}'.", uriInfo.getPath(), + ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } else { + return Response.ok(rule.getConfiguration().getProperties()).build(); + } + } + + @PUT + @Path("/{ruleUID}/config") + @Consumes(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Sets the rule configuration values.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response updateConfiguration( + @PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @ApiParam(value = "config") Map configurationParameters) throws IOException { + Map config = ConfigUtil.normalizeTypes(configurationParameters); + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + logger.info("Received HTTP PUT request for update config at '{}' for the unknown rule '{}'.", + uriInfo.getPath(), ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } else { + rule = RuleBuilder.create(rule).withConfiguration(new Configuration(config)).build(); + ruleRegistry.update(rule); + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + } + + @POST + @Path("/{ruleUID}/enable") + @Consumes(MediaType.TEXT_PLAIN) + @ApiOperation(value = "Sets the rule enabled status.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response enableRule(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @ApiParam(value = "enable", required = true) String enabled) throws IOException { + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + logger.info("Received HTTP PUT request for set enabled at '{}' for the unknown rule '{}'.", + uriInfo.getPath(), ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } else { + ruleManager.setEnabled(ruleUID, !"false".equalsIgnoreCase(enabled)); + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + } + + @POST + @Path("/{ruleUID}/runnow") + @Consumes(MediaType.TEXT_PLAIN) + @ApiOperation(value = "Executes actions of the rule.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response runNow(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) + throws IOException { + Rule rule = ruleRegistry.get(ruleUID); + if (rule == null) { + logger.info("Received HTTP PUT request for run now at '{}' for the unknown rule '{}'.", uriInfo.getPath(), + ruleUID); + return Response.status(Status.NOT_FOUND).build(); + } else { + ruleManager.runNow(ruleUID); + return Response.ok().build(); + } + } + + @GET + @Path("/{ruleUID}/triggers") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule triggers.", response = TriggerDTO.class, responseContainer = "List") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = TriggerDTO.class, responseContainer = "List"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response getTriggers(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + return Response.ok(TriggerDTOMapper.map(rule.getTriggers())).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @GET + @Path("/{ruleUID}/conditions") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule conditions.", response = ConditionDTO.class, responseContainer = "List") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = ConditionDTO.class, responseContainer = "List"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response getConditions(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + return Response.ok(ConditionDTOMapper.map(rule.getConditions())).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @GET + @Path("/{ruleUID}/actions") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule actions.", response = ActionDTO.class, responseContainer = "List") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = ActionDTO.class, responseContainer = "List"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found.") }) + public Response getActions(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + return Response.ok(ActionDTOMapper.map(rule.getActions())).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @GET + @Path("/{ruleUID}/{moduleCategory}/{id}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the rule's module corresponding to the given Category and ID.", response = ModuleDTO.class) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = ModuleDTO.class), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found or does not have a module with such Category and ID.") }) + public Response getModuleById(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @PathParam("moduleCategory") @ApiParam(value = "moduleCategory", required = true) String moduleCategory, + @PathParam("id") @ApiParam(value = "id", required = true) String id) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + final ModuleDTO dto = getModuleDTO(rule, moduleCategory, id); + if (dto != null) { + return Response.ok(dto).build(); + } + } + return Response.status(Status.NOT_FOUND).build(); + } + + @GET + @Path("/{ruleUID}/{moduleCategory}/{id}/config") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets the module's configuration.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found or does not have a module with such Category and ID.") }) + public Response getModuleConfig(@PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @PathParam("moduleCategory") @ApiParam(value = "moduleCategory", required = true) String moduleCategory, + @PathParam("id") @ApiParam(value = "id", required = true) String id) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + Module module = getModule(rule, moduleCategory, id); + if (module != null) { + return Response.ok(module.getConfiguration().getProperties()).build(); + } + } + return Response.status(Status.NOT_FOUND).build(); + } + + @GET + @Path("/{ruleUID}/{moduleCategory}/{id}/config/{param}") + @Produces(MediaType.TEXT_PLAIN) + @ApiOperation(value = "Gets the module's configuration parameter.", response = String.class) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = String.class), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found or does not have a module with such Category and ID.") }) + public Response getModuleConfigParam( + @PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @PathParam("moduleCategory") @ApiParam(value = "moduleCategory", required = true) String moduleCategory, + @PathParam("id") @ApiParam(value = "id", required = true) String id, + @PathParam("param") @ApiParam(value = "param", required = true) String param) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + Module module = getModule(rule, moduleCategory, id); + if (module != null) { + return Response.ok(module.getConfiguration().getProperties().get(param)).build(); + } + } + return Response.status(Status.NOT_FOUND).build(); + } + + @PUT + @Path("/{ruleUID}/{moduleCategory}/{id}/config/{param}") + @ApiOperation(value = "Sets the module's configuration parameter value.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Rule corresponding to the given UID does not found or does not have a module with such Category and ID.") }) + @Consumes(MediaType.TEXT_PLAIN) + public Response setModuleConfigParam( + @PathParam("ruleUID") @ApiParam(value = "ruleUID", required = true) String ruleUID, + @PathParam("moduleCategory") @ApiParam(value = "moduleCategory", required = true) String moduleCategory, + @PathParam("id") @ApiParam(value = "id", required = true) String id, + @PathParam("param") @ApiParam(value = "param", required = true) String param, + @ApiParam(value = "value", required = true) String value) { + Rule rule = ruleRegistry.get(ruleUID); + if (rule != null) { + Module module = getModule(rule, moduleCategory, id); + if (module != null) { + Configuration configuration = module.getConfiguration(); + configuration.put(param, ConfigUtil.normalizeType(value)); + module = ModuleBuilder.create(module).withConfiguration(configuration).build(); + ruleRegistry.update(rule); + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + } + return Response.status(Status.NOT_FOUND).build(); + } + + protected T getModuleById(final Collection coll, final String id) { + if (coll == null) { + return null; + } + for (final T module : coll) { + if (module.getId().equals(id)) { + return module; + } + } + return null; + } + + protected Trigger getTrigger(Rule rule, String id) { + return getModuleById(rule.getTriggers(), id); + } + + protected Condition getCondition(Rule rule, String id) { + return getModuleById(rule.getConditions(), id); + } + + protected Action getAction(Rule rule, String id) { + return getModuleById(rule.getActions(), id); + } + + protected Module getModule(Rule rule, String moduleCategory, String id) { + if (moduleCategory.equals("triggers")) { + return getTrigger(rule, id); + } else if (moduleCategory.equals("conditions")) { + return getCondition(rule, id); + } else if (moduleCategory.equals("actions")) { + return getAction(rule, id); + } else { + return null; + } + } + + protected ModuleDTO getModuleDTO(Rule rule, String moduleCategory, String id) { + if (moduleCategory.equals("triggers")) { + final Trigger trigger = getTrigger(rule, id); + return trigger == null ? null : TriggerDTOMapper.map(trigger); + } else if (moduleCategory.equals("conditions")) { + final Condition condition = getCondition(rule, id); + return condition == null ? null : ConditionDTOMapper.map(condition); + } else if (moduleCategory.equals("actions")) { + final Action action = getAction(rule, id); + return action == null ? null : ActionDTOMapper.map(action); + } else { + return null; + } + } + + @Override + public boolean isSatisfied() { + return ruleRegistry != null && ruleManager != null; + } + +} diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java new file mode 100644 index 00000000000..8dae991462b --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/TemplateResource.java @@ -0,0 +1,115 @@ +/** + * 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.rest.internal; + +import java.util.Collection; +import java.util.Locale; +import java.util.stream.Collectors; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.eclipse.smarthome.io.rest.LocaleService; +import org.eclipse.smarthome.io.rest.RESTResource; +import org.openhab.core.automation.dto.RuleTemplateDTO; +import org.openhab.core.automation.dto.RuleTemplateDTOMapper; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.template.TemplateRegistry; +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 io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +/** + * This class acts as a REST resource for templates and is registered with the Jersey servlet. + * + * @author Kai Kreuzer - Initial contribution + */ +@Path("templates") +@Api("templates") +@Component +public class TemplateResource implements RESTResource { + + private TemplateRegistry templateRegistry; + private LocaleService localeService; + + @Context + private UriInfo uriInfo; + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setTemplateRegistry(TemplateRegistry templateRegistry) { + this.templateRegistry = templateRegistry; + } + + protected void unsetTemplateRegistry(TemplateRegistry templateRegistry) { + this.templateRegistry = null; + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setLocaleService(LocaleService localeService) { + this.localeService = localeService; + } + + protected void unsetLocaleService(LocaleService localeService) { + this.localeService = null; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Get all available templates.", response = Template.class, responseContainer = "Collection") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", response = Template.class, responseContainer = "Collection") }) + public Response getAll(@HeaderParam("Accept-Language") @ApiParam(value = "language") String language) { + Locale locale = localeService.getLocale(language); + Collection result = templateRegistry.getAll(locale).stream() + .map(template -> RuleTemplateDTOMapper.map(template)).collect(Collectors.toList()); + return Response.ok(result).build(); + } + + @GET + @Path("/{templateUID}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Gets a template corresponding to the given UID.", response = Template.class) + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Template.class), + @ApiResponse(code = 404, message = "Template corresponding to the given UID does not found.") }) + public Response getByUID(@HeaderParam("Accept-Language") @ApiParam(value = "language") String language, + @PathParam("templateUID") @ApiParam(value = "templateUID", required = true) String templateUID) { + Locale locale = localeService.getLocale(language); + RuleTemplate template = templateRegistry.get(templateUID, locale); + if (template != null) { + return Response.ok(RuleTemplateDTOMapper.map(template)).build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } + + @Override + public boolean isSatisfied() { + return templateRegistry != null && localeService != null; + } +} diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTO.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTO.java new file mode 100644 index 00000000000..3da8cafbb34 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTO.java @@ -0,0 +1,28 @@ +/** + * 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.rest.internal.dto; + +import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.dto.RuleDTO; + +/** + * This is a data transfer object that is used to serialize rules with dynamic data like the status. + * + * @author Kai Kreuzer - Initial contribution + * + */ +public class EnrichedRuleDTO extends RuleDTO { + + public RuleStatusInfo status; + +} diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTOMapper.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTOMapper.java new file mode 100644 index 00000000000..a2afc6911e4 --- /dev/null +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/dto/EnrichedRuleDTOMapper.java @@ -0,0 +1,33 @@ +/** + * 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.rest.internal.dto; + +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleManager; +import org.openhab.core.automation.dto.RuleDTOMapper; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class EnrichedRuleDTOMapper extends RuleDTOMapper { + + public static EnrichedRuleDTO map(final Rule rule, final RuleManager ruleEngine) { + final EnrichedRuleDTO enrichedRuleDto = new EnrichedRuleDTO(); + fillProperties(rule, enrichedRuleDto); + enrichedRuleDto.status = ruleEngine.getStatusInfo(rule.getUID()); + return enrichedRuleDto; + } + +} diff --git a/bundles/org.openhab.core.automation/.classpath b/bundles/org.openhab.core.automation/.classpath new file mode 100644 index 00000000000..c1aac8d69df --- /dev/null +++ b/bundles/org.openhab.core.automation/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.automation/.project b/bundles/org.openhab.core.automation/.project new file mode 100644 index 00000000000..f4b127192c5 --- /dev/null +++ b/bundles/org.openhab.core.automation/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.automation + + + + + + 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/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.core.automation/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..0c4313356dd --- /dev/null +++ b/bundles/org.openhab.core.automation/.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/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.core.automation/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..b8947ec6fc0 --- /dev/null +++ b/bundles/org.openhab.core.automation/.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/.settings/org.eclipse.m2e.core.prefs b/bundles/org.openhab.core.automation/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000000..f897a7f1cb2 --- /dev/null +++ b/bundles/org.openhab.core.automation/.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/NOTICE b/bundles/org.openhab.core.automation/NOTICE new file mode 100644 index 00000000000..b8675cd02e8 --- /dev/null +++ b/bundles/org.openhab.core.automation/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/README.md b/bundles/org.openhab.core.automation/README.md new file mode 100644 index 00000000000..66b17075fc6 --- /dev/null +++ b/bundles/org.openhab.core.automation/README.md @@ -0,0 +1,48 @@ +# Action modules via annotated classes + +Action modules can be defined by writing classes and annotating their methods with special annotations. +The framework offers two providers, namely `AnnotatedActionModuleTypeProvider` and `AnnotatedThingActionModuleTypeProvider`, which collect these annotated elements and dynamically create the according action modules. + +## Types of annotated classes + +There are three different ways to offer action modules via annotations: + +### Service + +If the goal is to provide an action which is independent of a specific `ThingHandler` and which should only exists as one single instance, it should be implemented as a service. +This service has to implement the `org.openhab.core.automation.AnnotatedActions` interface. +It can be configured like any other framework service via a `ConfigDescription`, however its category should be `RuleActions`. + +### Multi Service + +This case is similar to the one above, except for that it can be instantiated and thus configured multiple times. +The service also has to implement the `org.openhab.core.automation.AnnotatedActions` interface. +It makes use of the multi service infrastructure of the framework and the specified "Service context" becomes the identifier for the specific configuration. +Its category should also be `RuleActions`. + +### Thing + +For actions that need access to the logic of a `ThingHandler`, one has to implement a service which implements the `org.eclipse.smarthome.core.thing.binding.AnnotatedActionThingHandlerService` interface. +The `ThingHandler` has to override the `Collection getServices()` method from the `BaseThingHandler` and return the class of the aforementioned service. +The framework takes care of registering and un-registering of that service. + +## Annotations + +Service classes mentioned above should have the following annotations: + +- `@ActionScope(name = "myScope")`: This annotation has to be on the class and `myScope` defines the first part of the ModuleType UID, for example `binding.myBindinName` or `twitter`. +- `@RuleAction(label = "@text/myLabel", description = "myDescription text")`: Each method that should be offered as an action has to have this annotation. The method name will be the second part of the ModuleType uid (after the scope, separated by a "."). There are more parameters available, basically all fields which are part of `org.openhab.core.automation.type.ActionType`. Translations are also possible if `@text/id` placeholders are used and the bundle providing the actions offers the corresponding files. +- `@ActionOutput(name = "output1", type = "java.lang.String")`: This annotation (or multiple of it) has to be on the return type of the method and specifies under which name and type a result will be available. Usually the type should be the fully qualified Java type, but in the future it will be extented to support further types. +- `@ActionInput(name = "input1")`: This annotation has to be before a parameter of the method to name the input for the module. If the annotation is omitted, the implicit name will be "pN", whereas "N" will be the position of the parameter, i.e. 0-n. + +## Method definition + +Each annotated method inside of such a service will be turned into an Action ModuleType, i.e. one can have multiple module type definitions per service if multiple methods are annotated. +In addition to the annotations the methods should have a proper name since it is used inside the ModuleType uid. +The return type of a method should be `Map`, because the automation engine uses this mapping between all its modules, or `void` if it does not provide any outputs. +However, there is one shortcut for simple dataypes like `boolean`, `String`, `int`, `double`, and `float`. Such return types will automatically be put into a map with the predefined keyword "result" for the following modules to process. +Within the implementation of the method, only those output names which are specified as `@ActionOutput` annotations should be used, i.e. the resulting map should only contain those which also appear in an `@ActionOutput` annotation. + +## Examples + +For examples, please see the package `org.eclipse.smarthome.magic.binding.internal.automation.modules` inside the `org.eclipse.smarthome.magic` bundle. diff --git a/bundles/org.openhab.core.automation/pom.xml b/bundles/org.openhab.core.automation/pom.xml new file mode 100644 index 00000000000..36b8107de86 --- /dev/null +++ b/bundles/org.openhab.core.automation/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.automation + + openHAB Core :: Bundles :: Automation + + + + org.openhab.core.bundles + org.openhab.core.config.core + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.scheduler + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.thing + ${project.version} + + + + diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Action.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Action.java new file mode 100644 index 00000000000..cabc735746b --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Action.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.openhab.core.automation; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This interface represents automation {@code Action} modules which are the expected result of {@link Rule}s execution. + * They describe the actual work that should be performed by the Rule as a response to a trigger. + *

+ * Each Action can provide information to the next Actions in the list through its {@link Output}s. The actions have + * {@link Input}s to process input data from other Actions or {@link Trigger}s. + *

+ * Actions can be configured. + *

+ * The building elements of the Actions are {@link ConfigDescriptionParameter}s, {@link Input}s and {@link Output}s. + * They are defined by the corresponding {@link ActionType}. + *

+ * Action modules are placed in the actions section of the {@link Rule} definition. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + */ +@NonNullByDefault +public interface Action extends Module { + + /** + * Gets the input references of the Action. The references define how the {@link Input}s of this {@link Module} are + * connected to {@link Output}s of other {@link Module}s. + * + * @return a map with the input references of this action. + */ + Map getInputs(); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/AnnotatedActions.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/AnnotatedActions.java new file mode 100644 index 00000000000..8b73d192a36 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/AnnotatedActions.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; + +/** + * Marker interface for RuleActions + * + * Every method in the implementation should provide annotations which are used to create the ModuleTypes + * + * @author Stefan Triller - initial contribution + * + */ +public interface AnnotatedActions { + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Condition.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Condition.java new file mode 100644 index 00000000000..f66b62899ef --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Condition.java @@ -0,0 +1,53 @@ +/** + * 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; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This interface represents automation {@code Condition} modules which are working as a filter for {@link Rule}'s + * executions. After being triggered, a Rule's execution will continue only if all its conditions are satisfied. + *

+ * Conditions can be used to check the output from the trigger or other data available in the system. To receive an + * output data from triggers the Conditions have {@link Input}s. + *

+ * Conditions can be configured. + *

+ * Conditions don't have {@link Output}s 'cause they don't provide information to the other modules of the Rule. + *

+ * Building elements of conditions as {@link ConfigDescriptionParameter}s and {@link Input}s. They are defined by the + * corresponding {@link ConditionType}. + *

+ * Condition modules are placed in conditions section of the {@link Rule} definition. + * + * @see Module + * @author Yordan Mihaylov - Initial Contribution + */ +@NonNullByDefault +public interface Condition extends Module { + + /** + * Gets the input references of the Condition. The references define how the {@link Input}s of this {@link Module} + * are connected to {@link Output}s of other {@link Module}s. + * + * @return a map that contains the input references of this condition. + */ + Map getInputs(); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ManagedRuleProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ManagedRuleProvider.java new file mode 100644 index 00000000000..b03d99dc95c --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ManagedRuleProvider.java @@ -0,0 +1,67 @@ +/** + * 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; + +import org.eclipse.smarthome.core.common.registry.AbstractManagedProvider; +import org.eclipse.smarthome.core.storage.StorageService; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleProvider; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleDTOMapper; +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; + +/** + * Implementation of a rule provider that uses the storage service for persistence + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Persistence implementation + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * @author Markus Rathgeb - fix mapping between element and persistable element + */ +@Component(service = { RuleProvider.class, ManagedRuleProvider.class }) +public class ManagedRuleProvider extends AbstractManagedProvider implements RuleProvider { + + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) + @Override + protected void setStorageService(final StorageService storageService) { + super.setStorageService(storageService); + } + + @Override + protected void unsetStorageService(final StorageService storageService) { + super.unsetStorageService(storageService); + } + + @Override + protected String getStorageName() { + return "automation_rules"; + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected Rule toElement(String key, RuleDTO persistableElement) { + return RuleDTOMapper.map(persistableElement); + } + + @Override + protected RuleDTO toPersistableElement(Rule element) { + return RuleDTOMapper.map(element); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java new file mode 100644 index 00000000000..0d64ee65c9e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Module.java @@ -0,0 +1,81 @@ +/** + * 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; + +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.type.Input; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.Output; + +/** + * This interface represents automation {@code Modules} which are building components of the {@link Rule}s. + *

+ * Each module is identified by id, which is unique in scope of the {@link Rule}. + *

+ * Each module has a {@link ModuleType} which provides meta data of the module. The meta data defines {@link Input}s, + * {@link Output}s and {@link ConfigDescriptionParameter}s which are the building elements of the {@link Module}. + *
+ * Setters of the module don't have immediate effect on the Rule. To apply the changes, the Module should be set on the + * {@link Rule} and the Rule has to be updated in {@link RuleRegistry} by invoking {@code update} method. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Kai Kreuzer - Initial Contribution + */ +@NonNullByDefault +public interface Module { + + /** + * Gets the {@link Module}'s unique identifier in the scope of the rule in which this module belongs. The identifier + * of the {@link Module} is used to identify it when other rule's module refers it as input. + * + * @return the {@link Module}'s unique identifier in the scope of the rule in which this module belongs. + */ + String getId(); + + /** + * Gets the module type unique identifier which is a reference to the corresponding {@link ModuleType} that + * describes this module. The {@link ModuleType} contains {@link Input}s, {@link Output}s and + * {@link ConfigDescriptionParameter}s of this module. + * + * @return the {@link ModuleType} unique identifier. + */ + String getTypeUID(); + + /** + * Gets the label of the {@link Module}. The label is user understandable name of the Module. + * + * @return the label of the module or {@code null} if not specified. + */ + @Nullable + String getLabel(); + + /** + * Gets the description of the {@link Module}. The description is a detailed, human understandable description of + * the Module. + * + * @return the detailed description of the module or {@code null} if not specified. + */ + @Nullable + String getDescription(); + + /** + * Gets the configuration values of the {@link Module}. + * + * @return the current configuration values of the {@link Module}. + */ + Configuration getConfiguration(); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ModuleHandlerCallback.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ModuleHandlerCallback.java new file mode 100644 index 00000000000..76cfd40252a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/ModuleHandlerCallback.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; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; + +/** + * This class is responsible to provide a {@link RegistryChangeListener} logic. A instance of it is added to + * {@link RuleRegistry} service, to listen for changes when a single {@link Rule} has been added, updated, enabled, + * disabled or removed and to involve Rule Engine to process these changes. Also to send a {@code run} command + * for a single {@link Rule} to the Rule Engine. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public interface ModuleHandlerCallback { + + /** + * This method gets enabled {@link RuleStatus} for a {@link Rule}. + * The enabled rule statuses are {@link RuleStatus#UNINITIALIZED}, {@link RuleStatus#IDLE} and + * {@link RuleStatus#RUNNING}. + * The disabled rule status is {@link RuleStatus#DISABLED}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@code true} when the {@link RuleStatus} is one of the {@link RuleStatus#UNINITIALIZED}, + * {@link RuleStatus#IDLE} and {@link RuleStatus#RUNNING}, {@code false} when it is + * {@link RuleStatus#DISABLED} and {@code null} when it is not available. + */ + @Nullable + Boolean isEnabled(String ruleUID); + + /** + * This method is used for changing enabled state of the {@link Rule}. + * The enabled rule statuses are {@link RuleStatus#UNINITIALIZED}, {@link RuleStatus#IDLE} and + * {@link RuleStatus#RUNNING}. + * The disabled rule status is {@link RuleStatus#DISABLED}. + * + * @param uid the unique identifier of the {@link Rule}. + * @param isEnabled a new enabled / disabled state of the {@link Rule}. + */ + void setEnabled(String uid, boolean isEnabled); + + /** + * This method gets {@link RuleStatusInfo} of the specified {@link Rule}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@link RuleStatusInfo} object containing status of the looking {@link Rule} or null when a rule with + * specified UID does not exists. + */ + @Nullable + RuleStatusInfo getStatusInfo(String ruleUID); + + /** + * Utility method which gets {@link RuleStatus} of the specified {@link Rule}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@link RuleStatus} object containing status of the looking {@link Rule} or null when a rule with + * specified UID does not exists. + */ + @Nullable + RuleStatus getStatus(String ruleUID); + + /** + * The method skips the triggers and the conditions and directly executes the actions of the rule. + * This should always be possible unless an action has a mandatory input that is linked to a trigger. + * In that case the action is skipped and the rule engine continues execution of rest actions. + * + * @param ruleUID id of the rule whose actions have to be executed. + */ + void runNow(String uid); + + /** + * Same as {@link #runNow(String)} with the additional option to enable/disable evaluation of + * conditions defined in the target rule. The context can be set here, too, but also might be {@code null}. + * + * @param ruleUID id of the rule whose actions have to be executed. + * @param considerConditions if {@code true} the conditions of the rule will be checked. + * @param context the context that is passed to the conditions and the actions of the rule. + */ + void runNow(String uid, boolean considerConditions, @Nullable Map context); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java new file mode 100644 index 00000000000..8b8fd73d783 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Rule.java @@ -0,0 +1,158 @@ +/** + * 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; + +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.eclipse.smarthome.core.common.registry.Identifiable; +import org.openhab.core.automation.template.RuleTemplate; + +/** + * An automation Rule is built from {@link Module}s and consists of three parts: + *

    + *
  • Triggers: a list of {@link Trigger} modules. Each {@link Trigger} from this list + * can start the evaluation of the Rule. A Rule with an empty list of {@link Trigger}s can + * only be triggered through the {@link RuleRegistry#runNow(String, boolean, java.util.Map)} method, + * or directly executed with the {@link RuleRegistry#runNow(String)} method. + *
  • Conditions: a list of {@link Condition} modules. When a Rule is triggered, the + * evaluation of the Rule {@link Condition}s will determine if the Rule will be executed. + * A Rule will be executed only when all it's {@link Condition}s are satisfied. If the {@link Condition}s + * list is empty, the Rule is considered satisfied. + *
  • Actions: a list of {@link Action} modules. These modules determine the actions that + * will be performed when a Rule is executed. + *
+ * Additionally, Rules can have tags - non-hierarchical keywords or terms for describing them. + * They can help the user to classify or label the Rules, and to filter and search them. + * + * @author Kai Kreuzer - Initial Contribution + */ +@NonNullByDefault +public interface Rule extends Identifiable { + + /** + * This method is used to obtain the identifier of the Rule. It can be specified by the {@link Rule}'s + * creator, or randomly generated. + * + * @return an identifier of this {@link Rule}. Can't be {@code null}. + */ + @Override + String getUID(); + + /** + * This method is used to obtain the {@link RuleTemplate} identifier of the template the {@link Rule} was created + * from. It will be used 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. If a {@link Rule} has not been + * created from a template, or has been successfully resolved by the {@link RuleRegistry}, this method will return + * {@code null}. + * + * @return the identifier of the {@link Rule}'s {@link RuleTemplate}, or {@code null} if the {@link Rule} has not + * been created from a template, or has been successfully resolved by the {@link RuleRegistry}. + */ + @Nullable + String getTemplateUID(); + + /** + * This method is used to obtain the {@link Rule}'s human-readable name. + * + * @return the {@link Rule}'s human-readable name, or {@code null}. + */ + @Nullable + String getName(); + + /** + * This method is used to obtain the {@link Rule}'s assigned tags. + * + * @return the {@link Rule}'s assigned tags. + */ + Set getTags(); + + /** + * This method is used to obtain the human-readable description of the purpose and consequences of the + * {@link Rule}'s execution. + * + * @return the {@link Rule}'s human-readable description, or {@code null}. + */ + @Nullable + String getDescription(); + + /** + * This method is used to obtain the {@link Rule}'s {@link Visibility}. + * + * @return the {@link Rule}'s {@link Visibility} value. + */ + Visibility getVisibility(); + + /** + * This method is used to obtain the {@link Rule}'s {@link Configuration}. + * + * @return current configuration values, or an empty {@link Configuration}. + */ + Configuration getConfiguration(); + + /** + * This method is used to obtain the {@link List} with {@link ConfigDescriptionParameter}s defining meta info for + * configuration properties of the {@link Rule}. + * + * @return a {@link List} of {@link ConfigDescriptionParameter}s. + */ + List getConfigurationDescriptions(); + + /** + * This method is used to get the conditions participating in {@link Rule}. + * + * @return a list with the conditions that belong to this {@link Rule}. + */ + List getConditions(); + + /** + * This method is used to get the actions participating in {@link Rule}. + * + * @return a list with the actions that belong to this {@link Rule}. + */ + List getActions(); + + /** + * This method is used to get the triggers participating in {@link Rule}. + * + * @return a list with the triggers that belong to this {@link Rule}. + */ + List getTriggers(); + + /** + * Obtains the modules of the {@link Rule}. + * + * @return the modules of the {@link Rule} or empty list if the {@link Rule} has no modules. + */ + List getModules(); + + /** + * This method is used to get a {@link Module} participating in {@link Rule} + * + * @param moduleId specifies the id of a module belonging to this {@link Rule}. + * @return module with specified id or {@code null} if it does not belong to this {@link Rule}. + */ + default @Nullable Module getModule(String moduleId) { + for (Module module : getModules()) { + if (module.getId().equals(moduleId)) { + return module; + } + } + return null; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleManager.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleManager.java new file mode 100644 index 00000000000..f4adcf36924 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleManager.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; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; + +/** + * This class is responsible to provide a {@link RegistryChangeListener} logic. A instance of it is added to + * {@link RuleRegistry} service, to listen for changes when a single {@link Rule} has been added, updated, enabled, + * disabled or removed and to involve Rule Engine to process these changes. Also to send a {@code run} command + * for a single {@link Rule} to the Rule Engine. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public interface RuleManager { + + /** + * This method gets enabled {@link RuleStatus} for a {@link Rule}. + * The enabled rule statuses are {@link RuleStatus#UNINITIALIZED}, {@link RuleStatus#IDLE} and + * {@link RuleStatus#RUNNING}. + * The disabled rule status is {@link RuleStatus#DISABLED}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@code true} when the {@link RuleStatus} is one of the {@link RuleStatus#UNINITIALIZED}, + * {@link RuleStatus#IDLE} and {@link RuleStatus#RUNNING}, {@code false} when it is + * {@link RuleStatus#DISABLED} and {@code null} when it is not available. + */ + @Nullable + Boolean isEnabled(String ruleUID); + + /** + * This method is used for changing enabled state of the {@link Rule}. + * The enabled rule statuses are {@link RuleStatus#UNINITIALIZED}, {@link RuleStatus#IDLE} and + * {@link RuleStatus#RUNNING}. + * The disabled rule status is {@link RuleStatus#DISABLED}. + * + * @param uid the unique identifier of the {@link Rule}. + * @param isEnabled a new enabled / disabled state of the {@link Rule}. + */ + void setEnabled(String uid, boolean isEnabled); + + /** + * This method gets {@link RuleStatusInfo} of the specified {@link Rule}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@link RuleStatusInfo} object containing status of the looking {@link Rule} or null when a rule with + * specified UID does not exists. + */ + @Nullable + RuleStatusInfo getStatusInfo(String ruleUID); + + /** + * Utility method which gets {@link RuleStatus} of the specified {@link Rule}. + * + * @param ruleUID UID of the {@link Rule} + * @return {@link RuleStatus} object containing status of the looking {@link Rule} or null when a rule with + * specified UID does not exists. + */ + @Nullable + RuleStatus getStatus(String ruleUID); + + /** + * The method skips the triggers and the conditions and directly executes the actions of the rule. + * This should always be possible unless an action has a mandatory input that is linked to a trigger. + * In that case the action is skipped and the rule engine continues execution of rest actions. + * + * @param ruleUID id of the rule whose actions have to be executed. + */ + void runNow(String uid); + + /** + * Same as {@link #runNow(String)} with the additional option to enable/disable evaluation of + * conditions defined in the target rule. The context can be set here, too, but also might be {@code null}. + * + * @param ruleUID id of the rule whose actions have to be executed. + * @param considerConditions if {@code true} the conditions of the rule will be checked. + * @param context the context that is passed to the conditions and the actions of the rule. + */ + void runNow(String uid, boolean considerConditions, @Nullable Map context); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RulePredicates.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RulePredicates.java new file mode 100644 index 00000000000..c3eb2457570 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RulePredicates.java @@ -0,0 +1,184 @@ +/** + * 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; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * This class add support for prefixes for {@link Rule} UIDs and provide default predicates for prefixes and tags. + * + * @author Victor Toni - initial contribution + * + */ +public class RulePredicates { + + /** + * Constant defining separator between prefix and UID. + */ + public static final String PREFIX_SEPARATOR = ":"; + + /** + * Gets the prefix of the {@link Rule}'s UID, if any exist. The UID is either set automatically when the + * {@link Rule} is added or by the creating party. It's an optional property. + *
+ *
+ * Implementation note: + *
+ * The name space is part of the UID and the prefix thereof. + *
+ * If the UID does not contain a {@link PREFIX_SEPARATOR} {@code null} will be returned. + *
+ * If the UID does contain a {@link PREFIX_SEPARATOR} the prefix until the first occurrence will be returned. + *
+ * If the prefix would have a zero length {@code null} will be returned. + * + * @return prefix of this {@link Rule}, or {@code null} if no prefix or an empty prefix is found. + */ + public static String getPrefix(Rule rule) { + if (null != rule) { + final String uid = rule.getUID(); + final int index = uid.indexOf(PREFIX_SEPARATOR); + // only when a delimiter was found and the prefix is not empty + if (0 < index) { + return uid.substring(0, index); + } + } + return null; + } + + /** + * Creates a {@link Predicate} which can be used to filter {@link Rule}s for a given prefix or {@code null} prefix. + * + * @param prefix to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasPrefix(final String prefix) { + if (null == prefix) { + return r -> null == getPrefix(r); + } else { + return r -> prefix.equals(getPrefix(r)); + } + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s for any of the given prefixes and even + * {@code null} prefix. + * + * @param prefixes to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasAnyOfPrefixes(String... prefixes) { + final HashSet namespaceSet = new HashSet<>(prefixes.length); + for (final String namespace : prefixes) { + namespaceSet.add(namespace); + } + + // this will even work for null namepace + return r -> namespaceSet.contains(getPrefix(r)); + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s with one or more tags. + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasTags() { + // everything with a tag is matching + // Rule.getTags() is never null + return r -> 0 < r.getTags().size(); + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s without tags. + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasNoTags() { + // Rule.getTags() is never null + return r -> r.getTags().isEmpty(); + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s with all given tags or no tags at all. + * All given tags must match, (the matched {@code Rule} might contain more). + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasAllTags(final Collection tags) { + if (tags == null || tags.isEmpty()) { + return (Predicate) r -> true; + } else { + final Set tagSet = new HashSet<>(tags); + + // everything containing _all_ given tags is matching + // (Rule might might have more tags than the given set) + return r -> r.getTags().containsAll(tagSet); + } + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s for all given tags or no tags at all. + * All given tags must match, (the matched {@code Rule} might contain more). + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasAllTags(final String... tags) { + return hasAllTags(tags == null ? null : Arrays.asList(tags)); + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s for any of the given tags or {@link Rule}s + * without tags. + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasAnyOfTags(final Collection tags) { + if (null == tags || tags.isEmpty()) { + // everything without a tag is matching + return hasNoTags(); + } else { + final Set tagSet = new HashSet<>(tags); + + // everything containing _any_ of the given tags is matching (more than one tag might match) + // if the collections are NOT disjoint, they have something in common + return r -> !Collections.disjoint(r.getTags(), tagSet); + } + } + + /** + * Creates a {@link Predicate} which can be used to match {@link Rule}s for any of the given tags or {@link Rule}s + * without tags. + * + * @param tags to search for. + * @return created {@link Predicate}. + */ + public static Predicate hasAnyOfTags(final String... tags) { + if (null == tags || 0 == tags.length) { + // everything without a tag is matching + return hasNoTags(); + } else { + return hasAnyOfTags(Arrays.asList(tags)); + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleProvider.java new file mode 100644 index 00000000000..759800ef864 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleProvider.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; + +import org.eclipse.smarthome.core.common.registry.Provider; + +/** + * This class is responsible for providing {@link Rule}s. {@link RuleProvider}s are tracked by the {@link RuleRegistry} + * service, which collect all rules from different providers of the same type. + * + * @author Kai Kreuzer - Initial contribution + */ +public interface RuleProvider extends Provider { + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.java new file mode 100644 index 00000000000..f2ef6e8844a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleRegistry.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; + +import java.util.Collection; + +import org.eclipse.smarthome.core.common.registry.Registry; + +/** + * The {@link RuleRegistry} provides basic functionality for managing {@link Rule}s. + * It can be used to + *
    + *
  • Add Rules with the {@link Registry#add(Object)} method.
  • + *
  • Get the existing rules with the {@link #getByTag(String)}, {@link #getByTags(String[])} methods.
  • + *
  • Update the existing rules with the {@link Registry#update(Object)} method.
  • + *
  • Remove Rules with the {@link Registry#remove(Object)} method.
  • + *
  • Manage the state (enabled or disabled) of the Rules: + *
      + *
    • A newly added Rule is always enabled.
    • + *
    • To check a Rule's state, use the {@link #isEnabled(String)} method.
    • + *
    • To change a Rule's state, use the {@link #setEnabled(String, boolean)} method.
    • + *
    + *
  • + *
+ *

+ * The {@link RuleRegistry} manages the status of the Rules: + *

    + *
  • To check a Rule's status info, use the {@link #getStatusInfo(String)} method.
  • + *
  • The status of a Rule enabled with {@link #setEnabled(String, boolean)}, is first set to + * {@link RuleStatus#UNINITIALIZED}.
  • + *
  • After a Rule is enabled, a verification procedure is initiated. If the verification of the modules IDs, + * connections between modules and configuration values of the modules is successful, and the module handlers are + * correctly set, the status is set to {@link RuleStatus#IDLE}.
  • + *
  • If some of the module handlers disappear, the Rule will become {@link RuleStatus#UNINITIALIZED} again.
  • + *
  • If one of the Rule's Triggers is triggered, the Rule becomes {@link RuleStatus#RUNNING}. + * When the execution is complete, it will become {@link RuleStatus#IDLE} again.
  • + *
  • If a Rule is disabled with {@link #setEnabled(String, boolean)}, it's status is set to + * {@link RuleStatus#DISABLED}.
  • + *
+ * + * @author Yordan Mihaylov - Initial Contribution + */ +public interface RuleRegistry extends Registry { + + /** + * This method is used to register a {@link Rule} into the {@link RuleRegistry}. First the {@link Rule} become + * {@link RuleStatus#UNINITIALIZED}. + * Then verification procedure will be done and the Rule become {@link RuleStatus#IDLE}. + * If the verification fails, the Rule will stay {@link RuleStatus#UNINITIALIZED}. + * + * @param rule a {@link Rule} instance which have to be added into the {@link RuleRegistry}. + * @return a copy of the added {@link Rule} + * @throws IllegalArgumentException when a rule with the same UID already exists or some of the conditions or + * actions has wrong format of input reference. + * @throws IllegalStateException when the RuleManagedProvider is unavailable. + */ + @Override + public Rule add(Rule rule); + + /** + * Gets a collection of {@link Rule}s which shares same tag. + * + * @param tag specifies a tag that will filter the rules. + * @return collection of {@link Rule}s having specified tag. + */ + public Collection getByTag(String tag); + + /** + * Gets a collection of {@link Rule}s which has specified tags. + * + * @param tags specifies tags that will filter the rules. + * @return collection of {@link Rule}s having specified tags. + */ + public Collection getByTags(String... tags); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatus.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatus.java new file mode 100644 index 00000000000..0f04abbd82a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatus.java @@ -0,0 +1,96 @@ +/** + * 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; + +/** + * This enumeration is used to present the main status of a {@link Rule}. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Rule Status transitions
From/To{@link #UNINITIALIZED}{@link #INITIALIZING}{@link #IDLE}{@link #RUNNING}
{@link #UNINITIALIZED}N/A + *
  • Add: Rule, ModuleHandler, ModuleType, Template
  • + *
  • Update: Rule
  • N/AN/A
    {@link #INITIALIZING}Resolving fails, Disable ruleN/AResolving succeedsN/A
    {@link #IDLE} + *
  • Remove: Rule, ModuleHandler
  • + *
  • Update: ModuleType
  • + *
  • Disable: Rule
  • N/AN/A + *
  • Triggered
  • + *
  • {@link RuleRegistry#runNow(String) runNow}
  • {@link #RUNNING} + *
  • Remove: Rule, ModuleHandler
  • + *
  • Update: ModuleType
  • + *
  • Disable: Rule
  • N/AExecution finishedN/A
    + * + * @author Yordan Mihaylov - Initial contribution + * @author Kai Kreuzer - Refactored to match ThingStatus implementation + * @author Ana Dimova - add java doc + */ +public enum RuleStatus { + UNINITIALIZED(1), + INITIALIZING(2), + IDLE(3), + RUNNING(4); + + private final int value; + + private RuleStatus(final int newValue) { + value = newValue; + } + + /** + * Gets the value of a rule status. + * + * @return the value + */ + public int getValue() { + return value; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java new file mode 100644 index 00000000000..3efc479941e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusDetail.java @@ -0,0 +1,107 @@ +/** + * 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; + +/** + * This enumeration is used to represent a detail of a {@link RuleStatus}. It can be considered as a sub-status. + * It shows the specific reasons why the status of the rule is like as is. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Rule Status Details
    Detail/Status{@link RuleStatus#UNINITIALIZED UNINITIALIZED}{@link RuleStatus#INITIALIZING INITIALIZING}{@link RuleStatus#IDLE IDLE}{@link RuleStatus#RUNNING RUNNING}
    {@link #NONE}Initial StateResolving startedSuccessfully resolvedRunning
    {@link #CONFIGURATION_ERROR}Resolving failedN/AN/AN/A
    {@link #HANDLER_INITIALIZING_ERROR}Resolving failedN/AN/AN/A
    {@link #HANDLER_MISSING_ERROR}Resolving failedN/AN/AN/A
    {@link #TEMPLATE_MISSING_ERROR}Resolving failedN/AN/AN/A
    {@link #INVALID_RULE}Resolving failedN/AN/AN/A
    {@link #DISABLED}DisabledN/AN/AN/A
    + * + * @author Yordan Mihaylov - Initial contribution + * @author Kai Kreuzer - Refactored to match ThingStatusDetail implementation + * @author Ana Dimova - add java doc + */ +public enum RuleStatusDetail { + NONE(0), + HANDLER_MISSING_ERROR(1), + HANDLER_INITIALIZING_ERROR(2), + CONFIGURATION_ERROR(3), + TEMPLATE_MISSING_ERROR(4), + INVALID_RULE(5), + DISABLED(6); + + private final int value; + + private RuleStatusDetail(final int newValue) { + value = newValue; + } + + /** + * Gets the value of the status detail. + * + * @return the value of the status detail. + */ + public int getValue() { + return value; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusInfo.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusInfo.java new file mode 100644 index 00000000000..78c5dc3a6e5 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/RuleStatusInfo.java @@ -0,0 +1,147 @@ +/** + * 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; + +/** + * This class is used to present status of a rule. The status consists of three parts: + * The main status, a status detail and a string description. + * + * @author Yordan Mihaylov - Initial contribution + * @author Kai Kreuzer - Refactored to match ThingStatusInfo implementation + */ +public class RuleStatusInfo { + + private RuleStatus status; + private RuleStatusDetail statusDetail; + private String description; + + /** + * Default constructor for deserialization e.g. by Gson. + */ + protected RuleStatusInfo() { + } + + /** + * Constructs a status info. + * + * @param status the status (must not be null) + * @throws IllegalArgumentException if status is null + */ + public RuleStatusInfo(RuleStatus status) throws IllegalArgumentException { + this(status, RuleStatusDetail.NONE); + } + + /** + * Constructs a status info. + * + * @param status the status (must not be null) + * @param statusDetail the detail of the status (must not be null) + * @throws IllegalArgumentException if status or status detail is null + */ + public RuleStatusInfo(RuleStatus status, RuleStatusDetail statusDetail) throws IllegalArgumentException { + this(status, statusDetail, null); + } + + /** + * Constructs a status info. + * + * @param status the status (must not be null) + * @param statusDetail the detail of the status (must not be null) + * @param description the description of the status + * @throws IllegalArgumentException if status or status detail is null + */ + public RuleStatusInfo(RuleStatus status, RuleStatusDetail statusDetail, String description) + throws IllegalArgumentException { + if (status == null) { + throw new IllegalArgumentException("Thing status must not be null"); + } + if (statusDetail == null) { + throw new IllegalArgumentException("Thing status detail must not be null"); + } + this.status = status; + this.statusDetail = statusDetail; + this.description = description; + } + + /** + * Gets the status itself. + * + * @return the status (not null) + */ + public RuleStatus getStatus() { + return status; + } + + /** + * Gets the detail of the status. + * + * @return the status detail (not null) + */ + public RuleStatusDetail getStatusDetail() { + return statusDetail; + } + + /** + * Gets the description of the status. + * + * @return the description + */ + public String getDescription() { + return description; + } + + @Override + public String toString() { + boolean hasDescription = getDescription() != null && !getDescription().isEmpty(); + return getStatus() + (getStatusDetail() == RuleStatusDetail.NONE ? "" : " (" + getStatusDetail() + ")") + + (hasDescription ? ": " + getDescription() : ""); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((status == null) ? 0 : status.hashCode()); + result = prime * result + ((statusDetail == null) ? 0 : statusDetail.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + RuleStatusInfo other = (RuleStatusInfo) obj; + if (description == null) { + if (other.description != null) { + return false; + } + } else if (!description.equals(other.description)) { + return false; + } + if (status != other.status) { + return false; + } + if (statusDetail != other.statusDetail) { + return false; + } + return true; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/StatusInfoCallback.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/StatusInfoCallback.java new file mode 100644 index 00000000000..4b1f397569b --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/StatusInfoCallback.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; + +/** + * This interface is used by {@link RuleRegistry} implementation to be notified of changes related to statuses of rules. + * + * @author Yordan Mihaylov - initial contribution + */ +public interface StatusInfoCallback { + + /** + * The method is called when the rule has update of its status. + * + * @param ruleUID UID of the {@link Rule} + * @param statusInfo new status info releated to the {@link Rule} + */ + void statusInfoChanged(String ruleUID, RuleStatusInfo statusInfo); +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Trigger.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Trigger.java new file mode 100644 index 00000000000..6e24bc332e8 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Trigger.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.openhab.core.automation; + +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; + +/** + * This interface represents automation {@code Trigger} modules which define what phenomenon will start the execution + * of the {@link Rule} and trigger it when an exact phenomenon occurs. Each of them can independently trigger the rule. + *

    + * The triggers do not receive information from other modules of the Rule so they don't have {@link Input}s. + *

    + * The triggers can be configured. + *

    + * The triggers have {@link Output}s to provide information about the occurred phenomenon to the {@link Condition}s and + * {@link Action}s of the Rule. + *

    + * Building elements of conditions as {@link ConfigDescriptionParameter}s and {@link Input}s are defined by + * {@link TriggerType}. + *

    + * Trigger modules are placed in triggers section of the {@link Rule} definition. + * + * @see Module + * @author Yordan Mihaylov - Initial Contribution + */ +public interface Trigger extends Module { + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Visibility.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Visibility.java new file mode 100644 index 00000000000..704f7b895bd --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/Visibility.java @@ -0,0 +1,40 @@ +/** + * 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; + +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.type.ModuleType; + +/** + * Defines visibility values of {@link Rule}s, {@link ModuleType}s and {@link Template}s. + * + * @author Yordan Mihaylov - Initial Contribution + * + */ +public enum Visibility { + /** + * The UI has always to show an object with such visibility. + */ + VISIBLE, + + /** + * The UI has always to hide an object with such visibility. + */ + HIDDEN, + + /** + * The UI has to show an object with such visibility only to experts. + */ + EXPERT + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInput.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInput.java new file mode 100644 index 00000000000..978e16fdfd3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInput.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.annotation; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Input parameter for an action module + * + * @author Stefan Triller - initial contribution + */ +@Repeatable(ActionInputs.class) +@Retention(RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ActionInput { + + String name(); + + String type() default ""; + + String label() default ""; + + String description() default ""; + + String[] tags() default {}; + + boolean required() default false; + + String reference() default ""; + + String defaultValue() default ""; +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInputs.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInputs.java new file mode 100644 index 00000000000..594fe54c57a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionInputs.java @@ -0,0 +1,30 @@ +/** + * 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.annotation; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Input parameter wrapper for an action module + * + * @author Stefan Triller - initial contribution + */ +@Retention(RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ActionInputs { + ActionInput[] value(); +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutput.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutput.java new file mode 100644 index 00000000000..1745d8a94ec --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutput.java @@ -0,0 +1,45 @@ +/** + * 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.annotation; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Output parameter for an action module + * + * @author Stefan Triller - initial contribution + */ +@Repeatable(ActionOutputs.class) +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface ActionOutput { + + String name(); + + String type(); + + String label() default ""; + + String description() default ""; + + String[] tags() default {}; + + String reference() default ""; + + String defaultValue() default ""; +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutputs.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutputs.java new file mode 100644 index 00000000000..638de9ad901 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionOutputs.java @@ -0,0 +1,30 @@ +/** + * 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.annotation; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Output parameter wrapper for an action module + * + * @author Stefan Triller - initial contribution + */ +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface ActionOutputs { + ActionOutput[] value(); +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionScope.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionScope.java new file mode 100644 index 00000000000..dd7ff084a16 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/ActionScope.java @@ -0,0 +1,31 @@ +/** + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Scope definition for an action module + * + * @author Stefan Triller - initial contribution + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ActionScope { + + String name(); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/RuleAction.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/RuleAction.java new file mode 100644 index 00000000000..e371659a930 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/annotation/RuleAction.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.openhab.core.automation.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.openhab.core.automation.Visibility; + +/** + * Marker annotation for an action module + * + * @author Stefan Triller - initial contribution + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RuleAction { + + String label(); + + String description() default ""; + + String[] tags() default {}; + + Visibility visibility() default Visibility.VISIBLE; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTO.java new file mode 100644 index 00000000000..120186b46d5 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTO.java @@ -0,0 +1,26 @@ +/** + * 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.dto; + +import java.util.Map; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ActionDTO extends ModuleDTO { + + public Map inputs; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTOMapper.java new file mode 100644 index 00000000000..ac656aaf8d1 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionDTOMapper.java @@ -0,0 +1,67 @@ +/** + * 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.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.dto.ActionDTO; +import org.openhab.core.automation.util.ModuleBuilder; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Kai Kreuzer - Changed to using ModuleBuilder + */ +public class ActionDTOMapper extends ModuleDTOMapper { + + public static ActionDTO map(final Action action) { + final ActionDTO actionDto = new ActionDTO(); + fillProperties(action, actionDto); + actionDto.inputs = action.getInputs(); + return actionDto; + } + + public static Action mapDto(final ActionDTO actionDto) { + return ModuleBuilder.createAction().withId(actionDto.id).withTypeUID(actionDto.type) + .withConfiguration(new Configuration(actionDto.configuration)).withInputs(actionDto.inputs) + .withLabel(actionDto.label).withDescription(actionDto.description).build(); + } + + public static List map(final Collection actions) { + if (actions == null) { + return null; + } + final List dtos = new ArrayList(actions.size()); + for (final Action action : actions) { + dtos.add(map(action)); + } + return dtos; + } + + public static List mapDto(final Collection dtos) { + if (dtos == null) { + return null; + } + final List actions = new ArrayList(dtos.size()); + for (final ActionDTO dto : dtos) { + actions.add(mapDto(dto)); + } + return actions; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTO.java new file mode 100644 index 00000000000..4199c460e7c --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTO.java @@ -0,0 +1,30 @@ +/** + * 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.dto; + +import java.util.List; + +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ActionTypeDTO extends ModuleTypeDTO { + + public List inputs; + public List outputs; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java new file mode 100644 index 00000000000..34de98275f8 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ActionTypeDTOMapper.java @@ -0,0 +1,78 @@ +/** + * 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.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.dto.ActionTypeDTO; +import org.openhab.core.automation.dto.CompositeActionTypeDTO; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.CompositeActionType; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Ana Dimova - extends Action Module type DTOs with composites + */ +public class ActionTypeDTOMapper extends ModuleTypeDTOMapper { + + public static ActionTypeDTO map(final ActionType actionType) { + return map(actionType, new ActionTypeDTO()); + } + + public static CompositeActionTypeDTO map(final CompositeActionType actionType) { + final CompositeActionTypeDTO actionTypeDto = map(actionType, new CompositeActionTypeDTO()); + actionTypeDto.children = ActionDTOMapper.map(actionType.getChildren()); + return actionTypeDto; + } + + public static ActionType map(CompositeActionTypeDTO actionTypeDto) { + if (actionTypeDto.children == null || actionTypeDto.children.isEmpty()) { + return new ActionType(actionTypeDto.uid, ConfigDescriptionDTOMapper.map(actionTypeDto.configDescriptions), + actionTypeDto.label, actionTypeDto.description, actionTypeDto.tags, actionTypeDto.visibility, + actionTypeDto.inputs, actionTypeDto.outputs); + } else { + return new CompositeActionType(actionTypeDto.uid, + ConfigDescriptionDTOMapper.map(actionTypeDto.configDescriptions), actionTypeDto.label, + actionTypeDto.description, actionTypeDto.tags, actionTypeDto.visibility, actionTypeDto.inputs, + actionTypeDto.outputs, ActionDTOMapper.mapDto(actionTypeDto.children)); + } + } + + public static List map(final Collection types) { + if (types == null) { + return null; + } + final List dtos = new ArrayList(types.size()); + for (final ActionType type : types) { + if (type instanceof CompositeActionType) { + dtos.add(map((CompositeActionType) type)); + } else { + dtos.add(map(type)); + } + } + return dtos; + } + + private static T map(final ActionType actionType, final T actionTypeDto) { + fillProperties(actionType, actionTypeDto); + actionTypeDto.inputs = actionType.getInputs(); + actionTypeDto.outputs = actionType.getOutputs(); + return actionTypeDto; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeActionTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeActionTypeDTO.java new file mode 100644 index 00000000000..a598ee299b8 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeActionTypeDTO.java @@ -0,0 +1,26 @@ +/** + * 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.dto; + +import java.util.List; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Ana Dimova - Initial contribution + * + */ +public class CompositeActionTypeDTO extends ActionTypeDTO { + + public List children; +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeConditionTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeConditionTypeDTO.java new file mode 100644 index 00000000000..2f8c247e778 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeConditionTypeDTO.java @@ -0,0 +1,27 @@ +/** + * 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.dto; + +import java.util.List; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Ana Dimova - Initial contribution + * + */ +public class CompositeConditionTypeDTO extends ConditionTypeDTO { + + public List children; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeTriggerTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeTriggerTypeDTO.java new file mode 100644 index 00000000000..f098362921a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/CompositeTriggerTypeDTO.java @@ -0,0 +1,27 @@ +/** + * 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.dto; + +import java.util.List; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Ana Dimova - Initial contribution + * + */ +public class CompositeTriggerTypeDTO extends TriggerTypeDTO { + + public List children; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTO.java new file mode 100644 index 00000000000..69ab428de46 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTO.java @@ -0,0 +1,26 @@ +/** + * 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.dto; + +import java.util.Map; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ConditionDTO extends ModuleDTO { + + public Map inputs; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTOMapper.java new file mode 100644 index 00000000000..49811885730 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionDTOMapper.java @@ -0,0 +1,66 @@ +/** + * 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.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.dto.ConditionDTO; +import org.openhab.core.automation.util.ModuleBuilder; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Kai Kreuzer - Changed to using ModuleBuilder + */ +public class ConditionDTOMapper extends ModuleDTOMapper { + + public static ConditionDTO map(final Condition condition) { + final ConditionDTO conditionDto = new ConditionDTO(); + fillProperties(condition, conditionDto); + conditionDto.inputs = condition.getInputs(); + return conditionDto; + } + + public static Condition mapDto(final ConditionDTO conditionDto) { + return ModuleBuilder.createCondition().withId(conditionDto.id).withTypeUID(conditionDto.type) + .withConfiguration(new Configuration(conditionDto.configuration)).withInputs(conditionDto.inputs) + .withLabel(conditionDto.label).withDescription(conditionDto.description).build(); + } + + public static List map(final List conditions) { + if (conditions == null) { + return null; + } + final List dtos = new ArrayList(conditions.size()); + for (final Condition action : conditions) { + dtos.add(map(action)); + } + return dtos; + } + + public static List mapDto(final List dtos) { + if (dtos == null) { + return null; + } + final List conditions = new ArrayList(dtos.size()); + for (final ConditionDTO dto : dtos) { + conditions.add(mapDto(dto)); + } + return conditions; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTO.java new file mode 100644 index 00000000000..35f741f56f9 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTO.java @@ -0,0 +1,28 @@ +/** + * 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.dto; + +import java.util.List; + +import org.openhab.core.automation.type.Input; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ConditionTypeDTO extends ModuleTypeDTO { + + public List inputs; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java new file mode 100644 index 00000000000..328e5798ba3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ConditionTypeDTOMapper.java @@ -0,0 +1,78 @@ +/** + * 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.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.dto.CompositeConditionTypeDTO; +import org.openhab.core.automation.dto.ConditionTypeDTO; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.ConditionType; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Ana Dimova - extends Condition Module type DTOs with composites + */ +public class ConditionTypeDTOMapper extends ModuleTypeDTOMapper { + + public static ConditionTypeDTO map(final ConditionType conditionType) { + return map(conditionType, new ConditionTypeDTO()); + } + + public static CompositeConditionTypeDTO map(final CompositeConditionType conditionType) { + final CompositeConditionTypeDTO conditionTypeDto = map(conditionType, new CompositeConditionTypeDTO()); + conditionTypeDto.children = ConditionDTOMapper.map(conditionType.getChildren()); + return conditionTypeDto; + } + + public static ConditionType map(CompositeConditionTypeDTO conditionTypeDto) { + if (conditionTypeDto.children == null || conditionTypeDto.children.isEmpty()) { + return new ConditionType(conditionTypeDto.uid, + ConfigDescriptionDTOMapper.map(conditionTypeDto.configDescriptions), conditionTypeDto.label, + conditionTypeDto.description, conditionTypeDto.tags, conditionTypeDto.visibility, + conditionTypeDto.inputs); + } else { + return new CompositeConditionType(conditionTypeDto.uid, + ConfigDescriptionDTOMapper.map(conditionTypeDto.configDescriptions), conditionTypeDto.label, + conditionTypeDto.description, conditionTypeDto.tags, conditionTypeDto.visibility, + conditionTypeDto.inputs, ConditionDTOMapper.mapDto(conditionTypeDto.children)); + } + } + + public static List map(final Collection types) { + if (types == null) { + return null; + } + final List dtos = new ArrayList(types.size()); + for (final ConditionType type : types) { + if (type instanceof CompositeConditionType) { + dtos.add(map((CompositeConditionType) type)); + } else { + dtos.add(map(type)); + } + } + return dtos; + } + + private static T map(final ConditionType conditionType, final T conditionTypeDto) { + fillProperties(conditionType, conditionTypeDto); + conditionTypeDto.inputs = conditionType.getInputs(); + return conditionTypeDto; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTO.java new file mode 100644 index 00000000000..47d0afc8254 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTO.java @@ -0,0 +1,30 @@ +/** + * 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.dto; + +import java.util.Map; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ModuleDTO { + + public String id; + public String label; + public String description; + public Map configuration; + public String type; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTOMapper.java new file mode 100644 index 00000000000..44a0ab5416a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleDTOMapper.java @@ -0,0 +1,33 @@ +/** + * 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.dto; + +import org.openhab.core.automation.Module; +import org.openhab.core.automation.dto.ModuleDTO; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ModuleDTOMapper { + + protected static void fillProperties(final Module from, final ModuleDTO to) { + to.id = from.getId(); + to.label = from.getLabel(); + to.description = from.getDescription(); + to.configuration = from.getConfiguration().getProperties(); + to.type = from.getTypeUID(); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTO.java new file mode 100644 index 00000000000..f20dcf3f006 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTO.java @@ -0,0 +1,35 @@ +/** + * 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.dto; + +import java.util.List; +import java.util.Set; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionParameterDTO; +import org.openhab.core.automation.Visibility; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ModuleTypeDTO { + + public String uid; + public Visibility visibility; + public Set tags; + public String label; + public String description; + public List configDescriptions; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTOMapper.java new file mode 100644 index 00000000000..87e983babf7 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/ModuleTypeDTOMapper.java @@ -0,0 +1,34 @@ +/** + * 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.dto; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.dto.ModuleTypeDTO; +import org.openhab.core.automation.type.ModuleType; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class ModuleTypeDTOMapper { + + protected static void fillProperties(final ModuleType from, final ModuleTypeDTO to) { + to.uid = from.getUID(); + to.visibility = from.getVisibility(); + to.tags = from.getTags(); + to.label = from.getLabel(); + to.description = from.getDescription(); + to.configDescriptions = ConfigDescriptionDTOMapper.mapParameters(from.getConfigurationDescriptions()); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.java new file mode 100644 index 00000000000..e0e9b9fbf5d --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTO.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.openhab.core.automation.dto; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionParameterDTO; +import org.openhab.core.automation.Visibility; + +/** + * This is a data transfer object that is used to serialize rules. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class RuleDTO { + + public List triggers; + public List conditions; + public List actions; + public Map configuration; + public List configDescriptions; + public String templateUID; + public String uid; + public String name; + public Set tags; + public Visibility visibility; + public String description; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.java new file mode 100644 index 00000000000..dbdd1a30f08 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleDTOMapper.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.dto; + +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.util.RuleBuilder; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Kai Kreuzer - Changed to using RuleBuilder + */ +public class RuleDTOMapper { + + public static RuleDTO map(final Rule rule) { + final RuleDTO ruleDto = new RuleDTO(); + fillProperties(rule, ruleDto); + return ruleDto; + } + + public static Rule map(final RuleDTO ruleDto) { + return RuleBuilder.create(ruleDto.uid).withActions(ActionDTOMapper.mapDto(ruleDto.actions)) + .withConditions(ConditionDTOMapper.mapDto(ruleDto.conditions)) + .withTriggers(TriggerDTOMapper.mapDto(ruleDto.triggers)) + .withConfiguration(new Configuration(ruleDto.configuration)) + .withConfigurationDescriptions(ConfigDescriptionDTOMapper.map(ruleDto.configDescriptions)) + .withTemplateUID(ruleDto.templateUID).withVisibility(ruleDto.visibility).withTags(ruleDto.tags) + .withName(ruleDto.name).withDescription(ruleDto.description).build(); + } + + protected static void fillProperties(final Rule from, final RuleDTO to) { + to.triggers = TriggerDTOMapper.map(from.getTriggers()); + to.conditions = ConditionDTOMapper.map(from.getConditions()); + to.actions = ActionDTOMapper.map(from.getActions()); + to.configuration = from.getConfiguration().getProperties(); + to.configDescriptions = ConfigDescriptionDTOMapper.mapParameters(from.getConfigurationDescriptions()); + to.templateUID = from.getTemplateUID(); + to.uid = from.getUID(); + to.name = from.getName(); + to.tags = from.getTags(); + to.visibility = from.getVisibility(); + to.description = from.getDescription(); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTO.java new file mode 100644 index 00000000000..8f0414f3067 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTO.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.dto; + +import java.util.List; +import java.util.Set; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionParameterDTO; +import org.openhab.core.automation.Visibility; + +/** + * This is a data transfer object that is used to serialize the rule templates. + * + * @author Ana Dimova - Initial contribution + * + */ +public class RuleTemplateDTO { + public String label; + public String uid; + public Set tags; + public String description; + public Visibility visibility; + public List configDescriptions; + public List triggers; + public List conditions; + public List actions; +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTOMapper.java new file mode 100644 index 00000000000..2b7c0e09fed --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/RuleTemplateDTOMapper.java @@ -0,0 +1,51 @@ +/** + * 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.dto; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.dto.RuleTemplateDTO; +import org.openhab.core.automation.template.RuleTemplate; + +/** + * This is a utility class to convert between the Rule Templates and RuleTemplateDTO objects. + * + * @author Ana Dimova - Initial contribution + * + */ +public class RuleTemplateDTOMapper { + + public static RuleTemplateDTO map(final RuleTemplate template) { + final RuleTemplateDTO templateDTO = new RuleTemplateDTO(); + fillProperties(template, templateDTO); + return templateDTO; + } + + public static RuleTemplate map(final RuleTemplateDTO ruleTemplateDto) { + return new RuleTemplate(ruleTemplateDto.uid, ruleTemplateDto.label, ruleTemplateDto.description, + ruleTemplateDto.tags, TriggerDTOMapper.mapDto(ruleTemplateDto.triggers), + ConditionDTOMapper.mapDto(ruleTemplateDto.conditions), ActionDTOMapper.mapDto(ruleTemplateDto.actions), + ConfigDescriptionDTOMapper.map(ruleTemplateDto.configDescriptions), ruleTemplateDto.visibility); + } + + protected static void fillProperties(final RuleTemplate from, final RuleTemplateDTO to) { + to.label = from.getLabel(); + to.uid = from.getUID(); + to.tags = from.getTags(); + to.description = from.getDescription(); + to.visibility = from.getVisibility(); + to.configDescriptions = ConfigDescriptionDTOMapper.mapParameters(from.getConfigurationDescriptions()); + to.triggers = TriggerDTOMapper.map(from.getTriggers()); + to.conditions = ConditionDTOMapper.map(from.getConditions()); + to.actions = ActionDTOMapper.map(from.getActions()); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTO.java new file mode 100644 index 00000000000..802f0e764e4 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTO.java @@ -0,0 +1,22 @@ +/** + * 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.dto; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class TriggerDTO extends ModuleDTO { + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTOMapper.java new file mode 100644 index 00000000000..39b03a8e527 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerDTOMapper.java @@ -0,0 +1,66 @@ +/** + * 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.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.dto.TriggerDTO; +import org.openhab.core.automation.util.ModuleBuilder; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Kai Kreuzer - Changed to using ModuleBuilder + */ +public class TriggerDTOMapper extends ModuleDTOMapper { + + public static TriggerDTO map(final Trigger trigger) { + final TriggerDTO triggerDto = new TriggerDTO(); + fillProperties(trigger, triggerDto); + return triggerDto; + } + + public static Trigger mapDto(final TriggerDTO triggerDto) { + return ModuleBuilder.createTrigger().withId(triggerDto.id).withTypeUID(triggerDto.type) + .withConfiguration(new Configuration(triggerDto.configuration)).withLabel(triggerDto.label) + .withDescription(triggerDto.description).build(); + } + + public static List map(final Collection triggers) { + if (triggers == null) { + return null; + } + final List dtos = new ArrayList(triggers.size()); + for (final Trigger trigger : triggers) { + dtos.add(map(trigger)); + } + return dtos; + } + + public static List mapDto(final Collection dtos) { + if (dtos == null) { + return null; + } + final List triggers = new ArrayList(dtos.size()); + for (final TriggerDTO dto : dtos) { + triggers.add(mapDto(dto)); + } + return triggers; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTO.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTO.java new file mode 100644 index 00000000000..f5b95e66e8e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTO.java @@ -0,0 +1,28 @@ +/** + * 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.dto; + +import java.util.List; + +import org.openhab.core.automation.type.Output; + +/** + * This is a data transfer object that is used to serialize the respective class. + * + * @author Markus Rathgeb - Initial contribution and API + */ +public class TriggerTypeDTO extends ModuleTypeDTO { + + public List outputs; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.java new file mode 100644 index 00000000000..a72b0b82ff3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/dto/TriggerTypeDTOMapper.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.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTOMapper; +import org.openhab.core.automation.dto.CompositeTriggerTypeDTO; +import org.openhab.core.automation.dto.TriggerTypeDTO; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.TriggerType; + +/** + * This is a utility class to convert between the respective object and its DTO. + * + * @author Markus Rathgeb - Initial contribution and API + * @author Ana Dimova - extends Trigger Module type DTOs with composites + */ +public class TriggerTypeDTOMapper extends ModuleTypeDTOMapper { + + public static TriggerTypeDTO map(final TriggerType triggerType) { + return map(triggerType, new TriggerTypeDTO()); + } + + public static CompositeTriggerTypeDTO map(final CompositeTriggerType triggerType) { + final CompositeTriggerTypeDTO triggerTypeDto = map(triggerType, new CompositeTriggerTypeDTO()); + triggerTypeDto.children = TriggerDTOMapper.map(triggerType.getChildren()); + return triggerTypeDto; + } + + public static TriggerType map(final CompositeTriggerTypeDTO triggerTypeDto) { + if (triggerTypeDto.children == null || triggerTypeDto.children.isEmpty()) { + return new TriggerType(triggerTypeDto.uid, + ConfigDescriptionDTOMapper.map(triggerTypeDto.configDescriptions), triggerTypeDto.label, + triggerTypeDto.description, triggerTypeDto.tags, triggerTypeDto.visibility, triggerTypeDto.outputs); + } else { + return new CompositeTriggerType(triggerTypeDto.uid, + ConfigDescriptionDTOMapper.map(triggerTypeDto.configDescriptions), triggerTypeDto.label, + triggerTypeDto.description, triggerTypeDto.tags, triggerTypeDto.visibility, triggerTypeDto.outputs, + TriggerDTOMapper.mapDto(triggerTypeDto.children)); + } + } + + public static List map(final Collection types) { + if (types == null) { + return null; + } + final List dtos = new ArrayList(types.size()); + for (final TriggerType type : types) { + if (type instanceof CompositeTriggerType) { + dtos.add(map((CompositeTriggerType) type)); + } else { + dtos.add(map(type)); + } + } + return dtos; + } + + private static T map(final TriggerType triggerType, final T triggerTypeDto) { + fillProperties(triggerType, triggerTypeDto); + triggerTypeDto.outputs = triggerType.getOutputs(); + return triggerTypeDto; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AbstractRuleRegistryEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AbstractRuleRegistryEvent.java new file mode 100644 index 00000000000..5861b76b5fb --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/AbstractRuleRegistryEvent.java @@ -0,0 +1,49 @@ +/** + * 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.events; + +import org.eclipse.smarthome.core.events.AbstractEvent; +import org.openhab.core.automation.dto.RuleDTO; + +/** + * abstract class for rule events + * + * @author Benedikt Niehues - initial contribution + * @author Markus Rathgeb - Use the DTO for the Rule representation + * + */ +public abstract class AbstractRuleRegistryEvent extends AbstractEvent { + + private final RuleDTO rule; + + /** + * Must be called in subclass constructor to create a new rule registry event. + * + * @param topic the topic of the event + * @param payload the payload of the event + * @param source the source of the event + * @param ruleDTO the ruleDTO for which this event is created + */ + public AbstractRuleRegistryEvent(String topic, String payload, String source, RuleDTO rule) { + super(topic, payload, source); + this.rule = rule; + } + + /** + * @return the RuleDTO which caused the Event + */ + public RuleDTO getRule() { + return this.rule; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleAddedEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleAddedEvent.java new file mode 100644 index 00000000000..10baeba8310 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleAddedEvent.java @@ -0,0 +1,49 @@ +/** + * 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.events; + +import org.openhab.core.automation.dto.RuleDTO; + +/** + * An {@link RuleAddedEvent} notifies subscribers that a rule has been added. + * + * @author Benedikt Niehues - initial contribution + * + */ +public class RuleAddedEvent extends AbstractRuleRegistryEvent { + + public static final String TYPE = RuleAddedEvent.class.getSimpleName(); + + /** + * constructs a new rule added event + * + * @param topic the topic of the event + * @param payload the payload of the event + * @param source the source of the event + * @param ruleDTO the rule for which this event is created + */ + public RuleAddedEvent(String topic, String payload, String source, RuleDTO rule) { + super(topic, payload, source, rule); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return "Rule '" + getRule().uid + "' has been added."; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleRemovedEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleRemovedEvent.java new file mode 100644 index 00000000000..e13584aa391 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleRemovedEvent.java @@ -0,0 +1,49 @@ +/** + * 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.events; + +import org.openhab.core.automation.dto.RuleDTO; + +/** + * An {@link RuleRemovedEvent} notifies subscribers that a rule has been removed. + * + * @author Benedikt Niehues - initial contribution + * + */ +public class RuleRemovedEvent extends AbstractRuleRegistryEvent { + + public static final String TYPE = RuleRemovedEvent.class.getSimpleName(); + + /** + * Constructs a new rule removed event + * + * @param topic the topic of the event + * @param payload the payload of the event + * @param source the source of the event + * @param rule the rule for which this event is + */ + public RuleRemovedEvent(String topic, String payload, String source, RuleDTO rule) { + super(topic, payload, source, rule); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return "Rule '" + getRule().uid + "' has been removed."; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleStatusInfoEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleStatusInfoEvent.java new file mode 100644 index 00000000000..834dec7496e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleStatusInfoEvent.java @@ -0,0 +1,71 @@ +/** + * 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.events; + +import org.eclipse.smarthome.core.events.AbstractEvent; +import org.openhab.core.automation.RuleStatusInfo; + +/** + * An {@link RuleStatusInfoEvent} notifies subscribers that a rule status has been updated. + * + * @author Benedikt Niehues - initial contribution + * @author Kai Kreuzer - added toString method + * + */ +public class RuleStatusInfoEvent extends AbstractEvent { + + public static final String TYPE = RuleStatusInfoEvent.class.getSimpleName(); + + private RuleStatusInfo statusInfo; + private String ruleId; + + /** + * constructs a new rule status event + * + * @param topic the topic of the event + * @param payload the payload of the event + * @param source the source of the event + * @param statusInfo the status info for this event + * @param ruleId the rule for which this event is + */ + public RuleStatusInfoEvent(String topic, String payload, String source, RuleStatusInfo statusInfo, String ruleId) { + super(topic, payload, source); + this.statusInfo = statusInfo; + this.ruleId = ruleId; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * @return the statusInfo + */ + public RuleStatusInfo getStatusInfo() { + return statusInfo; + } + + /** + * @return the ruleId + */ + public String getRuleId() { + return ruleId; + } + + @Override + public String toString() { + return ruleId + " updated: " + statusInfo.toString(); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleUpdatedEvent.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleUpdatedEvent.java new file mode 100644 index 00000000000..4c10444ff23 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/events/RuleUpdatedEvent.java @@ -0,0 +1,60 @@ +/** + * 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.events; + +import org.openhab.core.automation.dto.RuleDTO; + +/** + * An {@link RuleUpdatedEvent} notifies subscribers that a rule has been updated. + * + * @author Benedikt Niehues - initial contribution + * + */ +public class RuleUpdatedEvent extends AbstractRuleRegistryEvent { + + public static final String TYPE = RuleUpdatedEvent.class.getSimpleName(); + + private final RuleDTO oldRule; + + /** + * constructs a new rule updated event + * + * @param topic the topic of the event + * @param payload the payload of the event + * @param source the source of the event + * @param rule the rule for which is this event + * @param oldRule the rule that has been updated + */ + public RuleUpdatedEvent(String topic, String payload, String source, RuleDTO rule, RuleDTO oldRule) { + super(topic, payload, source, rule); + this.oldRule = oldRule; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * @return the oldRuleDTO + */ + public RuleDTO getOldRule() { + return oldRule; + } + + @Override + public String toString() { + return "Rule '" + getRule().uid + "' has been updated."; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java new file mode 100644 index 00000000000..8d7264d555d --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java @@ -0,0 +1,45 @@ +/** + * 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.handler; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; + +/** + * This interface should be implemented by external modules which provide functionality for processing {@link Action} + * modules. This functionality is called to execute the {@link Action}s of the {@link Rule} when it is needed. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + */ +@NonNullByDefault +public interface ActionHandler extends ModuleHandler { + + /** + * Called to execute an {@link Action} of the {@link Rule} when it is needed. + * + * @param context an unmodifiable map containing the outputs of the {@link Trigger} that triggered the {@link Rule}, + * the outputs of all preceding {@link Action}s, and the inputs for this {@link Action}. + * + * @return a map with the {@code outputs} which are the result of the {@link Action}'s execution (may be null). + */ + @Nullable + Map execute(Map context); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandler.java new file mode 100644 index 00000000000..877983ce196 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandler.java @@ -0,0 +1,42 @@ +/** + * 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.handler; + +import org.openhab.core.automation.Module; +import org.openhab.core.automation.ModuleHandlerCallback; + +/** + * This is a base class that can be used by any ModuleHandler implementation + * + * @author Kai Kreuzer - Initial Contribution + */ +public class BaseModuleHandler implements ModuleHandler { + + protected T module; + protected ModuleHandlerCallback callback; + + public BaseModuleHandler(T module) { + this.module = module; + } + + @Override + public void setCallback(ModuleHandlerCallback callback) { + this.callback = callback; + } + + @Override + public void dispose() { + this.callback = null; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java new file mode 100644 index 00000000000..5d3aa577bf6 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseModuleHandlerFactory.java @@ -0,0 +1,88 @@ +/** + * 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.handler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; + +/** + * This class provides a {@link ModuleHandlerFactory} base implementation. It is used by its subclasses for base + * implementation of creating and disposing {@link ModuleHandler} instances. They only have to implement + * {@link #internalCreate(Module, String)} method for creating concrete instances needed for the operation of the + * {@link Module}s. + * + * @author Kai Kreuzer - Initial Contribution + * @author Benedikt Niehues - change behavior for unregistering ModuleHandler + */ +@NonNullByDefault +public abstract class BaseModuleHandlerFactory implements ModuleHandlerFactory { + + private final Map<@NonNull String, @NonNull ModuleHandler> handlers = new HashMap<>(); + + /** + * Should be overridden by the implementations that extend this base class. Called from DS to deactivate the + * {@link ModuleHandlerFactory}. + */ + protected void deactivate() { + for (ModuleHandler handler : handlers.values()) { + handler.dispose(); + } + handlers.clear(); + } + + /** + * Provides all available {@link ModuleHandler}s created by concrete factory implementation. + * + * @return a map with keys calculated by concatenated rule UID and module Id and values representing + * {@link ModuleHandler} created for concrete module corresponding to the module Id and belongs to rule with + * such UID. + */ + protected Map getHandlers() { + return Collections.unmodifiableMap(handlers); + } + + @Override + @SuppressWarnings("null") + public @Nullable ModuleHandler getHandler(Module module, String ruleUID) { + String id = ruleUID + module.getId(); + ModuleHandler handler = handlers.get(id); + handler = handler == null ? internalCreate(module, ruleUID) : handler; + if (handler != null) { + handlers.put(id, handler); + } + return handler; + } + + /** + * Creates a new {@link ModuleHandler} for a given {@code module} and {@code ruleUID}. + * + * @param module the {@link Module} for which a handler should be created. + * @param ruleUID the identifier of the {@link Rule} that the given module belongs to. + * @return a {@link ModuleHandler} instance or {@code null} if thins module type is not supported. + */ + protected abstract @Nullable ModuleHandler internalCreate(Module module, String ruleUID); + + @Override + public void ungetHandler(Module module, String ruleUID, ModuleHandler handler) { + if (handlers.remove(ruleUID + module.getId(), handler)) { + handler.dispose(); + } + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseTriggerModuleHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseTriggerModuleHandler.java new file mode 100644 index 00000000000..4761954d832 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/BaseTriggerModuleHandler.java @@ -0,0 +1,28 @@ +/** + * 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.handler; + +import org.openhab.core.automation.Trigger; + +/** + * This is a base class that can be used by TriggerModuleHandler implementations + * + * @author Vasil Ilchev - Initial contribution + */ +public class BaseTriggerModuleHandler extends BaseModuleHandler implements TriggerHandler { + + public BaseTriggerModuleHandler(Trigger module) { + super(module); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java new file mode 100644 index 00000000000..b56f0707773 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java @@ -0,0 +1,40 @@ +/** + * 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.handler; + +import java.util.Map; + +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; + +/** + * This interface provides common functionality for processing {@link Condition} modules. + * + * @see ModuleHandler + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + */ +public interface ConditionHandler extends ModuleHandler { + + /** + * Checks if the Condition is satisfied in the given {@code context}. + * + * @param context an unmodifiable map containing the outputs of the {@link Trigger} that triggered the {@link Rule} + * and the inputs of the {@link Condition}. + * @return {@code true} if {@link Condition} is satisfied, {@code false} otherwise. + */ + public boolean isSatisfied(Map context); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java new file mode 100644 index 00000000000..8646b11e272 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandler.java @@ -0,0 +1,40 @@ +/** + * 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.ModuleHandlerCallback; + +/** + * A common interface for all module Handler interfaces. The Handler interfaces are + * bridge between RuleManager and external modules used by the RuleManager. + * + * @author Yordan Mihaylov - Initial Contribution + * @see ModuleHandlerFactory + */ +@NonNullByDefault +public interface ModuleHandler { + + /** + * The method is called by RuleManager to free resources when {@link ModuleHandler} is released. + */ + public void dispose(); + + /** + * The callback is injected to the handler through this method. + * + * @param callback a {@link ModuleHandlerCallback} instance + */ + void setCallback(ModuleHandlerCallback callback); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java new file mode 100644 index 00000000000..3c4be0e6820 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ModuleHandlerFactory.java @@ -0,0 +1,71 @@ +/** + * 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.handler; + +import java.util.Collection; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; + +/** + * This interface represents a factory for {@link ModuleHandler} instances. It is used for creating + * and disposing the {@link TriggerHandler}s, {@link ConditionHandler}s and {@link ActionHandler}s + * needed for the operation of the {@link Module}s included in {@link Rule}s. + *

    + * {@link ModuleHandlerFactory} implementations must be registered as services in the OSGi framework. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Benedikt Niehues - change behavior for unregistering ModuleHandler + */ +@NonNullByDefault +public interface ModuleHandlerFactory { + + /** + * Returns the UIDs of the module types currently supported by this factory. + * A {@link ModuleHandlerFactory} instance can add new types to this list, but should not remove. If a + * module type is no longer supported, the {@link ModuleHandlerFactory} service must be unregistered, and + * then registered again with the new list. + *

    + * If two or more {@link ModuleHandlerFactory}s support the same module type, the Rule Engine will choose + * one of them randomly. Once a factory is chosen, it will be used to create instances of this module + * type until its service is unregistered. + * + * @return collection of module type UIDs supported by this factory. + */ + public Collection getTypes(); + + /** + * Creates a {@link ModuleHandler} instance needed for the operation of the {@link Module}s + * included in {@link Rule}s. + * + * @param module the {@link Module} for which a {@link ModuleHandler} instance must be created. + * @param ruleUID the identifier of the {@link Rule} that the given module belongs to. + * @return a new {@link ModuleHandler} instance, or {@code null} if the type of the + * {@code module} parameter is not supported by this factory. + */ + public @Nullable ModuleHandler getHandler(Module module, String ruleUID); + + /** + * Releases the {@link ModuleHandler} instance when it is not needed anymore + * for handling the specified {@code module} in the {@link Rule} with the specified {@code ruleUID}. + * If no other {@link Rule}s and {@link Module}s use this {@code handler} instance, it should be disposed. + * + * @param module the {@link Module} for which the {@code handler} was created. + * @param ruleUID the identifier of the {@link Rule} that the given module belongs to. + * @param handler the {@link ModuleHandler} instance that is no longer needed. + */ + public void ungetHandler(Module module, String ruleUID, ModuleHandler handler); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandler.java new file mode 100644 index 00000000000..16cdb548606 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandler.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.handler; + +/** + * This Handler interface is used by the RuleManager to set a callback interface to + * itself. The callback has to implemented {@link TriggerHandlerCallback} interface + * and it is used to notify the RuleManager when {@link TriggerHandler} was triggered + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + */ +public interface TriggerHandler extends ModuleHandler { +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandlerCallback.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandlerCallback.java new file mode 100644 index 00000000000..c5ae981485e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/TriggerHandlerCallback.java @@ -0,0 +1,49 @@ +/** + * 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.handler; + +import java.util.Map; + +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.type.Output; + +/** + * This is a callback interface to RuleManager which is used by the {@link TriggerHandler} to notify the RuleManager + * about firing of the {@link Trigger}. These calls from {@link Trigger}s must be stored in a queue + * and applied to the RuleAngine in order of their appearance. Each {@link Rule} has to create its own instance of + * {@link TriggerHandlerCallback}. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Kai Kreuzer - made it a sub-interface of ModuleHandlerCallback + */ +public interface TriggerHandlerCallback extends ModuleHandlerCallback { + + /** + * This method is used by the {@link TriggerHandler} to notify the RuleManager when + * the liked {@link Trigger} instance was fired. + * + * @param trigger instance of trigger which was fired. When one TriggerHandler + * serve more then one {@link Trigger} instances, this parameter + * defines which trigger was fired. + * @param context is a {@link Map} of output values of the triggered {@link Trigger}. Each entry of the map + * contains: + *

      + *
    • key - the id of the {@link Output} , + *
    • value - represents output value of the {@link Trigger}'s {@link Output} + *
    + */ + public void triggered(Trigger trigger, Map context); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ActionImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ActionImpl.java new file mode 100644 index 00000000000..e15c39d6dfc --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ActionImpl.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.internal; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This class is implementation of {@link Action} modules used in the {@link RuleEngineImpl}s. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + */ +@NonNullByDefault +public class ActionImpl extends ModuleImpl implements Action { + + private Map inputs = Collections.emptyMap(); + + /** + * Constructor of Action object. + * + * @param UID action unique id. + * @param typeUID module type unique id. + * @param configuration map of configuration values. + * @param label the label + * @param description description + * @param inputs set of connections to other modules (triggers and other actions). + */ + public ActionImpl(String UID, String typeUID, @Nullable Configuration configuration, @Nullable String label, + @Nullable String description, @Nullable Map inputs) { + super(UID, typeUID, configuration, label, description); + this.inputs = inputs == null ? Collections.emptyMap() : Collections.unmodifiableMap(inputs); + } + + /** + * This method is used to get input connections of the Action. The connections + * are links between {@link Input}s of the this {@link Module} and {@link Output}s + * of other {@link Module}s. + * + * @return map that contains the inputs of this action. + */ + @Override + public Map getInputs() { + return inputs; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Automation.ucls b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Automation.ucls new file mode 100644 index 00000000000..1085ef7822e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Automation.ucls @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConditionImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConditionImpl.java new file mode 100644 index 00000000000..0c6c00a1caf --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConditionImpl.java @@ -0,0 +1,64 @@ +/** + * 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.internal; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This class is implementation of {@link Condition} modules used in the {@link RuleEngineImpl}s. + * + * @author Yordan Mihaylov - Initial Contribution + */ +@NonNullByDefault +public class ConditionImpl extends ModuleImpl implements Condition { + + private Map inputs = Collections.emptyMap(); + + /** + * Constructor of {@link Condition} module object. + * + * @param id id of the module. + * @param typeUID unique module type id. + * @param configuration configuration values of the {@link Condition} module. + * @param label the label + * @param description description + * @param inputs set of {@link Input}s used by this module. + */ + public ConditionImpl(String id, String typeUID, @Nullable Configuration configuration, @Nullable String label, + @Nullable String description, @Nullable Map inputs) { + super(id, typeUID, configuration, label, description); + this.inputs = inputs == null ? Collections.emptyMap() : Collections.unmodifiableMap(inputs); + } + + /** + * This method is used to get input connections of the Condition. The connections + * are links between {@link Input}s of the current {@link Module} and {@link Output}s of other + * {@link Module}s. + * + * @return map that contains the inputs of this condition. + */ + @Override + public Map getInputs() { + return inputs; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Connection.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Connection.java new file mode 100644 index 00000000000..b753041109e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/Connection.java @@ -0,0 +1,182 @@ +/** + * 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.internal; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; + +/** + * This class defines connection between {@link Input} of the current {@link Module} and {@link Output} of the external + * one. The current module is the module containing {@link Connection} instance and the external one is the module where + * the current is connected to.
    + * The input of the current module is defined by name of the {@link Input}. The {@link Output} of the external module is + * defined by id of the module and name of the output. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field + */ +@NonNullByDefault +public final class Connection { + + private final @Nullable String outputModuleId; + private final @Nullable String outputName; + private final @Nullable String reference; + private final String inputName; + + /** + * This constructor is responsible for creation of connections between modules in the rule. + * + * @param inputName is an unique id of the {@code Input} in scope of the {@link Module}. + * @param reference the reference tokens of this connection + */ + public Connection(String inputName, String reference) { + this(inputName, null, null, Objects.requireNonNull(reference, "Configuration Reference can't be null.")); + } + + /** + * This constructor is responsible for creation of connections between modules in the rule. + * + * @param inputName is an unique name of the {@code Input} in scope of the {@link Module}. + * @param outputModuleId is an unique id of the {@code Module} in scope of the {@link Rule}. + * @param outputName is an unique name of the {@code Output} in scope of the {@link Module}. + * @param reference the reference tokens of this connection + */ + public Connection(String inputName, @Nullable String outputModuleId, @Nullable String outputName, + @Nullable String reference) { + this.inputName = Objects.requireNonNull(inputName, "Input name can't be null."); + if (inputName.isEmpty()) { + throw new IllegalArgumentException("Invalid name for Input."); + } + this.outputName = outputName; + this.outputModuleId = outputModuleId; + this.reference = reference; + } + + /** + * Gets the identifier of external {@link Module} of this connection. + * + * @return id of external {@link Module} + */ + public @Nullable String getOutputModuleId() { + return outputModuleId; + } + + /** + * Gets the output name of external {@link Module} of this connection. + * + * @return name of {@link Output} of external {@link Module}. + */ + public @Nullable String getOutputName() { + return outputName; + } + + /** + * Gets input name of current {@link Module} of this connection. + * + * @return name {@link Input} of the current {@link Module} + */ + public String getInputName() { + return inputName; + } + + /** + * Gets the reference tokens of this connection. + * + * @return the reference tokens. + */ + public @Nullable String getReference() { + return reference; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()); + sb.append("["); + if (outputModuleId != null) { + sb.append(outputModuleId); + sb.append("."); + sb.append(outputName); + } + if (reference != null) { + sb.append("("); + sb.append(reference); + sb.append(")"); + } + sb.append("->"); + sb.append(inputName); + sb.append("]"); + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + toString().hashCode(); + return result; + } + + /** + * Compares two connection objects. Two connections are equal if they own equal {@code inputName}, + * {@code outputModuleId}, {@code outputName} and {@code reference}. + * + * @return {@code true} when own equal {@code inputName}, {@code outputModuleId}, {@code outputName} and + * {@code reference} and {@code false} in the opposite. + */ + @SuppressWarnings("null") + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Connection other = (Connection) obj; + if (!inputName.equals(other.inputName)) { + return false; + } + if (outputModuleId == null) { + if (other.outputModuleId != null) { + return false; + } + } else if (!outputModuleId.equals(other.outputModuleId)) { + return false; + } + if (outputName == null) { + if (other.outputName != null) { + return false; + } + } else if (!outputName.equals(other.outputName)) { + return false; + } + if (reference == null) { + if (other.reference != null) { + return false; + } + } else if (!reference.equals(other.reference)) { + return false; + } + return true; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConnectionValidator.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConnectionValidator.java new file mode 100644 index 00000000000..b25a39d7399 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ConnectionValidator.java @@ -0,0 +1,340 @@ +/** + * 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.internal; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNull; +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.Trigger; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; + +/** + * This class contains utility methods for comparison of data types between connected inputs and outputs of modules + * participating in a rule. + * + * @author Ana Dimova - Initial contribution and API + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * @author Benedikt Niehues - validation of connection-types respects inheriting types + * @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field + * + */ +public class ConnectionValidator { + + public static final String CONFIG_REFERENCE_PATTERN = "\\${1}\\{{1}[A-Za-z0-9_-]+\\}{1}|\\${1}[A-Za-z0-9_-]+"; + public static final String OUTPUT_REFERENCE_PATTERN = "(\\[{1}\\\"{1}.+\\\"{1}\\]{1}|\\[{1}\\d+\\]{1}|\\.{1}[^\\[\\]][A-Za-z0-9_-]+[^\\]\\[\\.])*"; + public static final String MODULE_OUTPUT_PATTERN = "[A-Za-z0-9_-]+\\.{1}[A-Za-z0-9_-]+" + OUTPUT_REFERENCE_PATTERN; + public static final String CONNECTION_PATTERN = CONFIG_REFERENCE_PATTERN + "|" + MODULE_OUTPUT_PATTERN; + + /** + * Validates connections between inputs and outputs of modules participated in rule. It compares data + * types of connected inputs and outputs and throws exception when there is a lack of coincidence. + * + * @param r rule which must be checked + * @throws IllegalArgumentException when validation fails. + */ + public static void validateConnections(ModuleTypeRegistry mtRegistry, Rule r) { + if (r == null) { + throw new IllegalArgumentException("Validation of rule has failed! Rule must not be null!"); + } + validateConnections(mtRegistry, r.getTriggers(), r.getConditions(), r.getActions()); + } + + /** + * Validates connections between inputs and outputs of the modules participated in a rule. It checks is + * there unconnected required inputs and compatibility of data types of connected inputs and outputs. Throws + * exception if they are incompatible. + * + * @param triggers is a list with triggers of the rule whose connections have to be validated + * @param conditions is a list with conditions of the rule whose connections have to be validated + * @param actions is a list with actions of the rule whose connections have to be validated + * @throws IllegalArgumentException when validation fails. + */ + public static void validateConnections(ModuleTypeRegistry mtRegistry, + @NonNull List triggers, @NonNull List conditions, + @NonNull List actions) { + if (!triggers.isEmpty()) { + for (Condition condition : conditions) { + validateConditionConnections(mtRegistry, condition, triggers); + } + } + if (!triggers.isEmpty()) { + for (Action action : actions) { + validateActionConnections(mtRegistry, action, triggers, actions); + } + } + } + + /** + * Validates connections between outputs of triggers and actions and action's inputs. It checks is there + * unconnected required inputs and compatibility of data types of connected inputs and outputs. Throws exception if + * they are incompatible. + * + * @param action is an Action module whose connections have to be validated + * @param triggers is a list with triggers of the rule on which the action belongs + * @param actions is a list with actions of the rule on which the action belongs + * @throws IllegalArgumentException when validation fails. + */ + private static void validateActionConnections(ModuleTypeRegistry mtRegistry, Action action, + @NonNull List triggers, @NonNull List actions) { + + ActionType type = (ActionType) mtRegistry.get(action.getTypeUID()); // get module type of the condition + if (type == null) { + // if module type not exists in the system - throws exception + throw new IllegalArgumentException("Action Type \"" + action.getTypeUID() + "\" does not exist!"); + } + + List inputs = type.getInputs(); // get inputs of the condition according to module type definition + + // gets connected inputs from the condition module and put them into map + Set cons = getConnections(action.getInputs()); + Map connectionsMap = new HashMap<>(); + Iterator connectionsI = cons.iterator(); + while (connectionsI.hasNext()) { + Connection connection = connectionsI.next(); + String inputName = connection.getInputName(); + connectionsMap.put(inputName, connection); + } + + // checks is there unconnected required inputs + if (inputs != null && !inputs.isEmpty()) { + for (Input input : inputs) { + String name = input.getName(); + Connection connection = connectionsMap.get(name); + if (connection == null && input.isRequired()) { + throw new IllegalArgumentException("Required input \"" + name + "\" of the condition \"" + + action.getId() + "\" not connected"); + } else if (connection != null) { + checkConnection(mtRegistry, connection, input, triggers, actions); + } + } + } + } + + /** + * Validates the connection between outputs of list of triggers and actions to the action's input. It + * checks if the input is unconnected and compatibility of data types of the input and connected output. Throws + * exception if they are incompatible. + * + * @param connection that should be validated + * @param input that should be validated + * @param triggers is a list with triggers of the rule on which the action belongs + * @param actions is a list with actions of the rule on which the action belongs + * @throws IllegalArgumentException when validation fails. + */ + private static void checkConnection(ModuleTypeRegistry mtRegistry, Connection connection, Input input, + @NonNull List triggers, @NonNull List actions) { + Map actionsMap = new HashMap<>(); + for (Action a : actions) { + actionsMap.put(a.getId(), a); + } + String moduleId = connection.getOutputModuleId(); + Action action = actionsMap.get(moduleId); + String msg = " Invalid Connection \"" + connection.getInputName() + "\" : "; + if (moduleId != null && action != null) { + String typeUID = action.getTypeUID(); + ActionType actionType = (ActionType) mtRegistry.get(typeUID); + if (actionType == null) { + throw new IllegalArgumentException(msg + " Action Type with UID \"" + typeUID + "\" does not exist!"); + } + checkCompatibility(msg, connection, input, actionType.getOutputs()); + } else { + checkConnection(mtRegistry, connection, input, triggers); + } + } + + /** + * Validates connections between trigger's outputs and condition's inputs. It checks is there unconnected + * required inputs and compatibility of data types of connected inputs and outputs. Throws exception if they are + * incompatible. + * + * @param condition is a Condition module whose connections have to be validated + * @param triggers is a list with triggers of the rule on which the condition belongs + * @throws IllegalArgumentException when validation fails. + */ + private static void validateConditionConnections(ModuleTypeRegistry mtRegistry, @NonNull Condition condition, + @NonNull List triggers) { + + ConditionType type = (ConditionType) mtRegistry.get(condition.getTypeUID()); // get module type of the condition + if (type == null) { + // if module type not exists in the system - throws exception + throw new IllegalArgumentException("Condition Type \"" + condition.getTypeUID() + "\" does not exist!"); + } + + List inputs = type.getInputs(); // get inputs of the condition according to module type definition + + // gets connected inputs from the condition module and put them into map + Set cons = getConnections(condition.getInputs()); + Map connectionsMap = new HashMap<>(); + Iterator connectionsI = cons.iterator(); + while (connectionsI.hasNext()) { + Connection connection = connectionsI.next(); + String inputName = connection.getInputName(); + connectionsMap.put(inputName, connection); + } + + // checks is there unconnected required inputs + if (inputs != null && !inputs.isEmpty()) { + for (Input input : inputs) { + String name = input.getName(); + Connection connection = connectionsMap.get(name); + if (connection != null) { + checkConnection(mtRegistry, connection, input, triggers); + } else if (input.isRequired()) { + throw new IllegalArgumentException("Required input \"" + name + "\" of the condition \"" + + condition.getId() + "\" not connected"); + } + } + } + } + + /** + * Validates the connection between outputs of list of triggers to the action's or condition's input. It + * checks if the input is unconnected and compatibility of data types of the input and connected output. Throws + * exception if they are incompatible. + * + * @param connection that should be validated + * @param input that should be validated + * @param triggers is a list with triggers of the rule on which the action belongs + * @throws IllegalArgumentException when validation fails. + */ + private static void checkConnection(ModuleTypeRegistry mtRegistry, Connection connection, Input input, + @NonNull List triggers) { + + Map triggersMap = new HashMap<>(); + for (Trigger trigger : triggers) { + triggersMap.put(trigger.getId(), trigger); + } + String moduleId = connection.getOutputModuleId(); + String msg = " Invalid Connection \"" + connection.getInputName() + "\" : "; + if (moduleId != null) { + Trigger trigger = triggersMap.get(moduleId); + if (trigger == null) { + throw new IllegalArgumentException(msg + " Trigger with ID \"" + moduleId + "\" does not exist!"); + } + String triggerTypeUID = trigger.getTypeUID(); + TriggerType triggerType = (TriggerType) mtRegistry.get(triggerTypeUID); + if (triggerType == null) { + throw new IllegalArgumentException( + msg + " Trigger Type with UID \"" + triggerTypeUID + "\" does not exist!"); + } + checkCompatibility(msg, connection, input, triggerType.getOutputs()); + } + } + + /** + * Checks the compatibility of data types of the input and connected output. Throws + * exception if they are incompatible. + * + * @param msg message should be extended with an information and thrown as exception when validation fails. + * @param connection that should be validated + * @param input that should be validated + * @param outputs list with outputs of the module connected to the given input + * @throws IllegalArgumentException when validation fails. + */ + private static void checkCompatibility(String msg, Connection connection, Input input, List outputs) { + if (connection.getReference() != null) { + // we are referencing a value inside an existing data structure of the output and will not check if the + // property inside of it really exists and if its type is compatible, so the connection will be treated as + // valid + return; + } + String outputName = connection.getOutputName(); + if (outputs != null && !outputs.isEmpty()) { + for (Output output : outputs) { + if (output.getName().equals(outputName)) { + if (input.getType().equals("*")) { + return; + } else { + try { + Class outputType = Class.forName(output.getType()); + Class inputType = Class.forName(input.getType()); + if (inputType.isAssignableFrom(outputType)) { + return; + } else { + throw new IllegalArgumentException(msg + " Incompatible types : \"" + output.getType() + + "\" and \"" + input.getType() + "\"."); + } + } catch (ClassNotFoundException e) { + if (output.getType().equals(input.getType())) { + return; + } else { + throw new IllegalArgumentException(msg + " Incompatible types : \"" + output.getType() + + "\" and \"" + input.getType() + "\"."); + } + } + } + } + } + throw new IllegalArgumentException(msg + " Output with name \"" + outputName + + "\" not exists in the ModuleImpl with ID \"" + connection.getOutputModuleId() + "\""); + } + } + + /** + * Collects the {@link Connection}s of {@link Module}s. + * + * @param inputs the map of input references of the module. + * @return collected set of Connections. + * @throws IllegalArgumentException if there is a value in the {@code inputs} map with an invalid format for a + * connection. + */ + public static Set getConnections(Map inputs) { + Set connections = new HashSet<>(); + for (Entry input : inputs.entrySet()) { + String inputName = input.getKey(); + String reference = input.getValue(); + Connection connection = getConnection(inputName, reference); + connections.add(connection); + } + return connections; + } + + private static Connection getConnection(String inputName, String reference) { + if (reference == null || !Pattern.matches(CONNECTION_PATTERN, reference)) { + throw new IllegalArgumentException("Wrong format of Connection : " + inputName + ": " + reference); + } + if (Pattern.matches(CONFIG_REFERENCE_PATTERN, reference)) { + return new Connection(inputName, reference); + } else { + if (!Pattern.matches(MODULE_OUTPUT_PATTERN, reference)) { + throw new IllegalArgumentException("Wrong format of Connection : " + inputName + ": " + reference); + } + final Pattern pattern = Pattern.compile("\\.|\\["); + final String[] referenceTokens = pattern.split(reference, 3); + String outputModuleId = referenceTokens[0]; + String outputName = referenceTokens[1]; + if (referenceTokens.length == 3) { + return new Connection(inputName, outputModuleId, outputName, + reference.substring(reference.indexOf(outputName) + outputName.length())); + } else { + return new Connection(inputName, outputModuleId, outputName, null); + } + } + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ModuleImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ModuleImpl.java new file mode 100644 index 00000000000..074d9d72ec5 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/ModuleImpl.java @@ -0,0 +1,157 @@ +/** + * 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.internal; + +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.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.Output; + +/** + * Modules are building components of the {@link Rule}s. Each ModuleImpl is + * identified by id, which is unique in scope of the {@link Rule}. It also has a {@link ModuleType} which provides + * meta + * data of the module. The meta data + * defines {@link Input}s, {@link Output}s and {@link ConfigDescriptionParameter}s parameters of the {@link ModuleImpl}. + *
    + * Setters of the module don't have immediate effect on the Rule. To apply the + * changes, they should be set on the {@link Rule} and the Rule has to be + * updated by RuleManager + * + * @author Yordan Mihaylov - Initial Contribution + * + */ +public abstract class ModuleImpl implements Module { + + /** + * Id of the ModuleImpl. It is mandatory and unique identifier in scope of the {@link Rule}. The id of the + * {@link ModuleImpl} is used to identify the module + * in the {@link Rule}. + */ + private String id; + + /** + * The label is a short, user friendly name of the {@link ModuleImpl} defined by + * this descriptor. + */ + private String label; + + /** + * The description is a long, user friendly description of the {@link ModuleImpl} defined by this descriptor. + */ + private String description; + + /** + * Configuration values of the ModuleImpl. + * + * @see {@link ConfigDescriptionParameter}. + */ + private Configuration configuration; + + /** + * Unique type id of this module. + */ + private String type; + + /** + * Constructor of the module. + * + * @param id the module id. + * @param typeUID unique id of the module type. + * @param configuration configuration values of the module. + * @param label the label + * @param description the description + */ + public ModuleImpl(String id, String typeUID, @Nullable Configuration configuration, @Nullable String label, + @Nullable String description) { + this.id = id; + this.type = typeUID; + this.configuration = new Configuration(configuration); + this.label = label; + this.description = description; + } + + @Override + public String getId() { + return id; + } + + /** + * This method is used for setting the id of the ModuleImpl. + * + * @param id of the module. + */ + public void setId(String id) { + this.id = id; + } + + @Override + public String getTypeUID() { + return type; + } + + /** + * This method is used for setting the typeUID of the ModuleImpl. + * + * @param typeUID of the module. + */ + public void setTypeUID(String typeUID) { + this.type = typeUID; + } + + @Override + public String getLabel() { + return label; + } + + /** + * This method is used for setting the label of the ModuleImpl. + * + * @param label of the module. + */ + public void setLabel(String label) { + this.label = label; + } + + @Override + public String getDescription() { + return description; + } + + /** + * This method is used for setting the description of the ModuleImpl. + * + * @param description of the module. + */ + public void setDescription(String description) { + this.description = description; + } + + @Override + public Configuration getConfiguration() { + return configuration; + } + + /** + * This method is used for setting the configuration of the {@link ModuleImpl}. + * + * @param configuration new configuration values. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = new Configuration(configuration); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java new file mode 100644 index 00000000000..2815d68f872 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java @@ -0,0 +1,1474 @@ +/** + * 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.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventPublisher; +import org.eclipse.smarthome.core.storage.Storage; +import org.eclipse.smarthome.core.storage.StorageService; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleManager; +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.events.RuleStatusInfoEvent; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.ModuleHandlerFactory; +import org.openhab.core.automation.handler.TriggerHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.automation.internal.TriggerHandlerCallbackImpl.TriggerData; +import org.openhab.core.automation.internal.composite.CompositeModuleHandlerFactory; +import org.openhab.core.automation.internal.ruleengine.WrappedAction; +import org.openhab.core.automation.internal.ruleengine.WrappedCondition; +import org.openhab.core.automation.internal.ruleengine.WrappedModule; +import org.openhab.core.automation.internal.ruleengine.WrappedRule; +import org.openhab.core.automation.internal.ruleengine.WrappedTrigger; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; +import org.openhab.core.automation.util.ReferenceResolver; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is responsible to initialize and execute {@link Rule}s, when the {@link Rule}s are added in rule + * engine. Each {@link Rule} has associated {@link RuleStatusInfo} object which shows its {@link RuleStatus} and + * {@link RuleStatusDetail}. The states are self excluded and they are: + *
  • disabled - the rule is temporary not available. This status is set by the user. + *
  • uninitialized - the rule is enabled, but it is still not working, because some of the module handlers are + * not available or its module types or template are not resolved. The initialization problem is described by the status + * details. + *
  • idle - the rule is enabled and initialized and it is waiting for triggering events. + *
  • running - the rule is enabled and initialized and it is executing at the moment. When the execution is + * finished, it goes to the idle state. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider, registry implementation and customized modules + * @author Benedikt Niehues - change behavior for unregistering ModuleHandler + * @author Markus Rathgeb - use a managed rule + * @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field + */ +@Component(immediate = true) +@NonNullByDefault +public class RuleEngineImpl implements RuleManager, RegistryChangeListener { + + /** + * Constant defining separator between module id and output name. + */ + public static final char OUTPUT_SEPARATOR = '.'; + + private static final String DISABLED_RULE_STORAGE = "automation_rules_disabled"; + + /** + * Delay between rule's re-initialization tries. + */ + private long scheduleReinitializationDelay; + + private final @NonNullByDefault({}) Map managedRules = new ConcurrentHashMap<>(); + + /** + * {@link Map} holding all created {@link TriggerHandlerCallback} instances, corresponding to each {@link Rule}. + * There is only one {@link TriggerHandlerCallback} instance per {@link Rule}. The relation is + * {@link Rule}'s UID to {@link TriggerHandlerCallback} instance. + */ + private final @NonNullByDefault({}) Map thCallbacks = new HashMap(); + + /** + * {@link Map} holding all {@link ModuleType} UIDs that are available in some rule's module definition. The relation + * is {@link ModuleType}'s UID to {@link Set} of {@link Rule} UIDs. + */ + private final @NonNullByDefault({}) Map> mapModuleTypeToRules = new HashMap>(); + + /** + * {@link Map} holding all available {@link ModuleHandlerFactory}s linked with {@link ModuleType}s that they + * supporting. The relation is {@link ModuleType}'s UID to {@link ModuleHandlerFactory} instance. + */ + private final @NonNullByDefault({}) Map moduleHandlerFactories; + + /** + * {@link Set} holding all available {@link ModuleHandlerFactory}s. + */ + private final Set allModuleHandlerFactories = new CopyOnWriteArraySet<>(); + + /** + * The storage for the disable information + */ + private @Nullable Storage disabledRulesStorage; + + /** + * Locker which does not permit rule initialization when the rule engine is stopping. + */ + private boolean isDisposed = false; + + protected Logger logger = LoggerFactory.getLogger(RuleEngineImpl.class.getName()); + + /** + * A callback that is notified when the status of a {@link Rule} changes. + */ + private @NonNullByDefault({}) RuleRegistryImpl ruleRegistry; + + /** + * {@link Map} holding all Rule context maps. Rule context maps contain dynamic parameters used by the + * {@link Rule}'s {@link ModuleImpl}s to communicate with each other during the {@link Rule}'s execution. + * The context map of a {@link Rule} is cleaned when the execution is completed. The relation is + * {@link Rule}'s UID to Rule context map. + */ + private @NonNullByDefault({}) final Map> contextMap; + + /** + * This field holds reference to {@link ModuleTypeRegistry}. The {@link RuleEngineImpl} needs it to auto-map + * connection between rule's modules and to determine module handlers. + */ + private @NonNullByDefault({}) ModuleTypeRegistry mtRegistry; + + /** + * Provides all composite {@link ModuleHandler}s. + */ + private @NonNullByDefault({}) CompositeModuleHandlerFactory compositeFactory; + + /** + * {@link Map} holding all scheduled {@link Rule} re-initialization tasks. The relation is {@link Rule}'s + * UID to + * re-initialization task as a {@link Future} instance. + */ + private final @NonNullByDefault({}) Map> scheduleTasks = new HashMap<>(31); + + /** + * Performs the {@link Rule} re-initialization tasks. + */ + private @Nullable ScheduledExecutorService executor; + + /** + * This field holds {@link RegistryChangeListener} that listen for changes in the rule registry. + * We cannot implement the interface ourselves as we are already a RegistryChangeListener for module types. + */ + private @NonNullByDefault({}) RegistryChangeListener listener; + + /** + * Posts an event through the event bus in an asynchronous way. {@link RuleEngineImpl} use it for posting the + * {@link RuleStatusInfoEvent}. + */ + private @Nullable EventPublisher eventPublisher; + private static final String SOURCE = RuleEngineImpl.class.getSimpleName(); + + private final ModuleHandlerCallback moduleHandlerCallback = new ModuleHandlerCallback() { + + @Override + public @Nullable Boolean isEnabled(String ruleUID) { + return RuleEngineImpl.this.isEnabled(ruleUID); + } + + @Override + public void setEnabled(String uid, boolean isEnabled) { + RuleEngineImpl.this.setEnabled(uid, isEnabled); + } + + @Override + public @Nullable RuleStatusInfo getStatusInfo(String ruleUID) { + return RuleEngineImpl.this.getStatusInfo(ruleUID); + } + + @Override + public @Nullable RuleStatus getStatus(String ruleUID) { + return RuleEngineImpl.this.getStatus(ruleUID); + } + + @Override + public void runNow(String uid) { + RuleEngineImpl.this.runNow(uid); + } + + @Override + public void runNow(String uid, boolean considerConditions, @Nullable Map context) { + RuleEngineImpl.this.runNow(uid, considerConditions, context); + } + + }; + + /** + * Constructor of {@link RuleEngineImpl}. It initializes the logger and starts tracker for + * {@link ModuleHandlerFactory} services. + */ + public RuleEngineImpl() { + this.contextMap = new HashMap>(); + this.moduleHandlerFactories = new HashMap(20); + } + + /** + * This method is used to create a {@link CompositeModuleHandlerFactory} that handles all composite + * {@link ModuleType}s. Called from DS to activate the rule engine component. + */ + @Activate + protected void activate() { + compositeFactory = new CompositeModuleHandlerFactory(mtRegistry, this); + + // enable the rules that are not persisted as Disabled; + for (Rule rule : ruleRegistry.getAll()) { + String uid = rule.getUID(); + final Storage disabledRulesStorage = this.disabledRulesStorage; + if (disabledRulesStorage == null || disabledRulesStorage.get(uid) == null) { + setEnabled(uid, true); + } + } + } + + /** + * Bind the {@link ModuleTypeRegistry} service - called from DS. + * + * @param moduleTypeRegistry a {@link ModuleTypeRegistry} service. + */ + @Reference + protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + mtRegistry = moduleTypeRegistry; + mtRegistry.addRegistryChangeListener(this); + } + + /** + * Unbind the {@link ModuleTypeRegistry} service - called from DS. + * + * @param moduleTypeRegistry a {@link ModuleTypeRegistry} service. + */ + protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + mtRegistry.removeRegistryChangeListener(this); + mtRegistry = null; + } + + /** + * Bind the {@link RuleRegistry} service - called from DS. + * + * @param ruleRegistry a {@link RuleRegistry} service. + */ + @Reference + protected void setRuleRegistry(RuleRegistry ruleRegistry) { + if (ruleRegistry instanceof RuleRegistryImpl) { + this.ruleRegistry = (RuleRegistryImpl) ruleRegistry; + scheduleReinitializationDelay = this.ruleRegistry.getScheduleReinitializationDelay(); + listener = new RegistryChangeListener() { + @Override + public void added(Rule rule) { + RuleEngineImpl.this.addRule(rule); + } + + @Override + public void removed(Rule rule) { + RuleEngineImpl.this.removeRule(rule.getUID()); + } + + @Override + public void updated(Rule oldRule, Rule rule) { + removed(oldRule); + added(rule); + } + }; + ruleRegistry.addRegistryChangeListener(listener); + for (Rule rule : ruleRegistry.getAll()) { + addRule(rule); + } + } else { + logger.error("Unexpected RuleRegistry service: {}", ruleRegistry); + } + } + + /** + * Unbind the {@link RuleRegistry} service - called from DS. + * + * @param ruleRegistry a {@link RuleRegistry} service. + */ + protected void unsetRuleRegistry(RuleRegistry ruleRegistry) { + if (this.ruleRegistry == ruleRegistry) { + ruleRegistry.removeRegistryChangeListener(listener); + listener = null; + this.ruleRegistry = null; + } + } + + /** + * Bind the {@link StorageService} - called from DS. + * + * @param storageService the {@link StorageService} instance. + */ + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setStorageService(StorageService storageService) { + this.disabledRulesStorage = storageService. getStorage(DISABLED_RULE_STORAGE, + this.getClass().getClassLoader()); + } + + /** + * Unbind the {@link StorageService} - called from DS. + * + * @param storageService the {@link StorageService} instance. + */ + protected void unsetStorageService(StorageService storageService) { + this.disabledRulesStorage = null; + } + + @Override + public void added(ModuleType moduleType) { + String moduleTypeName = moduleType.getUID(); + for (ModuleHandlerFactory moduleHandlerFactory : allModuleHandlerFactories) { + Collection moduleTypes = moduleHandlerFactory.getTypes(); + if (moduleTypes.contains(moduleTypeName)) { + synchronized (this) { + this.moduleHandlerFactories.put(moduleTypeName, moduleHandlerFactory); + } + break; + } + } + Set rules = null; + synchronized (this) { + Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName); + if (rulesPerModule != null) { + rules = new HashSet(); + rules.addAll(rulesPerModule); + } + } + if (rules != null) { + for (String rUID : rules) { + RuleStatus ruleStatus = getRuleStatus(rUID); + if (ruleStatus == RuleStatus.UNINITIALIZED) { + scheduleRuleInitialization(rUID); + } + } + } + } + + @Override + public void removed(ModuleType moduleType) { + // removing module types does not effect the rule + } + + @Override + public void updated(ModuleType oldElement, ModuleType moduleType) { + if (moduleType.equals(oldElement)) { + return; + } + String moduleTypeName = moduleType.getUID(); + Set rules = null; + synchronized (this) { + Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName); + if (rulesPerModule != null) { + rules = new HashSet(); + rules.addAll(rulesPerModule); + } + } + if (rules != null) { + for (String rUID : rules) { + final RuleStatus ruleStatus = getRuleStatus(rUID); + if (ruleStatus == null) { + continue; + } + if (ruleStatus.equals(RuleStatus.IDLE) || ruleStatus.equals(RuleStatus.RUNNING)) { + unregister(getManagedRule(rUID), RuleStatusDetail.HANDLER_MISSING_ERROR, + "Update Module Type " + moduleType.getUID()); + setStatus(rUID, new RuleStatusInfo(RuleStatus.INITIALIZING)); + } + } + } + } + + /** + * Bind the {@link ModuleHandlerFactory} service - called from DS. + * + * @param moduleHandlerFactory a {@link ModuleHandlerFactory} service. + */ + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) { + logger.debug("ModuleHandlerFactory added {}", moduleHandlerFactory.getClass().getSimpleName()); + allModuleHandlerFactories.add(moduleHandlerFactory); + Collection moduleTypes = moduleHandlerFactory.getTypes(); + Set notInitializedRules = null; + for (Iterator it = moduleTypes.iterator(); it.hasNext();) { + String moduleTypeName = it.next(); + Set rules = null; + synchronized (this) { + moduleHandlerFactories.put(moduleTypeName, moduleHandlerFactory); + Set rulesPerModule = mapModuleTypeToRules.get(moduleTypeName); + if (rulesPerModule != null) { + rules = new HashSet(); + rules.addAll(rulesPerModule); + } + } + if (rules != null) { + for (String rUID : rules) { + RuleStatus ruleStatus = getRuleStatus(rUID); + if (ruleStatus == RuleStatus.UNINITIALIZED) { + notInitializedRules = notInitializedRules != null ? notInitializedRules + : new HashSet(20); + notInitializedRules.add(rUID); + } + } + } + } + if (notInitializedRules != null) { + for (final String rUID : notInitializedRules) { + scheduleRuleInitialization(rUID); + } + } + } + + /** + * Unbind the {@link ModuleHandlerFactory} service - called from DS. + * + * @param moduleHandlerFactory a {@link ModuleHandlerFactory} service. + */ + protected void removeModuleHandlerFactory(ModuleHandlerFactory moduleHandlerFactory) { + if (moduleHandlerFactory instanceof CompositeModuleHandlerFactory) { + compositeFactory.deactivate(); + compositeFactory = null; + } + allModuleHandlerFactories.remove(moduleHandlerFactory); + Collection moduleTypes = moduleHandlerFactory.getTypes(); + removeMissingModuleTypes(moduleTypes); + for (Iterator it = moduleTypes.iterator(); it.hasNext();) { + String moduleTypeName = it.next(); + moduleHandlerFactories.remove(moduleTypeName); + } + } + + /** + * This method add a new rule into rule engine. Scope identity of the Rule is the identity of the caller. + * + * @param rule a rule which has to be added. + */ + protected void addRule(Rule newRule) { + synchronized (this) { + if (isDisposed) { + throw new IllegalStateException("RuleEngineImpl is disposed!"); + } + } + final String rUID = newRule.getUID(); + final WrappedRule rule = new WrappedRule(newRule); + managedRules.put(rUID, rule); + RuleStatusInfo initStatusInfo = disabledRulesStorage == null || disabledRulesStorage.get(rUID) == null + ? new RuleStatusInfo(RuleStatus.INITIALIZING) + : new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.DISABLED); + rule.setStatusInfo(initStatusInfo); + + WrappedRule oldRule = getManagedRule(rUID); + if (oldRule != null) { + unregister(oldRule); + } + + if (isEnabled(rUID)) { + setRule(rule); + } + } + + /** + * This method tries to initialize the rule. It uses available {@link ModuleHandlerFactory}s to create + * {@link ModuleHandler}s for all {@link ModuleImpl}s of the {@link Rule} and to link them. When all the modules + * have associated module handlers then the {@link Rule} is initialized and it is ready to working. It goes into + * idle state. Otherwise the Rule stays into not initialized and continue to wait missing handlers, module types + * or templates. + * + * @param rule the rule which tried to be initialized. + */ + private void setRule(WrappedRule rule) { + if (isDisposed) { + return; + } + String rUID = rule.getUID(); + setStatus(rUID, new RuleStatusInfo(RuleStatus.INITIALIZING)); + try { + for (final WrappedAction action : rule.getActions()) { + updateMapModuleTypeToRule(rUID, action.unwrap().getTypeUID()); + action.setConnections(ConnectionValidator.getConnections(action.getInputs())); + } + for (final WrappedCondition condition : rule.getConditions()) { + updateMapModuleTypeToRule(rUID, condition.unwrap().getTypeUID()); + condition.setConnections(ConnectionValidator.getConnections(condition.getInputs())); + } + for (final WrappedTrigger trigger : rule.getTriggers()) { + updateMapModuleTypeToRule(rUID, trigger.unwrap().getTypeUID()); + } + validateModuleIDs(rule); + autoMapConnections(rule); + ConnectionValidator.validateConnections(mtRegistry, rule.unwrap()); + } catch (IllegalArgumentException e) { + // change status to UNINITIALIZED + setStatus(rUID, new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.INVALID_RULE, + "Validation of rule " + rUID + " has failed! " + e.getLocalizedMessage())); + return; + } + final String errMsgs = setModuleHandlers(rUID, rule.getModules()); + if (errMsgs == null) { + register(rule); + // change status to IDLE + setStatus(rUID, new RuleStatusInfo(RuleStatus.IDLE)); + Future f = scheduleTasks.remove(rUID); + if (f != null) { + if (!f.isDone()) { + f.cancel(true); + } + } + if (scheduleTasks.isEmpty()) { + if (executor != null) { + executor.shutdown(); + executor = null; + } + } + } else { + // change status to UNINITIALIZED + setStatus(rUID, + new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.HANDLER_INITIALIZING_ERROR, errMsgs)); + unregister(rule); + } + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + protected void unsetEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = null; + } + + /** + * This method can be used in order to post events through the Eclipse SmartHome events bus. A common + * use case is to notify event subscribers about the {@link Rule}'s status change. + * + * @param ruleUID the UID of the {@link Rule}, whose status is changed. + * @param statusInfo the new {@link Rule}s status. + */ + protected void postRuleStatusInfoEvent(String ruleUID, RuleStatusInfo statusInfo) { + if (eventPublisher != null) { + EventPublisher ep = eventPublisher; + Event event = RuleEventFactory.createRuleStatusInfoEvent(statusInfo, ruleUID, SOURCE); + try { + ep.post(event); + } catch (Exception ex) { + logger.error("Could not post event of type '{}'.", event.getType(), ex); + } + } + } + + /** + * This method links modules to corresponding module handlers. + * + * @param rUID id of rule containing these modules + * @param modules list of modules + * @return null when all modules are connected or list of RuleErrors for missing handlers. + */ + private > @Nullable String setModuleHandlers(String rUID, List modules) { + StringBuilder sb = null; + for (T mm : modules) { + final Module m = mm.unwrap(); + try { + ModuleHandler moduleHandler = getModuleHandler(m, rUID); + if (moduleHandler != null) { + if (mm instanceof WrappedAction) { + ((WrappedAction) mm).setModuleHandler((ActionHandler) moduleHandler); + } else if (mm instanceof WrappedCondition) { + ((WrappedCondition) mm).setModuleHandler((ConditionHandler) moduleHandler); + } else if (mm instanceof WrappedTrigger) { + ((WrappedTrigger) mm).setModuleHandler((TriggerHandler) moduleHandler); + } + } else { + if (sb == null) { + sb = new StringBuilder(); + } + String message = "Missing handler '" + m.getTypeUID() + "' for module '" + m.getId() + "'"; + sb.append(message).append("\n"); + logger.trace(message); + } + } catch (Throwable t) { + if (sb == null) { + sb = new StringBuilder(); + } + String message = "Getting handler '" + m.getTypeUID() + "' for module '" + m.getId() + "' failed: " + + t.getMessage(); + sb.append(message).append("\n"); + logger.trace(message); + } + } + return sb != null ? sb.toString() : null; + } + + /** + * Gets {@link TriggerHandlerCallback} for passed {@link Rule}. If it does not exists, a callback object is + * created. + * + * @param rule rule object for which the callback is looking for. + * @return a {@link TriggerHandlerCallback} corresponding to the passed {@link Rule} object. + */ + private synchronized TriggerHandlerCallbackImpl getTriggerHandlerCallback(String ruleUID) { + TriggerHandlerCallbackImpl result = thCallbacks.get(ruleUID); + if (result == null) { + result = new TriggerHandlerCallbackImpl(this, ruleUID); + thCallbacks.put(ruleUID, result); + } + return result; + } + + /** + * Unlink module handlers from their modules. The method is called when the rule containing these modules goes into + * {@link RuleStatus#UNINITIALIZED} state. + * + * @param modules list of modules which should be disconnected. + */ + private > void removeModuleHandlers(List modules, String ruleUID) { + for (T mm : modules) { + final Module m = mm.unwrap(); + ModuleHandler handler = mm.getModuleHandler(); + + if (handler != null) { + ModuleHandlerFactory factory = getModuleHandlerFactory(m.getTypeUID()); + if (factory != null) { + factory.ungetHandler(m, ruleUID, handler); + } + mm.setModuleHandler(null); + } + } + } + + /** + * This method register the Rule to start working. This is the final step of initialization process where + * triggers received {@link TriggerHandlerCallback}s object and starts to notify the rule engine when they are + * triggered. After activating all triggers the rule goes into IDLE state. + * + * @param rule an initialized rule which has to starts tracking the triggers. + */ + private void register(WrappedRule rule) { + final String ruleUID = rule.getUID(); + + TriggerHandlerCallback thCallback = getTriggerHandlerCallback(ruleUID); + rule.getTriggers().forEach(trigger -> { + TriggerHandler triggerHandler = trigger.getModuleHandler(); + if (triggerHandler != null) { + triggerHandler.setCallback(thCallback); + } + }); + rule.getConditions().forEach(condition -> { + ConditionHandler conditionHandler = condition.getModuleHandler(); + if (conditionHandler != null) { + conditionHandler.setCallback(moduleHandlerCallback); + } + }); + rule.getActions().forEach(action -> { + ActionHandler actionHandler = action.getModuleHandler(); + if (actionHandler != null) { + actionHandler.setCallback(moduleHandlerCallback); + } + }); + } + + /** + * This method unregister a {@link Rule} and it stops working. It is called when some + * {@link ModuleHandlerFactory} is disposed or some {@link ModuleType} is updated. The {@link Rule} is + * available but its state should become {@link RuleStatus#UNINITIALIZED}. + * + * @param r rule that should be unregistered. + * @param detail provides the {@link RuleStatusDetail}, corresponding to the new uninitialized status, should + * be {@code null} if the status will be skipped. + * @param msg provides the {@link RuleStatusInfo} description, corresponding to the new uninitialized + * status, should be {@code null} if the status will be skipped. + */ + private void unregister(@Nullable WrappedRule r, @Nullable RuleStatusDetail detail, @Nullable String msg) { + if (r != null) { + unregister(r); + setStatus(r.getUID(), new RuleStatusInfo(RuleStatus.UNINITIALIZED, detail, msg)); + } + } + + /** + * This method unregister a {@link Rule} and it stops working. It is called when the {@link Rule} is + * removed, updated or disabled. Also it is called when some {@link ModuleHandlerFactory} is disposed or some + * {@link ModuleType} is updated. + * + * @param r rule that should be unregistered. + */ + private void unregister(WrappedRule r) { + String rUID = r.getUID(); + synchronized (this) { + TriggerHandlerCallbackImpl callback = thCallbacks.remove(rUID); + if (callback != null) { + callback.dispose(); + } + } + removeModuleHandlers(r.getModules(), rUID); + } + + /** + * This method is used to obtain a {@link ModuleHandler} for the specified {@link ModuleImpl}. + * + * @param m the {@link ModuleImpl} which is looking for a handler. + * @param ruleUID UID of the {@link Rule} that the specified {@link ModuleImpl} belongs to. + * @return handler that processing this module. Could be {@code null} if the {@link ModuleHandlerFactory} is not + * available. + */ + private @Nullable ModuleHandler getModuleHandler(Module m, String ruleUID) { + String moduleTypeId = m.getTypeUID(); + ModuleHandlerFactory mhf = getModuleHandlerFactory(moduleTypeId); + if (mhf == null || mtRegistry.get(moduleTypeId) == null) { + return null; + } + return mhf.getHandler(m, ruleUID); + } + + /** + * Gets the {@link ModuleHandlerFactory} for the {@link ModuleType} with the specified UID. + * + * @param moduleTypeId the UID of the {@link ModuleType}. + * @return the {@link ModuleHandlerFactory} responsible for the {@link ModuleType}. + */ + public @Nullable ModuleHandlerFactory getModuleHandlerFactory(String moduleTypeId) { + ModuleHandlerFactory mhf = null; + synchronized (this) { + mhf = moduleHandlerFactories.get(moduleTypeId); + } + if (mhf == null) { + ModuleType mt = mtRegistry.get(moduleTypeId); + if (mt instanceof CompositeTriggerType || // + mt instanceof CompositeConditionType || // + mt instanceof CompositeActionType) { + mhf = compositeFactory; + } + } + return mhf; + } + + /** + * Updates the {@link ModuleType} to {@link Rule}s mapping. The method adds the {@link Rule}'s UID to the + * list of + * {@link Rule}s that use this {@link ModuleType}. + * + * @param rUID the UID of the {@link Rule}. + * @param moduleTypeId the UID of the {@link ModuleType}. + */ + public synchronized void updateMapModuleTypeToRule(String rUID, String moduleTypeId) { + Set rules = mapModuleTypeToRules.get(moduleTypeId); + if (rules == null) { + rules = new HashSet(11); + } + rules.add(rUID); + mapModuleTypeToRules.put(moduleTypeId, rules); + } + + /** + * This method removes Rule from the rule engine. + * + * @param rUID id of removed {@link Rule} + * @return true when a rule is deleted, false when there is no rule with such id. + */ + protected boolean removeRule(String rUID) { + final WrappedRule r = managedRules.remove(rUID); + if (r != null) { + unregister(r); + synchronized (this) { + for (Iterator>> it = mapModuleTypeToRules.entrySet().iterator(); it + .hasNext();) { + Map.Entry> e = it.next(); + Set rules = e.getValue(); + if (rules != null && rules.contains(rUID)) { + rules.remove(rUID); + if (rules.size() < 1) { + it.remove(); + } + } + } + } + scheduleTasks.remove(rUID); + return true; + } + return false; + } + + /** + * Gets {@link Rule} corresponding to the passed id. This method is used internally and it does not create a + * copy of the rule. + * + * @param rUID unique id of the {@link Rule} + * @return internal {@link Rule} object + */ + private @Nullable WrappedRule getManagedRule(String rUID) { + return managedRules.get(rUID); + } + + protected @Nullable Rule getRule(String rUID) { + final WrappedRule managedRule = getManagedRule(rUID); + return managedRule != null ? managedRule.unwrap() : null; + } + + @Override + public synchronized void setEnabled(String uid, boolean enable) { + final WrappedRule rule = managedRules.get(uid); + if (rule == null) { + throw new IllegalArgumentException(String.format("No rule with id=%s was found!", uid)); + } + if (enable) { + if (disabledRulesStorage != null) { + disabledRulesStorage.remove(uid); + } + if (getStatus(rule.getUID()) == RuleStatus.UNINITIALIZED) { + register(rule); + // change status to IDLE + setStatus(rule.getUID(), new RuleStatusInfo(RuleStatus.IDLE)); + } + } else { + if (disabledRulesStorage != null) { + disabledRulesStorage.put(uid, true); + } + unregister(rule, RuleStatusDetail.DISABLED, null); + } + } + + @Override + public @Nullable RuleStatusInfo getStatusInfo(String ruleUID) { + if (ruleUID == null) { + return null; + } + final WrappedRule rule = managedRules.get(ruleUID); + if (rule == null) { + return null; + } + return rule.getStatusInfo(); + } + + @Override + public @Nullable RuleStatus getStatus(String ruleUID) { + RuleStatusInfo statusInfo = getStatusInfo(ruleUID); + return statusInfo == null ? null : statusInfo.getStatus(); + } + + @Override + public @Nullable Boolean isEnabled(String ruleUID) { + return getStatus(ruleUID) == null ? null + : !getStatusInfo(ruleUID).getStatusDetail().equals(RuleStatusDetail.DISABLED); + } + + /** + * This method updates the status of the {@link Rule} + * + * @param ruleUID unique id of the rule + * @param newStatusInfo the new status of the rule + */ + private void setStatus(String ruleUID, RuleStatusInfo newStatusInfo) { + final WrappedRule rule = managedRules.get(ruleUID); + if (rule == null) { + return; + } + rule.setStatusInfo(newStatusInfo); + postRuleStatusInfoEvent(ruleUID, newStatusInfo); + } + + /** + * Creates and schedules a re-initialization task for the {@link Rule} with the specified UID. + * + * @param rUID the UID of the {@link Rule}. + */ + protected void scheduleRuleInitialization(final String rUID) { + Future f = scheduleTasks.get(rUID); + if (f == null || f.isDone()) { + ScheduledExecutorService ex = getScheduledExecutor(); + f = ex.schedule(new Runnable() { + @Override + public void run() { + final WrappedRule managedRule = getManagedRule(rUID); + if (managedRule == null) { + return; + } + setRule(managedRule); + } + }, scheduleReinitializationDelay, TimeUnit.MILLISECONDS); + scheduleTasks.put(rUID, f); + } + } + + private void removeMissingModuleTypes(Collection moduleTypes) { + Map> mapMissingHandlers = null; + for (Iterator it = moduleTypes.iterator(); it.hasNext();) { + String moduleTypeName = it.next(); + Set rules = null; + synchronized (this) { + rules = mapModuleTypeToRules.get(moduleTypeName); + } + if (rules != null) { + for (String rUID : rules) { + RuleStatus ruleStatus = getRuleStatus(rUID); + if (ruleStatus == null) { + continue; + } + switch (ruleStatus) { + case RUNNING: + case IDLE: + mapMissingHandlers = mapMissingHandlers != null ? mapMissingHandlers + : new HashMap>(20); + List list = mapMissingHandlers.get(rUID); + if (list == null) { + list = new ArrayList(5); + } + list.add(moduleTypeName); + mapMissingHandlers.put(rUID, list); + break; + default: + break; + } + } + } + } // for + if (mapMissingHandlers != null) { + for (Entry> e : mapMissingHandlers.entrySet()) { + String rUID = e.getKey(); + List missingTypes = e.getValue(); + StringBuffer sb = new StringBuffer(); + sb.append("Missing handlers: "); + for (String typeUID : missingTypes) { + sb.append(typeUID).append(", "); + } + unregister(getManagedRule(rUID), RuleStatusDetail.HANDLER_MISSING_ERROR, + sb.substring(0, sb.length() - 2)); + } + } + } + + /** + * This method runs a {@link Rule}. It is called by the {@link TriggerHandlerCallback}'s thread when a new + * {@link TriggerData} is available. This method switches + * + * @param ruleUID the {@link Rule} which has to evaluate new {@link TriggerData}. + * @param td {@link TriggerData} object containing new values for {@link Trigger}'s {@link Output}s + */ + protected void runRule(String ruleUID, TriggerHandlerCallbackImpl.TriggerData td) { + if (thCallbacks.get(ruleUID) == null) { + // the rule was unregistered + return; + } + synchronized (this) { + final RuleStatus ruleStatus = getRuleStatus(ruleUID); + if (ruleStatus != RuleStatus.IDLE) { + logger.error("Failed to execute rule ‘{}' with status '{}'", ruleUID, ruleStatus.name()); + return; + } + // change state to RUNNING + setStatus(ruleUID, new RuleStatusInfo(RuleStatus.RUNNING)); + } + try { + clearContext(ruleUID); + + setTriggerOutputs(ruleUID, td); + final WrappedRule rule = managedRules.get(ruleUID); + boolean isSatisfied = calculateConditions(rule); + if (isSatisfied) { + executeActions(rule, true); + logger.debug("The rule '{}' is executed.", ruleUID); + } else { + logger.debug("The rule '{}' is NOT executed, since it has unsatisfied conditions.", ruleUID); + } + } catch (Throwable t) { + logger.error("Failed to execute rule '{}': {}", ruleUID, t.getMessage()); + logger.debug("", t); + } + // change state to IDLE only if the rule has not been DISABLED. + synchronized (this) { + if (getRuleStatus(ruleUID) == RuleStatus.RUNNING) { + setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE)); + } + } + } + + @Override + public void runNow(String ruleUID, boolean considerConditions, @Nullable Map context) { + final WrappedRule rule = getManagedRule(ruleUID); + if (rule == null) { + logger.warn("Failed to execute rule '{}': Invalid Rule UID", ruleUID); + return; + } + synchronized (this) { + final RuleStatus ruleStatus = getRuleStatus(ruleUID); + if (ruleStatus != RuleStatus.IDLE) { + logger.error("Failed to execute rule ‘{}' with status '{}'", ruleUID, ruleStatus.name()); + return; + } + // change state to RUNNING + setStatus(ruleUID, new RuleStatusInfo(RuleStatus.RUNNING)); + } + try { + clearContext(ruleUID); + if (context != null && !context.isEmpty()) { + getContext(ruleUID, null).putAll(context); + } + if (considerConditions) { + if (calculateConditions(rule)) { + executeActions(rule, false); + } + } else { + executeActions(rule, false); + } + logger.debug("The rule '{}' is executed.", ruleUID); + } catch (Throwable t) { + logger.error("Failed to execute rule '{}': ", ruleUID, t); + } + // change state to IDLE only if the rule has not been DISABLED. + synchronized (this) { + if (getRuleStatus(ruleUID) == RuleStatus.RUNNING) { + setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE)); + } + } + } + + @Override + public void runNow(String ruleUID) { + runNow(ruleUID, false, null); + } + + /** + * Clears all dynamic parameters from the {@link Rule}'s context. + * + * @param ruleUID the UID of the rule whose context must be cleared. + */ + protected void clearContext(String ruleUID) { + Map context = contextMap.get(ruleUID); + if (context != null) { + context.clear(); + } + } + + /** + * The method updates {@link Output} of the {@link Trigger} with a new triggered data. + * + * @param td new Triggered data. + */ + private void setTriggerOutputs(String ruleUID, TriggerData td) { + Trigger t = td.getTrigger(); + updateContext(ruleUID, t.getId(), td.getOutputs()); + } + + /** + * Updates current context of rule engine. + * + * @param moduleUID uid of updated module. + * + * @param outputs new output values. + */ + private void updateContext(String ruleUID, String moduleUID, Map outputs) { + Map context = getContext(ruleUID, null); + if (outputs != null) { + for (Map.Entry entry : outputs.entrySet()) { + String key = moduleUID + OUTPUT_SEPARATOR + entry.getKey(); + context.put(key, entry.getValue()); + } + } + } + + /** + * @return copy of current context in rule engine + */ + private Map getContext(String ruleUID, @Nullable Set connections) { + @NonNullByDefault({}) + Map context = contextMap.get(ruleUID); + if (context == null) { + context = new HashMap(); + contextMap.put(ruleUID, context); + } + if (connections != null) { + StringBuffer sb = new StringBuffer(); + for (Connection c : connections) { + String outputModuleId = c.getOutputModuleId(); + if (outputModuleId != null) { + sb.append(outputModuleId).append(OUTPUT_SEPARATOR).append(c.getOutputName()); + Object outputValue = context.get(sb.toString()); + sb.setLength(0); + if (outputValue != null) { + if (c.getReference() == null) { + context.put(c.getInputName(), outputValue); + } else { + context.put(c.getInputName(), ReferenceResolver.resolveComplexDataReference(outputValue, + ReferenceResolver.splitReferenceToTokens(c.getReference()))); + } + } + } else { + // get reference from context + String ref = c.getReference(); + final Object value = ReferenceResolver.resolveReference(ref, context); + + if (value != null) { + context.put(c.getInputName(), value); + } + } + } + } + return context; + } + + /** + * This method checks if all rule's condition are satisfied or not. + * + * @param rule the checked rule + * @return true when all conditions of the rule are satisfied, false otherwise. + */ + private boolean calculateConditions(WrappedRule rule) { + List conditions = rule.getConditions(); + if (conditions.size() == 0) { + return true; + } + final String ruleUID = rule.getUID(); + RuleStatus ruleStatus = null; + for (Iterator it = conditions.iterator(); it.hasNext();) { + ruleStatus = getRuleStatus(ruleUID); + if (ruleStatus != RuleStatus.RUNNING) { + return false; + } + final WrappedCondition managedCondition = it.next(); + final Condition condition = managedCondition.unwrap(); + ConditionHandler tHandler = managedCondition.getModuleHandler(); + Map context = getContext(ruleUID, managedCondition.getConnections()); + if (tHandler != null && !tHandler.isSatisfied(Collections.unmodifiableMap(context))) { + logger.debug("The condition '{}' of rule '{}' is unsatisfied.", + new Object[] { condition.getId(), ruleUID }); + return false; + } + } + return true; + } + + /** + * This method evaluates actions of the {@link Rule} and set their {@link Output}s when they exists. + * + * @param rule executed rule. + */ + private void executeActions(WrappedRule rule, boolean stopOnFirstFail) { + final String ruleUID = rule.getUID(); + final Collection actions = rule.getActions(); + if (actions.size() == 0) { + return; + } + RuleStatus ruleStatus = null; + for (Iterator it = actions.iterator(); it.hasNext();) { + ruleStatus = getRuleStatus(ruleUID); + if (ruleStatus != RuleStatus.RUNNING) { + return; + } + final WrappedAction managedAction = it.next(); + final Action action = managedAction.unwrap(); + ActionHandler aHandler = managedAction.getModuleHandler(); + if (aHandler != null) { + Map context = getContext(ruleUID, managedAction.getConnections()); + try { + Map outputs = aHandler.execute(Collections.unmodifiableMap(context)); + if (outputs != null) { + context = getContext(ruleUID, null); + updateContext(ruleUID, action.getId(), outputs); + } + } catch (Throwable t) { + String errMessage = "Fail to execute action: " + action.getId(); + if (stopOnFirstFail) { + RuntimeException re = new RuntimeException(errMessage, t); + throw re; + } else { + logger.warn(errMessage, t); + } + } + } + } + } + + /** + * The method cleans used resources by rule engine when it is deactivated. + */ + @Deactivate + protected void deactivate() { + synchronized (this) { + if (isDisposed) { + return; + } + isDisposed = true; + } + if (compositeFactory != null) { + compositeFactory.deactivate(); + compositeFactory = null; + } + for (Future f : scheduleTasks.values()) { + f.cancel(true); + } + if (scheduleTasks.isEmpty() && executor != null) { + executor.shutdown(); + executor = null; + } + scheduleTasks.clear(); + contextMap.clear(); + unsetRuleRegistry(ruleRegistry); + } + + /** + * This method gets rule's status object. + * + * @param rUID rule's UID + * @return status of the rule or null when such rule does not exists. + */ + protected @Nullable RuleStatus getRuleStatus(String rUID) { + RuleStatusInfo info = getStatusInfo(rUID); + if (info != null) { + return info.getStatus(); + } + return null; + } + + private ScheduledExecutorService getScheduledExecutor() { + final ScheduledExecutorService currentExecutor = executor; + if (currentExecutor != null && !currentExecutor.isShutdown()) { + return currentExecutor; + } + final ScheduledExecutorService newExecutor = Executors.newSingleThreadScheduledExecutor(); + executor = newExecutor; + return newExecutor; + } + + /** + * Validates IDs of modules. The module ids must be alphanumeric with only underscores and dashes. + * + * @param rule the rule to validate + * @throws IllegalArgumentException when a module id contains illegal characters + */ + private void validateModuleIDs(WrappedRule rule) { + for (final WrappedModule mm : rule.getModules()) { + final Module m = mm.unwrap(); + String mId = m.getId(); + if (!mId.matches("[A-Za-z0-9_-]*")) { + rule.setStatusInfo(new RuleStatusInfo(RuleStatus.UNINITIALIZED, RuleStatusDetail.INVALID_RULE, + "It is null or not fit to the pattern: [A-Za-z0-9_-]*")); + throw new IllegalArgumentException( + "Invalid module uid: " + mId + ". It is null or not fit to the pattern: [A-Za-z0-9_-]*"); + } + } + } + + /** + * The auto mapping tries to link not connected module inputs to output of other modules. The auto mapping will link + * input to output only when following criteria are done: 1) input must not be connected. The auto mapping will not + * overwrite explicit connections done by the user. 2) input tags must be subset of the output tags. 3) condition + * inputs can be connected only to triggers' outputs 4) action outputs can be connected to both conditions and + * actions + * outputs 5) There is only one output, based on previous criteria, where the input can connect to. If more then one + * candidate outputs exists for connection, this is a conflict and the auto mapping leaves the input unconnected. + * Auto mapping is always applied when the rule is added or updated. It changes initial value of inputs of + * conditions and actions participating in the rule. If an "auto map" connection has to be removed, the tags of + * corresponding input/output have to be changed. + * + * @param rule updated rule + */ + private void autoMapConnections(WrappedRule rule) { + Map, OutputRef> triggerOutputTags = new HashMap, OutputRef>(11); + for (WrappedTrigger mt : rule.getTriggers()) { + final Trigger t = mt.unwrap(); + TriggerType tt = (TriggerType) mtRegistry.get(t.getTypeUID()); + if (tt != null) { + initTagsMap(t.getId(), tt.getOutputs(), triggerOutputTags); + } + } + Map, OutputRef> actionOutputTags = new HashMap, OutputRef>(11); + for (WrappedAction ma : rule.getActions()) { + final Action a = ma.unwrap(); + ActionType at = (ActionType) mtRegistry.get(a.getTypeUID()); + if (at != null) { + initTagsMap(a.getId(), at.getOutputs(), actionOutputTags); + } + } + // auto mapping of conditions + if (!triggerOutputTags.isEmpty()) { + for (WrappedCondition mc : rule.getConditions()) { + final Condition c = mc.unwrap(); + boolean isConnectionChanged = false; + ConditionType ct = (ConditionType) mtRegistry.get(c.getTypeUID()); + if (ct != null) { + Set connections = copyConnections(mc.getConnections()); + + for (Input input : ct.getInputs()) { + if (isConnected(input, connections)) { + continue; // the input is already connected. Skip it. + } + if (addAutoMapConnections(input, triggerOutputTags, connections)) { + isConnectionChanged = true; + } + } + if (isConnectionChanged) { + // update condition inputs + Map connectionMap = getConnectionMap(connections); + mc.setInputs(connectionMap); + mc.setConnections(connections); + } + } + } + } + // auto mapping of actions + if (!triggerOutputTags.isEmpty() || !actionOutputTags.isEmpty()) { + for (final WrappedAction ma : rule.getActions()) { + final Action a = ma.unwrap(); + boolean isConnectionChanged = false; + ActionType at = (ActionType) mtRegistry.get(a.getTypeUID()); + if (at != null) { + Set connections = copyConnections(ma.getConnections()); + for (Input input : at.getInputs()) { + if (isConnected(input, connections)) { + continue; // the input is already connected. Skip it. + } + if (addAutoMapConnections(input, triggerOutputTags, connections)) { + isConnectionChanged = true; + } + if (addAutoMapConnections(input, actionOutputTags, connections)) { + isConnectionChanged = true; + } + } + if (isConnectionChanged) { + // update condition inputs + Map connectionMap = getConnectionMap(connections); + ma.setInputs(connectionMap); + ma.setConnections(connections); + } + } + } + } + } + + /** + * Try to connect a free input to available outputs. + * + * @param input a free input which has to be connected + * @param outputTagMap a map of set of tags to outptu references + * @param currentConnections current connections of this module + * @return true when only one output which meets auto mapping criteria is found. False otherwise. + */ + private boolean addAutoMapConnections(Input input, Map, OutputRef> outputTagMap, + Set currentConnections) { + boolean result = false; + Set inputTags = input.getTags(); + OutputRef outputRef = null; + boolean conflict = false; + if (inputTags.size() > 0) { + for (Set outTags : outputTagMap.keySet()) { + if (outTags.containsAll(inputTags)) { // input tags must be subset of the output ones + if (outputRef == null) { + outputRef = outputTagMap.get(outTags); + } else { + conflict = true; // already exist candidate for autoMap + break; + } + } + } + if (!conflict && outputRef != null) { + currentConnections + .add(new Connection(input.getName(), outputRef.getModuleId(), outputRef.getOutputName(), null)); + result = true; + } + } + return result; + } + + private void initTagsMap(String moduleId, List outputs, Map, OutputRef> tagMap) { + for (Output output : outputs) { + Set tags = output.getTags(); + if (tags.size() > 0) { + if (tagMap.get(tags) != null) { + // this set of output tags already exists. (conflict) + tagMap.remove(tags); + } else { + tagMap.put(tags, new OutputRef(moduleId, output.getName())); + } + } + } + } + + private boolean isConnected(Input input, Set connections) { + for (Connection connection : connections) { + if (connection.getInputName().equals(input.getName())) { + return true; + } + } + return false; + } + + private Map getConnectionMap(Set connections) { + Map connectionMap = new HashMap<>(); + for (Connection connection : connections) { + connectionMap.put(connection.getInputName(), + connection.getOutputModuleId() + "." + connection.getOutputName()); + } + return connectionMap; + } + + /** + * Utility method creating deep copy of passed connection set. + * + * @param connections connections used by this module. + * @return copy of passed connections. + */ + private Set copyConnections(Set connections) { + Set result = new HashSet<>(connections.size()); + for (Iterator it = connections.iterator(); it.hasNext();) { + Connection c = it.next(); + result.add(new Connection(c.getInputName(), c.getOutputModuleId(), c.getOutputName(), c.getReference())); + } + return result; + } + + class OutputRef { + + private final String moduleId; + private final String outputName; + + public OutputRef(String moduleId, String outputName) { + this.moduleId = moduleId; + this.outputName = outputName; + } + + public String getModuleId() { + return moduleId; + } + + public String getOutputName() { + return outputName; + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEventFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEventFactory.java new file mode 100644 index 00000000000..3046c8e9c88 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEventFactory.java @@ -0,0 +1,185 @@ +/** + * 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.internal; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.eclipse.smarthome.core.events.AbstractEventFactory; +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventFactory; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleDTOMapper; +import org.openhab.core.automation.events.RuleAddedEvent; +import org.openhab.core.automation.events.RuleRemovedEvent; +import org.openhab.core.automation.events.RuleStatusInfoEvent; +import org.openhab.core.automation.events.RuleUpdatedEvent; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is a factory that creates Rule Events. + * + * @author Benedikt Niehues - initial contribution + * @author Markus Rathgeb - Use the DTO for the Rule representation + */ +@Component(service = EventFactory.class, immediate = true) +public class RuleEventFactory extends AbstractEventFactory { + + private final Logger logger = LoggerFactory.getLogger(RuleEventFactory.class); + + private static final String RULE_STATE_EVENT_TOPIC = "smarthome/rules/{ruleID}/state"; + + private static final String RULE_ADDED_EVENT_TOPIC = "smarthome/rules/{ruleID}/added"; + + private static final String RULE_REMOVED_EVENT_TOPIC = "smarthome/rules/{ruleID}/removed"; + + private static final String RULE_UPDATED_EVENT_TOPIC = "smarthome/rules/{ruleID}/updated"; + + private static final Set SUPPORTED_TYPES = new HashSet<>(); + + static { + SUPPORTED_TYPES.add(RuleAddedEvent.TYPE); + SUPPORTED_TYPES.add(RuleRemovedEvent.TYPE); + SUPPORTED_TYPES.add(RuleStatusInfoEvent.TYPE); + SUPPORTED_TYPES.add(RuleUpdatedEvent.TYPE); + } + + public RuleEventFactory() { + super(SUPPORTED_TYPES); + } + + @Override + protected Event createEventByType(String eventType, String topic, String payload, String source) throws Exception { + logger.trace("creating ruleEvent of type: {}", eventType); + if (eventType == null) { + return null; + } + if (eventType.equals(RuleAddedEvent.TYPE)) { + return createRuleAddedEvent(topic, payload, source); + } else if (eventType.equals(RuleRemovedEvent.TYPE)) { + return createRuleRemovedEvent(topic, payload, source); + } else if (eventType.equals(RuleStatusInfoEvent.TYPE)) { + return createRuleStatusInfoEvent(topic, payload, source); + } else if (eventType.equals(RuleUpdatedEvent.TYPE)) { + return createRuleUpdatedEvent(topic, payload, source); + } + return null; + } + + private Event createRuleUpdatedEvent(String topic, String payload, String source) { + RuleDTO[] ruleDTO = deserializePayload(payload, RuleDTO[].class); + if (ruleDTO.length != 2) { + throw new IllegalArgumentException("Creation of RuleUpdatedEvent failed: invalid payload: " + payload); + } + return new RuleUpdatedEvent(topic, payload, source, ruleDTO[0], ruleDTO[1]); + } + + private Event createRuleStatusInfoEvent(String topic, String payload, String source) { + RuleStatusInfo statusInfo = deserializePayload(payload, RuleStatusInfo.class); + return new RuleStatusInfoEvent(topic, payload, source, statusInfo, getRuleId(topic)); + } + + private Event createRuleRemovedEvent(String topic, String payload, String source) { + RuleDTO ruleDTO = deserializePayload(payload, RuleDTO.class); + return new RuleRemovedEvent(topic, payload, source, ruleDTO); + } + + private Event createRuleAddedEvent(String topic, String payload, String source) { + RuleDTO ruleDTO = deserializePayload(payload, RuleDTO.class); + return new RuleAddedEvent(topic, payload, source, ruleDTO); + } + + private String getRuleId(String topic) { + String[] topicElements = getTopicElements(topic); + if (topicElements.length != 4) { + throw new IllegalArgumentException("Event creation failed, invalid topic: " + topic); + } + return topicElements[2]; + } + + /** + * Creates a rule updated event. + * + * @param rule the new rule. + * @param oldRule the rule that has been updated. + * @param source the source of the event. + * @return {@link RuleUpdatedEvent} instance. + */ + public static RuleUpdatedEvent createRuleUpdatedEvent(Rule rule, Rule oldRule, String source) { + String topic = buildTopic(RULE_UPDATED_EVENT_TOPIC, rule); + final RuleDTO ruleDto = RuleDTOMapper.map(rule); + final RuleDTO oldRuleDto = RuleDTOMapper.map(oldRule); + List rules = new LinkedList<>(); + rules.add(ruleDto); + rules.add(oldRuleDto); + String payload = serializePayload(rules); + return new RuleUpdatedEvent(topic, payload, source, ruleDto, oldRuleDto); + } + + /** + * Creates a rule status info event. + * + * @param statusInfo the status info of the event. + * @param ruleUID the UID of the rule for which the event is created. + * @param source the source of the event. + * @return {@link RuleStatusInfoEvent} instance. + */ + public static RuleStatusInfoEvent createRuleStatusInfoEvent(RuleStatusInfo statusInfo, String ruleUID, + String source) { + String topic = buildTopic(RULE_STATE_EVENT_TOPIC, ruleUID); + String payload = serializePayload(statusInfo); + return new RuleStatusInfoEvent(topic, payload, source, statusInfo, ruleUID); + } + + /** + * Creates a rule removed event. + * + * @param rule the rule for which this event is created. + * @param source the source of the event. + * @return {@link RuleRemovedEvent} instance. + */ + public static RuleRemovedEvent createRuleRemovedEvent(Rule rule, String source) { + String topic = buildTopic(RULE_REMOVED_EVENT_TOPIC, rule); + final RuleDTO ruleDto = RuleDTOMapper.map(rule); + String payload = serializePayload(ruleDto); + return new RuleRemovedEvent(topic, payload, source, ruleDto); + } + + /** + * Creates a rule added event. + * + * @param rule the rule for which this event is created. + * @param source the source of the event. + * @return {@link RuleAddedEvent} instance. + */ + public static RuleAddedEvent createRuleAddedEvent(Rule rule, String source) { + String topic = buildTopic(RULE_ADDED_EVENT_TOPIC, rule); + final RuleDTO ruleDto = RuleDTOMapper.map(rule); + String payload = serializePayload(ruleDto); + return new RuleAddedEvent(topic, payload, source, ruleDto); + } + + private static String buildTopic(String topic, String ruleUID) { + return topic.replace("{ruleID}", ruleUID); + } + + private static String buildTopic(String topic, Rule rule) { + return buildTopic(topic, rule.getUID()); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java new file mode 100644 index 00000000000..019361c2ac4 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleImpl.java @@ -0,0 +1,299 @@ +/** + * 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.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +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.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.template.RuleTemplate; + +/** + * This is the internal implementation of a {@link Rule}, which comes with full getters and setters. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Initial Contribution + * @author Vasil Ilchev - Initial Contribution + * @author Kai Kreuzer - Introduced transient status and made it implement the Rule interface + */ +@NonNullByDefault +public class RuleImpl implements Rule { + + protected @NonNullByDefault({}) List triggers; + protected @NonNullByDefault({}) List conditions; + protected @NonNullByDefault({}) List actions; + protected @NonNullByDefault({}) Configuration configuration; + protected @NonNullByDefault({}) List configDescriptions; + protected @Nullable String templateUID; + protected @NonNullByDefault({}) String uid; + protected @Nullable String name; + protected @NonNullByDefault({}) Set tags; + protected @NonNullByDefault({}) Visibility visibility; + protected @Nullable String description; + + /** + * Constructor for creating an empty {@link Rule} with a specified rule identifier. + * When {@code null} is passed for the {@code uid} parameter, the {@link Rule}'s identifier will be randomly + * generated. + * + * @param uid the rule's identifier, or {@code null} if a random identifier should be generated. + */ + public RuleImpl(@Nullable String uid) { + this(uid, null, null, null, null, null, null, null, null, null, null); + } + + /** + * Utility constructor for creating a {@link Rule} from a set of modules, or from a template. + * When {@code null} is passed for the {@code uid} parameter, the {@link Rule}'s identifier will be randomly + * generated. + * + * @param uid the {@link Rule}'s identifier, or {@code null} if a random identifier should be generated. + * @param name the rule's name + * @param description the rule's description + * @param tags the tags + * @param triggers the {@link Rule}'s triggers list, or {@code null} if the {@link Rule} should have no triggers or + * will be created from a template. + * @param conditions the {@link Rule}'s conditions list, or {@code null} if the {@link Rule} should have no + * conditions, or will be created from a template. + * @param actions the {@link Rule}'s actions list, or {@code null} if the {@link Rule} should have no actions, or + * will be created from a template. + * @param configDescriptions metadata describing the configuration of the {@link Rule}. + * @param configuration the values that will configure the modules of the {@link Rule}. + * @param templateUID the {@link RuleTemplate} identifier of the template that will be used by the + * {@link RuleRegistry} to validate the {@link Rule}'s configuration, as well as to create and configure + * the {@link Rule}'s modules, or null if the {@link Rule} should not be created from a template. + * @param visibility the {@link Rule}'s visibility + */ + public RuleImpl(@Nullable String uid, final @Nullable String name, final @Nullable String description, + final @Nullable Set tags, @Nullable List triggers, @Nullable List conditions, + @Nullable List actions, @Nullable List configDescriptions, + @Nullable Configuration configuration, @Nullable String templateUID, @Nullable Visibility visibility) { + this.uid = uid == null ? UUID.randomUUID().toString() : uid; + this.name = name; + this.description = description; + this.tags = tags == null ? Collections.emptySet() : Collections.unmodifiableSet(new HashSet<>(tags)); + this.triggers = triggers == null ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(triggers)); + this.conditions = conditions == null ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(conditions)); + this.actions = actions == null ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(actions)); + this.configDescriptions = configDescriptions == null ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(configDescriptions)); + this.configuration = configuration == null ? new Configuration() + : new Configuration(configuration.getProperties()); + this.templateUID = templateUID; + this.visibility = visibility == null ? Visibility.VISIBLE : visibility; + } + + @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 RuleImpl}: to validate the {@link RuleImpl}'s configuration, as well + * as to create and configure the {@link RuleImpl}'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 RuleImpl}'s human-readable name. + * + * @param ruleName the {@link RuleImpl}'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 RuleImpl}'s assigned tags. + * + * @param ruleTags the {@link RuleImpl}'s assigned tags. + */ + public void setTags(@Nullable Set ruleTags) { + tags = ruleTags == null ? Collections.emptySet() : Collections.unmodifiableSet(ruleTags); + } + + @Override + @Nullable + public String getDescription() { + return description; + } + + /** + * This method is used to specify human-readable description of the purpose and consequences of the + * {@link RuleImpl}'s execution. + * + * @param ruleDescription the {@link RuleImpl}'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 RuleImpl}'s {@link Visibility}. + * + * @param visibility the {@link RuleImpl}'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 RuleImpl}'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 RuleImpl}. + */ + public void setConfigurationDescriptions(@Nullable List configDescriptions) { + this.configDescriptions = configDescriptions == null ? Collections.emptyList() + : Collections.unmodifiableList(configDescriptions); + } + + @Override + public List getConditions() { + return conditions; + } + + /** + * This method is used to specify the conditions participating in {@link RuleImpl}. + * + * @param conditions a list with the conditions that should belong to this {@link RuleImpl}. + */ + public void setConditions(@Nullable List conditions) { + this.conditions = conditions == null ? Collections.emptyList() : Collections.unmodifiableList(conditions); + } + + @Override + public List getActions() { + return actions; + } + + @Override + public List getTriggers() { + return triggers; + } + + /** + * This method is used to specify the actions participating in {@link RuleImpl} + * + * @param actions a list with the actions that should belong to this {@link RuleImpl}. + */ + public void setActions(@Nullable List actions) { + this.actions = actions == null ? Collections.emptyList() : Collections.unmodifiableList(actions); + } + + /** + * This method is used to specify the triggers participating in {@link RuleImpl} + * + * @param triggers a list with the triggers that should belong to this {@link RuleImpl}. + */ + public void setTriggers(@Nullable List triggers) { + this.triggers = triggers == null ? Collections.emptyList() : Collections.unmodifiableList(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; + } + + @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 RuleImpl)) { + return false; + } + RuleImpl other = (RuleImpl) obj; + if (!uid.equals(other.uid)) { + return false; + } + return true; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java new file mode 100644 index 00000000000..6a6a656d6ed --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleRegistryImpl.java @@ -0,0 +1,692 @@ +/** + * 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.internal; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type; +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.core.common.registry.AbstractRegistry; +import org.eclipse.smarthome.core.common.registry.Provider; +import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; +import org.eclipse.smarthome.core.events.EventPublisher; +import org.eclipse.smarthome.core.storage.StorageService; +import org.openhab.core.automation.ManagedRuleProvider; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleProvider; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.RuleStatus; +import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.internal.template.RuleTemplateRegistry; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.TemplateRegistry; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.util.ConfigurationNormalizer; +import org.openhab.core.automation.util.ReferenceResolver; +import org.openhab.core.automation.util.RuleBuilder; +import org.osgi.framework.BundleContext; +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 is the main implementation of the {@link RuleRegistry}, which is registered as a service. + * The {@link RuleRegistryImpl} provides basic functionality for managing {@link Rule}s. + * It can be used to + *
      + *
    • Add Rules with the {@link #add(Rule)}, {@link #added(Provider, Rule)}, {@link #addProvider(RuleProvider)} + * methods.
    • + *
    • Get the existing rules with the {@link #get(String)}, {@link #getAll()}, {@link #getByTag(String)}, + * {@link #getByTags(String[])} methods.
    • + *
    • Update the existing rules with the {@link #update(Rule)}, {@link #updated(Provider, Rule, Rule)} methods.
    • + *
    • Remove Rules with the {@link #remove(String)} method.
    • + *
    + *

    + * This class also persists the rules into the {@link StorageService} service and restores + * them when the system is restarted. + *

    + * The {@link RuleRegistry} manages the state (enabled or disabled) of the Rules: + *

      + *
    • A newly added Rule is always enabled.
    • + *
    • To check a Rule's state, use the {@link #isEnabled(String)} method.
    • + *
    • To change a Rule's state, use the {@link #setEnabled(String, boolean)} method.
    • + *
    + *

    + * The {@link RuleRegistry} manages the status of the Rules: + *

      + *
    • To check a Rule's status info, use the {@link #getStatusInfo(String)} method.
    • + *
    • The status of a newly added Rule, or a Rule enabled with {@link #setEnabled(String, boolean)}, or an updated + * Rule, is first set to {@link RuleStatus#UNINITIALIZED}.
    • + *
    • After a Rule is added or enabled, or updated, a verification procedure is initiated. If the verification of the + * modules IDs, connections between modules and configuration values of the modules is successful, and the module + * handlers are correctly set, the status is set to {@link RuleStatus#IDLE}.
    • + *
    • If some of the module handlers disappear, the Rule will become {@link RuleStatus#UNINITIALIZED} again.
    • + *
    • If one of the Rule's Triggers is triggered, the Rule becomes {@link RuleStatus#RUNNING}. + * When the execution is complete, it will become {@link RuleStatus#IDLE} again.
    • + *
    • If a Rule is disabled with {@link #setEnabled(String, boolean)}, it's status is set to + * {@link RuleStatus#DISABLED}.
    • + *
    + * + * @author Yordan Mihaylov - Initial Contribution + * @author Ana Dimova - Persistence implementation & updating rules from providers + * @author Kai Kreuzer - refactored (managed) provider and registry implementation and other fixes + * @author Benedikt Niehues - added events for rules + * @author Victor Toni - return only copies of {@link Rule}s + */ +@Component(service = RuleRegistry.class, immediate = true, property = { "rule.reinitialization.delay:Long=500" }) +public class RuleRegistryImpl extends AbstractRegistry + implements RuleRegistry, RegistryChangeListener { + + /** + * Default value of delay between rule's re-initialization tries. + */ + private static final long DEFAULT_REINITIALIZATION_DELAY = 500; + + /** + * Delay between rule's re-initialization tries. + */ + private static final String CONFIG_PROPERTY_REINITIALIZATION_DELAY = "rule.reinitialization.delay"; + + private static final String SOURCE = RuleRegistryImpl.class.getSimpleName(); + + private final Logger logger = LoggerFactory.getLogger(RuleRegistryImpl.class.getName()); + + /** + * Delay between rule's re-initialization tries. + */ + private long scheduleReinitializationDelay; + private ModuleTypeRegistry moduleTypeRegistry; + private RuleTemplateRegistry templateRegistry; + + /** + * {@link Map} of template UIDs to rules where these templates participated. + */ + private final Map> mapTemplateToRules = new HashMap>(); + + /** + * Constructor that is responsible to invoke the super constructor with appropriate providerClazz + * {@link RuleProvider} - the class of the providers that should be tracked automatically after activation. + */ + public RuleRegistryImpl() { + super(RuleProvider.class); + } + + /** + * Activates this component. Called from DS. + * + * @param componentContext this component context. + */ + @Activate + protected void activate(BundleContext bundleContext, Map properties) throws Exception { + modified(properties); + super.activate(bundleContext); + } + + /** + * This method is responsible for updating the value of delay between rule's re-initialization tries. + * + * @param config a {@link Map} containing the new value of delay. + */ + @Modified + protected void modified(Map config) { + Object value = config == null ? null : config.get(CONFIG_PROPERTY_REINITIALIZATION_DELAY); + this.scheduleReinitializationDelay = (value != null && value instanceof Number) ? (((Number) value).longValue()) + : DEFAULT_REINITIALIZATION_DELAY; + if (value != null && !(value instanceof Number)) { + logger.warn("Invalid configuration value: {}. It MUST be Number.", value); + } + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + @Override + protected void setEventPublisher(EventPublisher eventPublisher) { + super.setEventPublisher(eventPublisher); + } + + @Override + protected void unsetEventPublisher(EventPublisher eventPublisher) { + super.unsetEventPublisher(eventPublisher); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, name = "ManagedRuleProvider") + protected void setManagedProvider(ManagedRuleProvider managedProvider) { + super.setManagedProvider(managedProvider); + } + + protected void unsetManagedProvider(ManagedRuleProvider managedProvider) { + super.unsetManagedProvider(managedProvider); + } + + /** + * Bind the {@link ModuleTypeRegistry} service - called from DS. + * + * @param moduleTypeRegistry a {@link ModuleTypeRegistry} service. + */ + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) + protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = moduleTypeRegistry; + } + + /** + * Unbind the {@link ModuleTypeRegistry} service - called from DS. + * + * @param moduleTypeRegistry a {@link ModuleTypeRegistry} service. + */ + protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = null; + } + + /** + * Bind the {@link RuleTemplateRegistry} service - called from DS. + * + * @param templateRegistry a {@link RuleTemplateRegistry} service. + */ + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) + protected void setTemplateRegistry(TemplateRegistry templateRegistry) { + if (templateRegistry instanceof RuleTemplateRegistry) { + this.templateRegistry = (RuleTemplateRegistry) templateRegistry; + templateRegistry.addRegistryChangeListener(this); + } + } + + /** + * Unbind the {@link RuleTemplateRegistry} service - called from DS. + * + * @param templateRegistry a {@link RuleTemplateRegistry} service. + */ + protected void unsetTemplateRegistry(TemplateRegistry templateRegistry) { + if (templateRegistry instanceof RuleTemplateRegistry) { + this.templateRegistry = null; + templateRegistry.removeRegistryChangeListener(this); + } + } + + /** + * This method is used to register a {@link Rule} into the {@link RuleEngineImpl}. First the {@link Rule} become + * {@link RuleStatus#UNINITIALIZED}. + * Then verification procedure will be done and the Rule become {@link RuleStatus#IDLE}. + * If the verification fails, the Rule will stay {@link RuleStatus#UNINITIALIZED}. + * + * @param rule a {@link Rule} instance which have to be added into the {@link RuleEngineImpl}. + * @return a copy of the added {@link Rule} + * @throws RuntimeException + * when passed module has a required configuration property and it is not specified + * in rule definition + * nor + * in the module's module type definition. + * @throws IllegalArgumentException + * when a module id contains dot or when the rule with the same UID already exists. + */ + @Override + public Rule add(Rule rule) { + super.add(rule); + Rule ruleCopy = get(rule.getUID()); + if (ruleCopy == null) { + throw new IllegalStateException(); + } + return ruleCopy; + } + + @Override + protected void notifyListenersAboutAddedElement(Rule element) { + postRuleAddedEvent(element); + postRuleStatusInfoEvent(element.getUID(), new RuleStatusInfo(RuleStatus.UNINITIALIZED)); + super.notifyListenersAboutAddedElement(element); + } + + @Override + protected void notifyListenersAboutUpdatedElement(Rule oldElement, Rule element) { + postRuleUpdatedEvent(element, oldElement); + super.notifyListenersAboutUpdatedElement(oldElement, element); + } + + /** + * @see RuleRegistryImpl#postEvent(org.eclipse.smarthome.core.events.Event) + */ + protected void postRuleAddedEvent(Rule rule) { + postEvent(RuleEventFactory.createRuleAddedEvent(rule, SOURCE)); + } + + /** + * @see RuleRegistryImpl#postEvent(org.eclipse.smarthome.core.events.Event) + */ + protected void postRuleRemovedEvent(Rule rule) { + postEvent(RuleEventFactory.createRuleRemovedEvent(rule, SOURCE)); + } + + /** + * @see RuleRegistryImpl#postEvent(org.eclipse.smarthome.core.events.Event) + */ + protected void postRuleUpdatedEvent(Rule rule, Rule oldRule) { + postEvent(RuleEventFactory.createRuleUpdatedEvent(rule, oldRule, SOURCE)); + } + + /** + * This method can be used in order to post events through the Eclipse SmartHome events bus. A common + * use case is to notify event subscribers about the {@link Rule}'s status change. + * + * @param ruleUID the UID of the {@link Rule}, whose status is changed. + * @param statusInfo the new {@link Rule}s status. + */ + protected void postRuleStatusInfoEvent(String ruleUID, RuleStatusInfo statusInfo) { + postEvent(RuleEventFactory.createRuleStatusInfoEvent(statusInfo, ruleUID, SOURCE)); + } + + @Override + protected void onRemoveElement(Rule rule) { + String uid = rule.getUID(); + String templateUID = rule.getTemplateUID(); + if (templateUID != null) { + updateRuleTemplateMapping(templateUID, uid, true); + } + } + + @Override + protected void notifyListenersAboutRemovedElement(Rule element) { + super.notifyListenersAboutRemovedElement(element); + postRuleRemovedEvent(element); + } + + @Override + public Collection getByTag(String tag) { + Collection result = new LinkedList(); + if (tag == null) { + forEach(result::add); + } else { + forEach(rule -> { + if (rule.getTags().contains(tag)) { + result.add(rule); + } + }); + } + return result; + } + + @Override + public Collection getByTags(String... tags) { + Set tagSet = tags != null ? new HashSet(Arrays.asList(tags)) : null; + Collection result = new LinkedList(); + if (tagSet == null || tagSet.isEmpty()) { + forEach(result::add); + } else { + forEach(rule -> { + if (rule.getTags().containsAll(tagSet)) { + result.add(rule); + } + }); + } + return result; + } + + /** + * The method checks if the rule has to be resolved by template or not. If the rule does not contain tempateUID it + * returns same rule, otherwise it tries to resolve the rule created from template. If the template is available + * the method creates a new rule based on triggers, conditions and actions from template. If the template is not + * available returns the same rule. + * + * @param rule a rule defined by template. + * @return the resolved rule(containing modules defined by the template) or not resolved rule, if the template is + * missing. + */ + private Rule resolveRuleByTemplate(Rule rule) { + String templateUID = rule.getTemplateUID(); + if (templateUID == null) { + return rule; + } + RuleTemplate template = templateRegistry.get(templateUID); + String uid = rule.getUID(); + if (template == null) { + updateRuleTemplateMapping(templateUID, uid, false); + logger.debug("Rule template {} does not exist.", templateUID); + return rule; + } else { + RuleImpl resolvedRule = (RuleImpl) RuleBuilder + .create(template, rule.getUID(), rule.getName(), rule.getConfiguration(), rule.getVisibility()) + .build(); + resolveConfigurations(resolvedRule); + updateRuleTemplateMapping(templateUID, uid, true); + return resolvedRule; + } + } + + /** + * Updates the content of the {@link Map} that maps the template to rules, using it to complete their definitions. + * + * @param templateUID the {@link RuleTemplate}'s UID specifying the template. + * @param ruleUID the {@link Rule}'s UID specifying a rule created by the specified template. + * @param resolved specifies if the {@link Map} should be updated by adding or removing the specified rule + * accordingly if the rule is resolved or not. + */ + private void updateRuleTemplateMapping(String templateUID, String ruleUID, boolean resolved) { + synchronized (this) { + Set ruleUIDs = mapTemplateToRules.get(templateUID); + if (ruleUIDs == null) { + ruleUIDs = new HashSet(); + mapTemplateToRules.put(templateUID, ruleUIDs); + } + if (resolved) { + ruleUIDs.remove(ruleUID); + } else { + ruleUIDs.add(ruleUID); + } + } + } + + @Override + protected void addProvider(Provider provider) { + super.addProvider(provider); + forEach(provider, rule -> { + try { + Rule resolvedRule = resolveRuleByTemplate(rule); + if (rule != resolvedRule && provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } + } catch (IllegalArgumentException e) { + logger.error("Added rule '{}' is invalid", rule.getUID(), e); + } + }); + } + + @Override + public void added(Provider provider, Rule element) { + String ruleUID = element.getUID(); + Rule resolvedRule = element; + try { + resolvedRule = resolveRuleByTemplate(element); + } catch (IllegalArgumentException e) { + logger.debug("Added rule '{}' is invalid", ruleUID, e); + } + super.added(provider, element); + if (element != resolvedRule) { + if (provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } else { + super.updated(provider, element, resolvedRule); + } + } + } + + @Override + public void updated(Provider provider, Rule oldElement, Rule element) { + String uid = element.getUID(); + if (oldElement != null && uid.equals(oldElement.getUID())) { + Rule resolvedRule = element; + try { + resolvedRule = resolveRuleByTemplate(element); + } catch (IllegalArgumentException e) { + logger.error("The rule '{}' is not updated, the new version is invalid", uid, e); + } + if (element != resolvedRule && provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } else { + super.updated(provider, oldElement, resolvedRule); + } + } else { + throw new IllegalArgumentException( + String.format("The rule '%s' is not updated, not matching with any existing rule", uid)); + } + } + + @Override + protected void onAddElement(Rule element) throws IllegalArgumentException { + String uid = element.getUID(); + try { + resolveConfigurations(element); + } catch (IllegalArgumentException e) { + logger.debug("Added rule '{}' is invalid", uid, e); + } + } + + @Override + protected void onUpdateElement(Rule oldElement, Rule element) throws IllegalArgumentException { + String uid = element.getUID(); + try { + resolveConfigurations(element); + } catch (IllegalArgumentException e) { + logger.debug("The new version of updated rule '{}' is invalid", uid, e); + } + } + + /** + * This method serves to resolve and normalize the {@link Rule}s configuration values and its module configurations. + * + * @param rule the {@link Rule}, whose configuration values and module configuration values should be resolved and + * normalized. + */ + private void resolveConfigurations(Rule rule) { + List configDescriptions = rule.getConfigurationDescriptions(); + Configuration configuration = rule.getConfiguration(); + ConfigurationNormalizer.normalizeConfiguration(configuration, + ConfigurationNormalizer.getConfigDescriptionMap(configDescriptions)); + Map configurationProperties = configuration.getProperties(); + if (rule.getTemplateUID() == null) { + String uid = rule.getUID(); + try { + validateConfiguration(configDescriptions, new HashMap<>(configurationProperties)); + resolveModuleConfigReferences(rule.getModules(), configurationProperties); + ConfigurationNormalizer.normalizeModuleConfigurations(rule.getModules(), moduleTypeRegistry); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(String.format("The rule '%s' has incorrect configurations", uid), e); + } + } + } + + /** + * This method serves to validate the {@link Rule}s configuration values. + * + * @param rule the {@link Rule}, whose configuration values should be validated. + */ + private void validateConfiguration(List configDescriptions, + Map configurations) { + if (configurations == null || configurations.isEmpty()) { + if (isOptionalConfig(configDescriptions)) { + return; + } else { + StringBuffer statusDescription = new StringBuffer(); + String msg = " '%s';"; + for (ConfigDescriptionParameter configParameter : configDescriptions) { + if (configParameter.isRequired()) { + String name = configParameter.getName(); + statusDescription.append(String.format(msg, name)); + } + } + throw new IllegalArgumentException( + "Missing required configuration properties: " + statusDescription.toString()); + } + } else { + for (ConfigDescriptionParameter configParameter : configDescriptions) { + String configParameterName = configParameter.getName(); + processValue(configurations.remove(configParameterName), configParameter); + } + if (!configurations.isEmpty()) { + StringBuffer statusDescription = new StringBuffer(); + String msg = " '%s';"; + for (String name : configurations.keySet()) { + statusDescription.append(String.format(msg, name)); + } + throw new IllegalArgumentException("Extra configuration properties: " + statusDescription.toString()); + } + } + } + + /** + * Utility method for {@link Rule}s configuration validation. + * + * @param configDescriptions the meta-data for {@link Rule}s configuration, used for validation. + * @return {@code true} if all configuration properties are optional or {@code false} if there is at least one + * required property. + */ + private boolean isOptionalConfig(List configDescriptions) { + if (configDescriptions != null && !configDescriptions.isEmpty()) { + boolean required = false; + Iterator i = configDescriptions.iterator(); + while (i.hasNext()) { + ConfigDescriptionParameter param = i.next(); + required = required || param.isRequired(); + } + return !required; + } + return true; + } + + /** + * Utility method for {@link Rule}s configuration validation. Validates the value of a configuration property. + * + * @param configValue the value for {@link Rule}s configuration property, that should be validated. + * @param configParameter the meta-data for {@link Rule}s configuration value, used for validation. + */ + private void processValue(Object configValue, ConfigDescriptionParameter configParameter) { + if (configValue != null) { + Type type = configParameter.getType(); + if (configParameter.isMultiple()) { + if (configValue instanceof List) { + @SuppressWarnings("rawtypes") + List lConfigValues = (List) configValue; + for (Object value : lConfigValues) { + if (!checkType(type, value)) { + throw new IllegalArgumentException("Unexpected value for configuration property \"" + + configParameter.getName() + "\". Expected type: " + type); + } + } + } else { + throw new IllegalArgumentException( + "Unexpected value for configuration property \"" + configParameter.getName() + + "\". Expected is Array with type for elements : " + type.toString() + "!"); + } + } else if (!checkType(type, configValue)) { + throw new IllegalArgumentException("Unexpected value for configuration property \"" + + configParameter.getName() + "\". Expected is " + type.toString() + "!"); + } + } else if (configParameter.isRequired()) { + throw new IllegalArgumentException( + "Required configuration property missing: \"" + configParameter.getName() + "\"!"); + } + } + + /** + * Avoid code duplication in {@link #processValue(Object, ConfigDescriptionParameter)} method. + * + * @param type the {@link Type} of a parameter that should be checked. + * @param configValue the value of a parameter that should be checked. + * @return true if the type and value matching or false in the opposite. + */ + private boolean checkType(Type type, Object configValue) { + switch (type) { + case TEXT: + return configValue instanceof String; + case BOOLEAN: + return configValue instanceof Boolean; + case INTEGER: + return configValue instanceof BigDecimal || configValue instanceof Integer + || configValue instanceof Double && ((Double) configValue).intValue() == (Double) configValue; + case DECIMAL: + return configValue instanceof BigDecimal || configValue instanceof Double; + } + return false; + } + + /** + * This method serves to replace module configuration references with the {@link Rule}s configuration values. + * + * @param modules the {@link Rule}'s modules, whose configuration values should be resolved. + * @param ruleConfiguration the {@link Rule}'s configuration values that should be resolve module configuration + * values. + */ + private void resolveModuleConfigReferences(List modules, Map ruleConfiguration) { + if (modules != null) { + StringBuffer statusDescription = new StringBuffer(); + for (Module module : modules) { + try { + ReferenceResolver.updateConfiguration(module.getConfiguration(), ruleConfiguration, logger); + } catch (IllegalArgumentException e) { + statusDescription.append(" in module[" + module.getId() + "]: " + e.getLocalizedMessage() + ";"); + } + } + String statusDescriptionStr = statusDescription.toString(); + if (!statusDescriptionStr.isEmpty()) { + throw new IllegalArgumentException(String.format("Incorrect configurations: %s", statusDescriptionStr)); + } + } + } + + @Override + public void added(RuleTemplate element) { + String templateUID = element.getUID(); + Set rules = new HashSet(); + synchronized (this) { + Set rulesForResolving = mapTemplateToRules.get(templateUID); + if (rulesForResolving != null) { + rules.addAll(rulesForResolving); + } + } + for (String rUID : rules) { + try { + Rule unresolvedRule = get(rUID); + Rule resolvedRule = resolveRuleByTemplate(unresolvedRule); + Provider provider = getProvider(rUID); + if (provider instanceof ManagedRuleProvider) { + update(resolvedRule); + } else { + updated(provider, unresolvedRule, unresolvedRule); + } + } catch (IllegalArgumentException e) { + logger.error("Resolving the rule '{}' by template '{}' failed", rUID, templateUID, e); + } + } + } + + @Override + public void removed(RuleTemplate element) { + // Do nothing - resolved rules are independent from templates + } + + @Override + public void updated(RuleTemplate oldElement, RuleTemplate element) { + // Do nothing - resolved rules are independent from templates + } + + /** + * Getter for {@link #scheduleReinitializationDelay} used by {@link RuleEngineImpl} to schedule rule's + * re-initialization + * tries. + * + * @return the {@link #scheduleReinitializationDelay}. + */ + long getScheduleReinitializationDelay() { + return scheduleReinitializationDelay; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerHandlerCallbackImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerHandlerCallbackImpl.java new file mode 100644 index 00000000000..6a39a195dd3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerHandlerCallbackImpl.java @@ -0,0 +1,135 @@ +/** + * 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.internal; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +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; + +/** + * This class is implementation of {@link TriggerHandlerCallback} used by the {@link Trigger}s to notify rule engine + * about + * appearing of new triggered data. There is one and only one {@link TriggerHandlerCallback} per RuleImpl and it is used + * by + * all + * rule's {@link Trigger}s. + * + * @author Yordan Mihaylov - Initial Contribution + * @author Kai Kreuzer - improved stability + */ +public class TriggerHandlerCallbackImpl implements TriggerHandlerCallback { + + private final String ruleUID; + + private ExecutorService executor; + + private Future future; + + private final RuleEngineImpl re; + + protected TriggerHandlerCallbackImpl(RuleEngineImpl re, String ruleUID) { + this.re = re; + this.ruleUID = ruleUID; + executor = Executors.newSingleThreadExecutor(); + } + + @Override + public void triggered(Trigger trigger, Map outputs) { + synchronized (this) { + if (executor == null) { + return; + } + future = executor.submit(new TriggerData(trigger, outputs)); + } + re.logger.debug("The trigger '{}' of rule '{}' is triggered.", trigger.getId(), ruleUID); + } + + public boolean isRunning() { + Future future = this.future; + return future == null || !future.isDone(); + } + + class TriggerData implements Runnable { + + private final Trigger trigger; + + public Trigger getTrigger() { + return trigger; + } + + public Map getOutputs() { + return outputs; + } + + private final Map outputs; + + public TriggerData(Trigger t, Map outputs) { + this.trigger = t; + this.outputs = outputs; + } + + @Override + public void run() { + re.runRule(ruleUID, this); + } + } + + public void dispose() { + synchronized (this) { + AccessController.doPrivileged((PrivilegedAction) () -> { + executor.shutdownNow(); + return null; + }); + executor = null; + } + } + + @Override + public Boolean isEnabled(String ruleUID) { + return re.isEnabled(ruleUID); + } + + @Override + public void setEnabled(String uid, boolean isEnabled) { + re.setEnabled(uid, isEnabled); + } + + @Override + public RuleStatusInfo getStatusInfo(String ruleUID) { + return re.getStatusInfo(ruleUID); + } + + @Override + public RuleStatus getStatus(String ruleUID) { + return re.getStatus(ruleUID); + } + + @Override + public void runNow(String uid) { + re.runNow(uid); + } + + @Override + public void runNow(String uid, boolean considerConditions, Map context) { + re.runNow(uid, considerConditions, context); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerImpl.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerImpl.java new file mode 100644 index 00000000000..f2501682abe --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/TriggerImpl.java @@ -0,0 +1,33 @@ +/** + * 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.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Trigger; + +/** + * This class is implementation of {@link Trigger} modules used in the {@link RuleEngine}s. + * + * @author Yordan Mihaylov - Initial Contribution + */ +@NonNullByDefault +public class TriggerImpl extends ModuleImpl implements Trigger { + + public TriggerImpl(String id, String typeUID, @Nullable Configuration configuration, @Nullable String label, + @Nullable String description) { + super(id, typeUID, configuration, label, description); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AbstractCommandProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AbstractCommandProvider.java new file mode 100644 index 00000000000..2e863ec307a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AbstractCommandProvider.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.internal.commands; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.template.TemplateProvider; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is base for {@link ModuleTypeProvider}, {@link TemplateProvider} and RuleImporter which are responsible + * for execution of automation commands. + *

    + * It provides functionality for tracking {@link Parser} services by implementing {@link ServiceTrackerCustomizer} and + * provides common functionality for exporting automation objects. + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractCommandProvider implements ServiceTrackerCustomizer { + + protected Logger logger; + + /** + * A bundle's execution context within the Framework. + */ + protected BundleContext bc; + + /** + * This Map provides reference between provider of resources and the loaded objects from these resources. + *

    + * The Map has for keys - {@link URL} resource provider and for values - Lists with UIDs of the objects. + */ + Map> providerPortfolio = new HashMap>(); + + /** + * This field is a {@link ServiceTracker} for {@link Parser} services. + */ + protected ServiceTracker parserTracker; + + /** + * This Map provides structure for fast access to the {@link Parser}s. This provides opportunity for high + * performance at runtime of the system. + */ + protected Map> parsers = new HashMap>(); + + /** + * This Map provides structure for fast access to the provided automation objects. This provides opportunity for + * high performance at runtime of the system, when the Rule Engine asks for any particular object, instead of + * waiting it for parsing every time. + *

    + * The Map has for keys UIDs of the objects and for values {@link Localizer}s of the objects. + */ + protected Map providedObjectsHolder = new HashMap(); + + protected List> listeners; + + /** + * This constructor is responsible for creation and opening a tracker for {@link Parser} services. + * + * @param context is the {@link BundleContext}, used for creating a tracker for {@link Parser} services. + */ + @SuppressWarnings("unchecked") + public AbstractCommandProvider(BundleContext context) { + this.bc = context; + logger = LoggerFactory.getLogger(AbstractCommandProvider.this.getClass()); + parserTracker = new ServiceTracker(context, Parser.class.getName(), this); + parserTracker.open(); + } + + /** + * This method is inherited from {@link AbstractPersistentProvider}. + * Extends parent's functionality with closing the {@link Parser} service tracker. + * Sets null to {@link #parsers}, {@link #providedObjectsHolder}, {@link #providerPortfolio} + */ + public void close() { + if (parserTracker != null) { + parserTracker.close(); + parserTracker = null; + parsers = null; + synchronized (providedObjectsHolder) { + providedObjectsHolder = null; + } + synchronized (providerPortfolio) { + providerPortfolio = null; + } + } + } + + /** + * This method tracks the {@link Parser} services and stores them into the Map "{@link #parsers}" in the + * memory, for fast access on demand. + * + * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference) + */ + @Override + public Object addingService(ServiceReference reference) { + @SuppressWarnings("unchecked") + Parser service = (Parser) bc.getService(reference); + String key = (String) reference.getProperty(Parser.FORMAT); + key = key == null ? Parser.FORMAT_JSON : key; + parsers.put(key, service); + return service; + } + + /** + * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference, + * java.lang.Object) + */ + @Override + public void modifiedService(ServiceReference reference, Object service) { + // do nothing + } + + /** + * This method removes the {@link Parser} service objects from the Map "{@link #parsers}". + * + * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference, + * java.lang.Object) + */ + @Override + public void removedService(ServiceReference reference, Object service) { + String key = (String) reference.getProperty(Parser.FORMAT); + key = key == null ? Parser.FORMAT_JSON : key; + parsers.remove(key); + } + + /** + * This method is responsible for execution of the {@link AutomationCommandExport} operation by choosing the + * {@link Parser} which to be used for exporting a set of automation objects, in a file. When the choice is made, + * the chosen {@link Parser} is used to do the export. + * + * @param parserType is a criteria for choosing the {@link Parser} which to be used. + * @param set a Set of automation objects for export. + * @param file is the file in which to export the automation objects. + * @throws Exception is thrown when I/O operation has failed or has been interrupted or generating of the text fails + * for some reasons. + */ + public String exportData(String parserType, Set set, File file) throws Exception { + Parser parser = parsers.get(parserType); + if (parser != null) { + try (OutputStreamWriter oWriter = new OutputStreamWriter(new FileOutputStream(file))) { + parser.serialize(set, oWriter); + return AutomationCommand.SUCCESS; + } + } else { + return String.format("%s! Parser \"%s\" not found!", AutomationCommand.FAIL, parserType); + } + } + + /** + * This method is responsible for execution of the {@link AutomationCommandImport} operation. + * + * @param parser the {@link Parser} which to be used for operation. + * @param inputStreamReader + * @return the set of automation objects created as result of the {@link AutomationCommandImport} operation. + * Operation can be successful or can fail because of {@link ParsingException}. + * @throws ParsingException is thrown when there are exceptions during the parsing process. It accumulates all of + * them. + */ + protected abstract Set importData(URL url, Parser parser, InputStreamReader inputStreamReader) + throws ParsingException; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommand.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommand.java new file mode 100644 index 00000000000..629d8963388 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommand.java @@ -0,0 +1,131 @@ +/** + * 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.internal.commands; + +/** + * This class is base for all automation commands. It defines common functionality for an automation command. Each class + * of commands is responsible for a group of commands, that are equivalent but each of them is related to a different + * provider. + * + * @author Ana Dimova - Initial Contribution + * + */ +public abstract class AutomationCommand { + + /** + * This constant is used as a part of the string representing understandable for the user message containing + * information for the success of the command. + */ + protected static final String SUCCESS = "SUCCESS"; + + /** + * This constant is used as a part of the string representing understandable for the user message containing + * information for the failure of the command. + */ + protected static final String FAIL = "FAIL"; + + /** + * This constant is used for detection of PrintStackTrace option. If some of the parameters of the command + * is equal to this constant, then the option is present. + */ + protected static final String OPTION_ST = "-st"; + + /** + * This field is an indicator of presence of PrintStackTrace option. Its value is true if the + * option is present and false in the opposite case. + */ + protected boolean st = false; + + /** + * This field keeps the result of parsing the parameters and options of the command. + */ + protected String parsingResult; + + /** + * This field keeps the identifier of the command because each class of commands is responsible for a group + * of commands. + */ + protected String command; + + /** + * This field keeps information about which provider is responsible for execution of the command. + */ + protected int providerType; + + /** + * This field keeps a reference to the particular implementation of the AutomationCommandsPluggable. + */ + protected AutomationCommandsPluggable autoCommands; + + /** + * This constructor is responsible for initializing the common properties for each automation command. + * + * @param command is the identifier of the command. + * @param parameterValues is an array of strings which are basis for initializing the options and parameters of the + * command. The order for their description is a random. + * @param providerType is which provider is responsible for execution of the command. + * @param autoCommands a reference to the particular implementation of the AutomationCommandsPluggable. + */ + public AutomationCommand(String command, String[] parameterValues, int providerType, + AutomationCommandsPluggable autoCommands) { + this.command = command; + this.providerType = providerType; + this.autoCommands = autoCommands; + parsingResult = parseOptionsAndParameters(parameterValues); + } + + /** + * This method is common for all automation commands and it is responsible for execution of every particular + * command. + * + * @return a string representing understandable for the user message containing information on the outcome of the + * command. + */ + public abstract String execute(); + + /** + * This method is responsible for processing the st option of the command. + * + * @param e is the exception appeared during the command's execution. + * @return a string representing the exception, corresponding to availability of the st option. + */ + protected String getStackTrace(Exception e) { + StringBuilder writer = new StringBuilder(); + if (st) { + StackTraceElement[] ste = e.getStackTrace(); + for (int i = 0; i < ste.length; i++) { + if (i == 0) { + writer.append(String.format("FAIL : %s", ste[i].toString() + "\n")); + } else { + writer.append(String.format("%s", ste[i].toString() + "\n")); + } + } + } else { + writer.append(String.format("FAIL : %s", e.getMessage() + "\n")); + } + return writer.toString(); + } + + /** + * This method is used to determine the options and parameters for every particular command. If there are redundant + * options and parameters or the required are missing the execution of the command will be ended and the parsing + * result will be returned as a result of the command. + * + * @param parameterValues is an array of strings which are basis for initializing the options and parameters of the + * command. The order for their description is a random. + * @return a string representing understandable for the user message containing information on the outcome of the + * parsing the parameters and options. + */ + protected abstract String parseOptionsAndParameters(String[] parameterValues); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandEnableRule.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandEnableRule.java new file mode 100644 index 00000000000..8c5bd53aeba --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandEnableRule.java @@ -0,0 +1,108 @@ +/** + * 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.internal.commands; + +import org.openhab.core.automation.RuleStatus; + +/** + * This class provides functionality of command {@link AutomationCommands#ENABLE_RULE}. + * + * @author Ana Dimova - Initial Contribution + * + */ +public class AutomationCommandEnableRule extends AutomationCommand { + + /** + * This field keeps the value of "enable" parameter of the command. + */ + private boolean enable; + + /** + * This field indicates the presence of the "enable" parameter of the command. + */ + private boolean hasEnable; + + /** + * This field keeps the specified rule UID. + */ + private String uid; + + public AutomationCommandEnableRule(String command, String[] parameterValues, int providerType, + AutomationCommandsPluggable autoCommands) { + super(command, parameterValues, providerType, autoCommands); + } + + @Override + public String execute() { + if (parsingResult != SUCCESS) { + return parsingResult; + } + if (hasEnable) { + autoCommands.setEnabled(uid, enable); + return SUCCESS; + } else { + RuleStatus status = autoCommands.getRuleStatus(uid); + if (status != null) { + return Printer.printRuleStatus(uid, status); + } + } + return FAIL; + } + + @Override + protected String parseOptionsAndParameters(String[] parameterValues) { + for (int i = 0; i < parameterValues.length; i++) { + if (null == parameterValues[i]) { + continue; + } + if (parameterValues[i].charAt(0) == '-') { + if (parameterValues[i].equals(OPTION_ST)) { + st = true; + continue; + } + return String.format("Unsupported option: %s", parameterValues[i]); + } + if (uid == null) { + uid = parameterValues[i]; + continue; + } + getEnable(parameterValues[i]); + if (hasEnable) { + continue; + } + if (uid == null) { + return "Missing required parameter: Rule UID"; + } + return String.format("Unsupported parameter: %s", parameterValues[i]); + } + return SUCCESS; + } + + /** + * Utility method for parsing the command parameter - "enable". + * + * @param parameterValue is the value entered from command line. + */ + private void getEnable(String parameterValue) { + if (parameterValue.equals("true")) { + enable = true; + hasEnable = true; + } else if (parameterValue.equals("false")) { + enable = false; + hasEnable = true; + } else { + hasEnable = false; + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandExport.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandExport.java new file mode 100644 index 00000000000..5a62c8f0b8a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandExport.java @@ -0,0 +1,192 @@ +/** + * 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.internal.commands; + +import java.io.File; +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.openhab.core.automation.parser.Parser; + +/** + * This class provides common functionality of commands: + *

      + *
    • {@link AutomationCommands#EXPORT_MODULE_TYPES} + *
    • {@link AutomationCommands#EXPORT_TEMPLATES} + *
    • {@link AutomationCommands#EXPORT_RULES} + *
    + * + * @author Ana Dimova - Initial Contribution + * + */ +public class AutomationCommandExport extends AutomationCommand { + + /** + * This constant is used for detection of ParserType parameter. If some of the parameters of the command + * is equal to this constant, then the ParserType parameter is present and its value is the next one. + */ + private static final String OPTION_P = "-p"; + + /** + * This field keeps the value of the ParserType parameter and it is initialized as + * {@link Parser#FORMAT_JSON} by default. + */ + private String parserType = Parser.FORMAT_JSON; + + /** + * This field keeps the path of the output file where the automation objects to be exported. + */ + private File file; + + /** + * This field stores the value of locale parameter of the command. + */ + private Locale locale = Locale.getDefault(); // For now is initialized with the default locale, but when the + // localization is implemented, it will be initialized with a parameter + // of the command. + + /** + * @see AutomationCommand#AutomationCommand(String, String[], int, AutomationCommandsPluggable) + */ + public AutomationCommandExport(String command, String[] params, int providerType, + AutomationCommandsPluggable autoCommands) { + super(command, params, providerType, autoCommands); + } + + /** + * This method is responsible for execution of commands: + *
      + *
    • {@link AutomationCommands#EXPORT_MODULE_TYPES} + *
    • {@link AutomationCommands#EXPORT_TEMPLATES} + *
    • {@link AutomationCommands#EXPORT_RULES} + *
    + */ + @SuppressWarnings("unchecked") + @Override + public String execute() { + if (parsingResult != SUCCESS) { + return parsingResult; + } + @SuppressWarnings("rawtypes") + Set set = new HashSet(); + switch (providerType) { + case AutomationCommands.MODULE_TYPE_PROVIDER: + @SuppressWarnings("rawtypes") + Collection collection = autoCommands.getTriggers(locale); + if (collection != null) { + set.addAll(collection); + } + collection = autoCommands.getConditions(locale); + if (collection != null) { + set.addAll(collection); + } + collection = autoCommands.getActions(locale); + if (collection != null) { + set.addAll(collection); + } + try { + return autoCommands.exportModuleTypes(parserType, set, file); + } catch (Exception e) { + return getStackTrace(e); + } + case AutomationCommands.TEMPLATE_PROVIDER: + collection = autoCommands.getTemplates(locale); + if (collection != null) { + set.addAll(collection); + } + try { + return autoCommands.exportTemplates(parserType, set, file); + } catch (Exception e) { + return getStackTrace(e); + } + case AutomationCommands.RULE_PROVIDER: + collection = autoCommands.getRules(); + if (collection != null) { + set.addAll(collection); + } + try { + return autoCommands.exportRules(parserType, set, file); + } catch (Exception e) { + return getStackTrace(e); + } + } + return String.format("%s : Unsupported provider type!", FAIL); + } + + /** + * This method serves to create a {@link File} object from a string that is passed as a parameter of the command. + * + * @param parameterValue is a string that is passed as parameter of the command and it supposed to be a file + * representation. + * @return a {@link File} object created from the string that is passed as a parameter of the command or null + * if the parent directory could not be found or created or the string could not be parsed. + */ + private File initFile(String parameterValue) { + File f = new File(parameterValue); + File parent = f.getParentFile(); + return (parent == null || (!parent.isDirectory() && !parent.mkdirs())) ? null : f; + } + + /** + * This method is invoked from the constructor to parse all parameters and options of the command EXPORT. + * If there are redundant parameters or options, or the required parameter is missing the result will be the failure + * of the command. This command has: + *
      + * Options: + *
        + *
      • PrintStackTrace which is common for all commands + *
      + *
    + *
      + * Parameters: + *
        + *
      • parserType is optional and by default its value is {@link Parser#FORMAT_JSON}. + *
      • file is required and specifies the path to the file for export. + *
      + *
    + */ + @Override + protected String parseOptionsAndParameters(String[] parameterValues) { + boolean getFile = true; + for (int i = 0; i < parameterValues.length; i++) { + if (null == parameterValues[i]) { + continue; + } + if (parameterValues[i].equals(OPTION_ST)) { + st = true; + } else if (parameterValues[i].equalsIgnoreCase(OPTION_P)) { + i++; + if (i >= parameterValues.length) { + return String.format("The option [%s] should be followed by value for the parser type.", OPTION_P); + } + parserType = parameterValues[i]; + } else if (parameterValues[i].charAt(0) == '-') { + return String.format("Unsupported option: %s", parameterValues[i]); + } else if (getFile) { + file = initFile(parameterValues[i]); + if (file != null) { + getFile = false; + } + } else { + return String.format("Unsupported parameter: %s", parameterValues[i]); + } + } + if (getFile) { + return "Missing destination file parameter!"; + } + return SUCCESS; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandImport.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandImport.java new file mode 100644 index 00000000000..b68ae049121 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandImport.java @@ -0,0 +1,165 @@ +/** + * 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.internal.commands; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +import org.openhab.core.automation.parser.Parser; + +/** + * This class provides common functionality of commands: + *
      + *
    • {@link AutomationCommands#IMPORT_MODULE_TYPES} + *
    • {@link AutomationCommands#IMPORT_TEMPLATES} + *
    • {@link AutomationCommands#IMPORT_RULES} + *
    + * + * @author Ana Dimova - Initial Contribution + * + */ +public class AutomationCommandImport extends AutomationCommand { + + /** + * This constant is used for detection of ParserType parameter. If some of the parameters of the command + * is equal to this constant, then the ParserType parameter is present and its value is the next one. + */ + private static final String OPTION_P = "-p"; + + /** + * This field keeps the value of the ParserType parameter and it is initialized as + * {@link Parser#FORMAT_JSON} by default. + */ + private String parserType = Parser.FORMAT_JSON; + + /** + * This field keeps URL of the source of automation objects that has to be imported. + */ + private URL url; + + /** + * @see AutomationCommand#AutomationCommand(String, String[], int, AutomationCommandsPluggable) + */ + public AutomationCommandImport(String command, String[] params, int adminType, + AutomationCommandsPluggable autoCommands) { + super(command, params, adminType, autoCommands); + } + + /** + * This method is responsible for execution of commands: + *
      + *
    • {@link AutomationCommands#IMPORT_MODULE_TYPES} + *
    • {@link AutomationCommands#IMPORT_TEMPLATES} + *
    • {@link AutomationCommands#IMPORT_RULES} + *
    + */ + @Override + public String execute() { + if (parsingResult != SUCCESS) { + return parsingResult; + } + try { + switch (providerType) { + case AutomationCommands.MODULE_TYPE_PROVIDER: + autoCommands.importModuleTypes(parserType, url); + break; + case AutomationCommands.TEMPLATE_PROVIDER: + autoCommands.importTemplates(parserType, url); + break; + case AutomationCommands.RULE_PROVIDER: + autoCommands.importRules(parserType, url); + break; + } + } catch (Exception e) { + return getStackTrace(e); + } + return SUCCESS + "\n"; + } + + /** + * This method serves to create an {@link URL} object or {@link File} object from a string that is passed as + * a parameter of the command. From the {@link File} object the URL is constructed. + * + * @param parameterValue is a string that is passed as parameter of the command and it supposed to be an URL + * representation. + * @return an {@link URL} object created from the string that is passed as parameter of the command or null + * if either no legal protocol could be found in the specified string or the string could not be parsed. + */ + private URL initURL(String parameterValue) { + try { + return new URL(parameterValue); + } catch (MalformedURLException mue) { + File f = new File(parameterValue); + if (f.isFile()) { + try { + return f.toURI().toURL(); + } catch (MalformedURLException e) { + } + } + } + return null; + } + + /** + * This method is invoked from the constructor to parse all parameters and options of the command EXPORT. + * If there are redundant parameters or options or the required is missing the result will be the failure of the + * command. This command has: + *
      + * Options: + *
        + *
      • PrintStackTrace is common for all commands and its presence triggers printing of stack trace in case + * of exception. + *
      + *
    + *
      + * Parameters: + *
        + *
      • parserType is optional and by default its value is {@link Parser#FORMAT_JSON}. + *
      • url is required and it points the resource of automation objects that has to be imported. + *
      + *
    + */ + @Override + protected String parseOptionsAndParameters(String[] parameterValues) { + boolean getUrl = true; + for (int i = 0; i < parameterValues.length; i++) { + if (null == parameterValues[i]) { + continue; + } + if (parameterValues[i].equals(OPTION_ST)) { + st = true; + } else if (parameterValues[i].equalsIgnoreCase(OPTION_P)) { + i++; + if (i >= parameterValues.length) { + return String.format("The option [%s] should be followed by value for the parser type.", OPTION_P); + } + parserType = parameterValues[i]; + } else if (parameterValues[i].charAt(0) == '-') { + return String.format("Unsupported option: %s", parameterValues[i]); + } else if (getUrl) { + url = initURL(parameterValues[i]); + if (url != null) { + getUrl = false; + } + } else { + return String.format("Unsupported parameter: %s", parameterValues[i]); + } + } + if (getUrl) { + return "Missing source URL parameter or its value is incorrect!"; + } + return SUCCESS; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java new file mode 100644 index 00000000000..5401fd9dabf --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandList.java @@ -0,0 +1,405 @@ +/** + * 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.internal.commands; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleStatus; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.type.ModuleType; + +/** + * This class provides common functionality of commands: + *
      + *
    • {@link AutomationCommands#LIST_MODULE_TYPES} + *
    • {@link AutomationCommands#LIST_TEMPLATES} + *
    • {@link AutomationCommands#LIST_RULES} + *
    + * + * @author Ana Dimova - Initial Contribution + * + */ +public class AutomationCommandList extends AutomationCommand { + + /** + * This field serves to keep the UID of a {@link Rule}, {@link Template} or {@link ModuleType}, or part of it, or + * sequence number of a {@link Rule}, {@link Template}, or {@link ModuleType} in the list. + */ + private String id; + + /** + * This field is used to search for templates or types of modules, which have been translated to the language from + * the locale. If the parameter locale is not passed to the command line then the default locale will be + * used. + */ + private Locale locale; + + /** + * @see AutomationCommand#AutomationCommand(String, String[], int, AutomationCommandsPluggable) + */ + public AutomationCommandList(String command, String[] params, int adminType, + AutomationCommandsPluggable autoCommands) { + super(command, params, adminType, autoCommands); + if (locale == null) { + locale = Locale.getDefault(); + } + } + + /** + * This method is responsible for execution of commands: + *
      + *
    • {@link AutomationCommands#LIST_MODULE_TYPES} + *
    • {@link AutomationCommands#LIST_TEMPLATES} + *
    • {@link AutomationCommands#LIST_RULES} + *
    + */ + @Override + public String execute() { + if (parsingResult != SUCCESS) { + return parsingResult; + } + if (providerType == AutomationCommands.MODULE_TYPE_PROVIDER) { + return listModuleTypes(); + } + if (providerType == AutomationCommands.TEMPLATE_PROVIDER) { + return listTemplates(); + } + if (providerType == AutomationCommands.RULE_PROVIDER) { + return listRules(); + } + return FAIL; + } + + /** + * This method is invoked from the constructor to parse all parameters and options of the command LIST. + * If there are redundant parameters or options the result will be the failure of the command. This command has: + *
      + * Options: + *
        + *
      • PrintStackTrace is common for all commands and its presence triggers printing of stack trace in case + * of exception. + *
      + *
    + *
      + * Parameters: + *
        + *
      • id is optional and its presence triggers printing of details on specified automation object. + *
      • locale is optional and it triggers printing of localized details on specified automation object. Its + * value is interpreted as language or language tag. If missing - the default locale will be used. + *
      + *
    + */ + @Override + protected String parseOptionsAndParameters(String[] parameterValues) { + boolean getId = true; + boolean getLocale = true; + for (int i = 0; i < parameterValues.length; i++) { + if (null == parameterValues[i]) { + continue; + } + if (parameterValues[i].charAt(0) == '-') { + if (parameterValues[i].equals(OPTION_ST)) { + st = true; + continue; + } + return String.format("Unsupported option: %s", parameterValues[i]); + } + if (getId) { + id = parameterValues[i]; + getId = false; + continue; + } + if (getLocale) { + String l = parameterValues[i]; + locale = new Locale(l); + getLocale = false; + } + if (getId && getLocale) { + return String.format("Unsupported parameter: %s", parameterValues[i]); + } + } + return SUCCESS; + } + + /** + * This method is responsible for execution of command {@link AutomationCommands#LIST_RULES}. + * + * @return a string representing understandable for the user message containing information on the outcome of the + * command {@link AutomationCommands#LIST_RULES}. + */ + private String listRules() { + Collection collection = autoCommands.getRules(); + Map rules = new Hashtable(); + Map listRules = null; + if (collection != null && !collection.isEmpty()) { + addCollection(collection, rules); + String[] uids = new String[rules.size()]; + Utils.quickSort(rules.keySet().toArray(uids), 0, rules.size()); + listRules = Utils.putInHastable(uids); + } + if (listRules != null && !listRules.isEmpty()) { + if (id != null) { + collection = getRuleByFilter(listRules); + if (collection.size() == 1) { + Rule r = (Rule) collection.toArray()[0]; + if (r != null) { + RuleStatus status = autoCommands.getRuleStatus(r.getUID()); + return Printer.printRule(r, status); + } else { + return String.format("Nonexistent ID: %s", id); + } + } else if (collection.isEmpty()) { + return String.format("Nonexistent ID: %s", id); + } else { + if (!rules.isEmpty()) { + rules.clear(); + } + addCollection(collection, rules); + listRules = Utils.filterList(rules, listRules); + } + } + return Printer.printRules(autoCommands, listRules); + } + return "There are no Rules available!"; + } + + /** + * This method is responsible for execution of command {@link AutomationCommands#LIST_TEMPLATES}. + * + * @return a string representing understandable for the user message containing information on the outcome of the + * command {@link AutomationCommands#LIST_TEMPLATES}. + */ + private String listTemplates() { + Collection collection = autoCommands.getTemplates(locale); + Map templates = new Hashtable(); + Map listTemplates = null; + if (collection != null && !collection.isEmpty()) { + addCollection(collection, templates); + String[] uids = new String[templates.size()]; + Utils.quickSort(templates.keySet().toArray(uids), 0, templates.size()); + listTemplates = Utils.putInHastable(uids); + } + if (listTemplates != null && !listTemplates.isEmpty()) { + if (id != null) { + collection = getTemplateByFilter(listTemplates); + if (collection.size() == 1) { + Template t = (Template) collection.toArray()[0]; + if (t != null) { + return Printer.printTemplate(t); + } else { + return String.format("Nonexistent ID: %s", id); + } + } else if (collection.isEmpty()) { + return String.format("Nonexistent ID: %s", id); + } else { + if (!templates.isEmpty()) { + templates.clear(); + } + addCollection(collection, templates); + listTemplates = Utils.filterList(templates, listTemplates); + } + } + if (listTemplates != null && !listTemplates.isEmpty()) { + return Printer.printTemplates(listTemplates); + } + } + return "There are no Templates available!"; + } + + /** + * This method is responsible for execution of command {@link AutomationCommands#LIST_MODULE_TYPES}. + * + * @return a string representing understandable for the user message containing information on the outcome of the + * command {@link AutomationCommands#LIST_MODULE_TYPES}. + */ + private String listModuleTypes() { + Map moduleTypes = new Hashtable(); + Collection collection = autoCommands.getTriggers(locale); + addCollection(collection, moduleTypes); + collection = autoCommands.getConditions(locale); + addCollection(collection, moduleTypes); + collection = autoCommands.getActions(locale); + addCollection(collection, moduleTypes); + Map listModuleTypes = null; + if (!moduleTypes.isEmpty()) { + String[] uids = new String[moduleTypes.size()]; + Utils.quickSort(moduleTypes.keySet().toArray(uids), 0, moduleTypes.size()); + listModuleTypes = Utils.putInHastable(uids); + } + if (listModuleTypes != null && !listModuleTypes.isEmpty()) { + if (id != null) { + collection = getModuleTypeByFilter(listModuleTypes); + if (collection.size() == 1) { + ModuleType mt = (ModuleType) collection.toArray()[0]; + if (mt != null) { + return Printer.printModuleType(mt); + } else { + return String.format("Nonexistent ID: %s", id); + } + } else if (collection.isEmpty()) { + return String.format("Nonexistent ID: %s", id); + } else { + if (!moduleTypes.isEmpty()) { + moduleTypes.clear(); + } + addCollection(collection, moduleTypes); + listModuleTypes = Utils.filterList(moduleTypes, listModuleTypes); + } + } + return Printer.printModuleTypes(listModuleTypes); + } + return "There are no Module Types available!"; + } + + /** + * This method reduces the list of {@link Rule}s so that their unique identifier or part of it to match the + * {@link #id} or + * the index in the list to match the {@link #id}. + * + * @param list is the list of {@link Rule}s for reducing. + * @return a collection of {@link Rule}s that match the filter. + */ + private Collection getRuleByFilter(Map list) { + Collection rules = new ArrayList(); + if (!list.isEmpty()) { + Rule r = null; + String uid = list.get(id); + if (uid != null) { + r = autoCommands.getRule(uid); + if (r != null) { + rules.add(r); + return rules; + } + } else { + r = autoCommands.getRule(id); + if (r != null) { + rules.add(r); + return rules; + } else { + for (String ruleUID : list.values()) { + if (ruleUID.indexOf(id) > -1) { + rules.add(autoCommands.getRule(ruleUID)); + } + } + } + } + } + return rules; + } + + /** + * This method reduces the list of {@link Template}s so that their unique identifier or part of it to match the + * {@link #id} or + * the index in the list to match the {@link #id}. + * + * @param list is the list of {@link Template}s for reducing. + * @return a collection of {@link Template}s that match the filter. + */ + private Collection getTemplateByFilter(Map list) { + Collection templates = new ArrayList(); + RuleTemplate t = null; + String uid = list.get(id); + if (uid != null) { + t = autoCommands.getTemplate(uid, locale); + if (t != null) { + templates.add(t); + return templates; + } + } else { + t = autoCommands.getTemplate(id, locale); + if (t != null) { + templates.add(t); + return templates; + } else { + for (String templateUID : list.keySet()) { + if (templateUID.indexOf(id) != -1) { + templates.add(autoCommands.getTemplate(templateUID, locale)); + } + } + } + } + return templates; + } + + /** + * This method reduces the list of {@link ModuleType}s so that their unique identifier or part of it to match the + * {@link #id} or + * the index in the list to match the {@link #id}. + * + * @param list is the list of {@link ModuleType}s for reducing. + * @return a collection of {@link ModuleType}s that match the filter. + */ + private Collection getModuleTypeByFilter(Map list) { + Collection moduleTypes = new ArrayList(); + if (!list.isEmpty()) { + ModuleType mt = null; + String uid = list.get(id); + if (uid != null) { + mt = autoCommands.getModuleType(uid, locale); + if (mt != null) { + moduleTypes.add(mt); + return moduleTypes; + } + } else { + mt = autoCommands.getModuleType(id, locale); + if (mt != null) { + moduleTypes.add(mt); + return moduleTypes; + } else { + for (String typeUID : list.values()) { + if (typeUID.indexOf(id) != -1) { + moduleTypes.add(autoCommands.getModuleType(typeUID, locale)); + } + } + } + } + } + return moduleTypes; + } + + /** + * This method converts a {@link Collection} of {@link Rule}s, {@link Template}s or {@link ModuleType}s to a + * {@link Hashtable} with keys - the UID of the object and values - the object. + * + * @param collection is the {@link Collection} of {@link Rule}s, {@link Template}s or {@link ModuleType}s which + * must be converted. + * @param list is the map with keys - the UID of the object and values - the object, which must be + * filled with the objects from collection. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void addCollection(Collection collection, Map list) { + if (collection != null && !collection.isEmpty()) { + Iterator i = collection.iterator(); + while (i.hasNext()) { + Object element = i.next(); + if (element instanceof ModuleType) { + list.put(((ModuleType) element).getUID(), element); + } + if (element instanceof RuleTemplate) { + list.put(((RuleTemplate) element).getUID(), element); + } + if (element instanceof Rule) { + list.put(((Rule) element).getUID(), element); + } + } + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandRemove.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandRemove.java new file mode 100644 index 00000000000..c7fa8b26a27 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandRemove.java @@ -0,0 +1,169 @@ +/** + * 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.internal.commands; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +import org.openhab.core.automation.Rule; + +/** + * This class provides common functionality of commands: + *
      + *
    • {@link AutomationCommands#REMOVE_MODULE_TYPES} + *
    • {@link AutomationCommands#REMOVE_TEMPLATES} + *
    • {@link AutomationCommands#REMOVE_RULES} + *
    • {@link AutomationCommands#REMOVE_RULE} + *
    + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - fixed feedback when deleting non-existent rule + * @author Marin Mitev - removed prefixes in the output + * + */ +public class AutomationCommandRemove extends AutomationCommand { + + /** + * This field keeps the UID of the {@link Rule} if command is {@link AutomationCommands#REMOVE_RULE} + */ + private String id; + + /** + * This field keeps URL of the source of automation objects that has to be removed. + */ + private URL url; + + /** + * @see AutomationCommand#AutomationCommand(String, String[], int, AutomationCommandsPluggable) + */ + public AutomationCommandRemove(String command, String[] params, int providerType, + AutomationCommandsPluggable autoCommands) { + super(command, params, providerType, autoCommands); + } + + /** + * This method is responsible for execution of commands: + *
      + *
    • {@link AutomationCommands#REMOVE_MODULE_TYPES} + *
    • {@link AutomationCommands#REMOVE_TEMPLATES} + *
    • {@link AutomationCommands#REMOVE_RULES} + *
    • {@link AutomationCommands#REMOVE_RULE} + *
    + */ + @Override + public String execute() { + if (parsingResult != SUCCESS) { + return parsingResult; + } + switch (providerType) { + case AutomationCommands.MODULE_TYPE_PROVIDER: + return autoCommands.remove(AutomationCommands.MODULE_TYPE_PROVIDER, url); + case AutomationCommands.TEMPLATE_PROVIDER: + return autoCommands.remove(AutomationCommands.TEMPLATE_PROVIDER, url); + case AutomationCommands.RULE_PROVIDER: + if (command == AutomationCommands.REMOVE_RULE) { + return autoCommands.removeRule(id); + } else if (command == AutomationCommands.REMOVE_RULES) { + return autoCommands.removeRules(id); + } + } + return FAIL; + } + + /** + * This method serves to create an {@link URL} object or {@link File} object from a string that is passed as + * a parameter of the command. From the {@link File} object the URL is constructed. + * + * @param parameterValue is a string that is passed as parameter of the command and it supposed to be an URL + * representation. + * @return an {@link URL} object created from the string that is passed as parameter of the command or null + * if either no legal protocol could be found in the specified string or the string could not be parsed. + */ + private URL initURL(String parameterValue) { + try { + return new URL(parameterValue); + } catch (MalformedURLException mue) { + File f = new File(parameterValue); + if (f.isFile()) { + try { + return f.toURI().toURL(); + } catch (MalformedURLException e) { + } + } + } + return null; + } + + /** + * This method is invoked from the constructor to parse all parameters and options of the command REMOVE. + * If there are redundant parameters or options or the required are missing the result will be the failure of the + * command. This command has: + *
      + * Options: + *
        + *
      • PrintStackTrace is common for all commands and its presence triggers printing of stack trace in case + * of exception. + *
      + *
    + *
      + * Parameters: + *
        + *
      • id is required for {@link AutomationCommands#REMOVE_RULE} command. If it is present for all + * REMOVE commands, except {@link AutomationCommands#REMOVE_RULE}, it will be treated as redundant. + *
      • url is required for all REMOVE commands, except {@link AutomationCommands#REMOVE_RULE}. + * If it is present for {@link AutomationCommands#REMOVE_RULE}, it will be treated as redundant. + *
      + *
    + */ + @Override + protected String parseOptionsAndParameters(String[] parameterValues) { + boolean getUrl = true; + boolean getId = true; + if (providerType == AutomationCommands.RULE_PROVIDER) { + getUrl = false; + } else { + getId = false; + } + for (int i = 0; i < parameterValues.length; i++) { + if (null == parameterValues[i]) { + continue; + } + if (parameterValues[i].equals(OPTION_ST)) { + st = true; + } else if (parameterValues[i].charAt(0) == '-') { + return String.format("Unsupported option: %s", parameterValues[i]); + } else if (getUrl) { + url = initURL(parameterValues[i]); + if (url != null) { + getUrl = false; + } + } else if (getId) { + id = parameterValues[i]; + if (id != null) { + getId = false; + } + } else { + return String.format("Unsupported parameter: %s", parameterValues[i]); + } + } + if (getUrl) { + return "Missing source URL parameter!"; + } + if (getId) { + return "Missing UID parameter!"; + } + return SUCCESS; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommands.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommands.java new file mode 100644 index 00000000000..2cc5757c8ee --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommands.java @@ -0,0 +1,522 @@ +/** + * 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.internal.commands; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.Locale; +import java.util.Set; + +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.RuleStatus; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.template.TemplateRegistry; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.type.TriggerType; +import org.osgi.framework.BundleContext; + +/** + * This class provides mechanism to separate the Automation Commands implementation from the Automation Core + * implementation. + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * + */ +public abstract class AutomationCommands { + + /** + * This static field is used to switch between providers in different commands. + */ + protected static final int RULE_PROVIDER = 1; + + /** + * This static field is used to switch between providers in different commands. + */ + protected static final int TEMPLATE_PROVIDER = 2; + + /** + * This static field is used to switch between providers in different commands. + */ + protected static final int MODULE_TYPE_PROVIDER = 3; + + /** + * This static field is an identifier of the command {@link AutomationCommandImport} for {@link ModuleType}s. + */ + protected static final String IMPORT_MODULE_TYPES = "importModuleTypes"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandImport} for {@link ModuleType}s. + */ + protected static final String IMPORT_MODULE_TYPES_SHORT = "imt"; + + /** + * This static field is an identifier of the command {@link AutomationCommandImport} for {@link RuleTemplate}s. + */ + protected static final String IMPORT_TEMPLATES = "importTemplates"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandImport} for {@link RuleTemplate}s. + */ + protected static final String IMPORT_TEMPLATES_SHORT = "it"; + + /** + * This static field is an identifier of the command {@link AutomationCommandImport} for {@link Rule}s. + */ + protected static final String IMPORT_RULES = "importRules"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandImport} for {@link Rule}s. + */ + protected static final String IMPORT_RULES_SHORT = "ir"; + + /** + * This static field is an identifier of the command {@link AutomationCommandExport} for {@link ModuleType}s. + */ + protected static final String EXPORT_MODULE_TYPES = "exportModuleTypes"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandExport} for {@link ModuleType}s. + */ + protected static final String EXPORT_MODULE_TYPES_SHORT = "emt"; + + /** + * This static field is an identifier of the command {@link AutomationCommandExport} for {@link RuleTemplate}s. + */ + protected static final String EXPORT_TEMPLATES = "exportTemplates"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandExport} for {@link RuleTemplate}s. + */ + protected static final String EXPORT_TEMPLATES_SHORT = "et"; + + /** + * This static field is an identifier of the command {@link AutomationCommandExport} for {@link Rule}s. + */ + protected static final String EXPORT_RULES = "exportRules"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandExport} for {@link Rule}s. + */ + protected static final String EXPORT_RULES_SHORT = "er"; + + /** + * This static field is an identifier of the command {@link AutomationCommandRemove} for {@link Rule}. + */ + protected static final String REMOVE_RULE = "removeRule"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandRemove} for {@link Rule}. + */ + protected static final String REMOVE_RULE_SHORT = "rmr"; + + /** + * This static field is an identifier of the command {@link AutomationCommandRemove} for {@link Rule}s. + */ + protected static final String REMOVE_RULES = "removeRules"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandRemove} for {@link Rule}s. + */ + protected static final String REMOVE_RULES_SHORT = "rmrs"; + + /** + * This static field is an identifier of the command {@link AutomationCommandRemove} for {@link RuleTemplate}s. + */ + protected static final String REMOVE_TEMPLATES = "removeTemplates"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandRemove} for {@link RuleTemplate}s. + */ + protected static final String REMOVE_TEMPLATES_SHORT = "rmts"; + + /** + * This static field is an identifier of the command {@link AutomationCommandRemove} for {@link ModuleType}s. + */ + protected static final String REMOVE_MODULE_TYPES = "removeModuleTypes"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandRemove} for {@link ModuleType}s. + */ + protected static final String REMOVE_MODULE_TYPES_SHORT = "rmmts"; + + /** + * This static field is an identifier of the command {@link AutomationCommandList} for {@link ModuleType}s. + */ + protected static final String LIST_MODULE_TYPES = "listModuleTypes"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandList} for {@link ModuleType}s. + */ + protected static final String LIST_MODULE_TYPES_SHORT = "lsmt"; + + /** + * This static field is an identifier of the command {@link AutomationCommandList} for {@link RuleTemplate}s. + */ + protected static final String LIST_TEMPLATES = "listTemplates"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandList} for {@link RuleTemplate}s. + */ + protected static final String LIST_TEMPLATES_SHORT = "lst"; + + /** + * This static field is an identifier of the command {@link AutomationCommandList} for {@link Rule}s. + */ + protected static final String LIST_RULES = "listRules"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandList} for {@link Rule}s. + */ + protected static final String LIST_RULES_SHORT = "lsr"; + + /** + * This static field is an identifier of the command {@link AutomationCommandEnableRule}. + */ + protected static final String ENABLE_RULE = "enableRule"; + + /** + * This static field is a short identifier of the command {@link AutomationCommandEnableRule}. + */ + protected static final String ENABLE_RULE_SHORT = "enr"; + + /** + * This field holds a reference to the {@link CommandlineModuleTypeProvider} instance. + */ + protected CommandlineModuleTypeProvider moduleTypeProvider; + + /** + * This field holds a reference to the {@link CommandlineTemplateProvider} instance. + */ + protected CommandlineTemplateProvider templateProvider; + + /** + * This field holds a reference to the {@link CommandlineRuleImporter} instance. + */ + protected CommandlineRuleImporter ruleImporter; + + /** + * This method is used for getting the rule corresponding to the specified UID from the RuleManager. + * + * @param uid + * specifies the wanted {@link Rule} uniquely. + * @return a {@link Rule}, corresponding to the specified UID. + */ + public abstract Rule getRule(String uid); + + /** + * This method is used to get the all existing rules from the RuleManager. + * + * @return a collection of all existing rules in the RuleManager. + */ + public abstract Collection getRules(); + + /** + * + * @param uid + * @return + */ + public abstract RuleStatus getRuleStatus(String uid); + + /** + * + * @param uid + * @param isEnabled + */ + public abstract void setEnabled(String uid, boolean isEnabled); + + /** + * This method is used for getting the {@link RuleTemplate} corresponding to the specified UID from the manager of + * the {@link Template}s. + * + * @param templateUID + * specifies the wanted {@link RuleTemplate} uniquely. + * @param locale + * a {@link Locale} that specifies the variant of the {@link RuleTemplate} that the user wants to see. + * Can be null and then the default locale will be used. + * @return a {@link RuleTemplate}, corresponding to the specified UID and locale. + */ + public abstract Template getTemplate(String templateUID, Locale locale); + + /** + * This method is used for getting the collection of {@link RuleTemplate}s corresponding to the specified locale + * from the manager of the {@link Template}s. + * + * @param locale + * a {@link Locale} that specifies the variant of the {@link RuleTemplate}s that the user wants to see. + * Can be null and then the default locale will be used. + * @return a collection of {@link RuleTemplate}s, corresponding to the specified locale. + */ + public abstract Collection getTemplates(Locale locale); + + /** + * This method is used for getting the {@link ModuleType} corresponding to the specified UID from the manager of the + * {@link ModuleType}s. + * + * @param typeUID + * specifies the wanted {@link ModuleType} uniquely. + * @param locale + * a {@link Locale} that specifies the variant of the {@link ModuleType} that the user wants to see. Can + * be null and then the default locale will be used. + * @return a {@link ModuleType}, corresponding to the specified UID and locale. + */ + public abstract ModuleType getModuleType(String typeUID, Locale locale); + + /** + * This method is used for getting the collection of {@link TriggerType}s corresponding to + * specified locale from the ModuleTypeRegistry. + * + * @param locale + * a {@link Locale} that specifies the variant of the {@link ModuleType}s that the user wants to see. Can + * be null and then the default locale will be used. + * @return a collection of {@link ModuleType}s from given class and locale. + */ + public abstract Collection getTriggers(Locale locale); + + /** + * This method is used for getting the collection of {@link ConditionType}s corresponding to + * specified locale from the ModuleTypeRegistry. + * + * @param locale + * a {@link Locale} that specifies the variant of the {@link ModuleType}s that the user wants to see. Can + * be null and then the default locale will be used. + * @return a collection of {@link ModuleType}s from given class and locale. + */ + public abstract Collection getConditions(Locale locale); + + /** + * This method is used for getting the collection of {@link ActionType}s corresponding to + * specified locale from the ModuleTypeRegistry. + * + * @param locale + * a {@link Locale} that specifies the variant of the {@link ModuleType}s that the user wants to see. Can + * be null and then the default locale will be used. + * @return a collection of {@link ModuleType}s from given class and locale. + */ + public abstract Collection getActions(Locale locale); + + /** + * This method is used for removing a rule corresponding to the specified UID from the RuleManager. + * + * @param uid + * specifies the wanted {@link Rule} uniquely. + * @return a string representing the result of the command. + */ + public abstract String removeRule(String uid); + + /** + * This method is used for removing the rules from the RuleManager, corresponding to the specified filter. + * + * @param ruleFilter + * specifies the wanted {@link Rule}s. + * @return a string representing the result of the command. + */ + public abstract String removeRules(String ruleFilter); + + /** + * This method is responsible for choosing a particular class of commands and creates an instance of this class on + * the basis of the identifier of the command. + * + * @param command + * is the identifier of the command. + * @param parameterValues + * is an array of strings which are basis for initializing the options and parameters of the command. The + * order for their description is a random. + * @return an instance of the class corresponding to the identifier of the command. + */ + protected abstract AutomationCommand parseCommand(String command, String[] parameterValues); + + /** + * Initializing method. + * + * @param bundleContext bundle's context + * @param ruleRegistry + * @param templateRegistry + */ + public void initialize(BundleContext bundleContext, ModuleTypeRegistry moduleTypeRegistry, + TemplateRegistry templateRegistry, RuleRegistry ruleRegistry) { + moduleTypeProvider = new CommandlineModuleTypeProvider(bundleContext, moduleTypeRegistry); + templateProvider = new CommandlineTemplateProvider(bundleContext, templateRegistry); + ruleImporter = new CommandlineRuleImporter(bundleContext, ruleRegistry); + } + + /** + * This method closes the providers and the importer. + */ + public void dispose() { + moduleTypeProvider.close(); + templateProvider.close(); + ruleImporter.close(); + moduleTypeProvider = null; + templateProvider = null; + ruleImporter = null; + } + + /** + * This method is responsible for exporting a set of {@link ModuleType}s in a specified file. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link ModuleType}s in text. + * @param set + * a set of {@link ModuleType}s to export. + * @param file + * a specified file for export. + * @throws Exception + * when I/O operation has failed or has been interrupted or generating of the text fails for some + * reasons. + * @return a string representing the result of the command. + */ + public String exportModuleTypes(String parserType, Set set, File file) throws Exception { + return moduleTypeProvider.exportModuleTypes(parserType, set, file); + } + + /** + * This method is responsible for exporting a set of {@link Template}s in a specified file. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link Template}s in text. + * @param set + * a set of {@link Template}s to export. + * @param file + * a specified file for export. + * @throws Exception + * when I/O operation has failed or has been interrupted or generating of the text fails for some + * reasons. + * @return a string representing the result of the command. + */ + public String exportTemplates(String parserType, Set set, File file) throws Exception { + return templateProvider.exportTemplates(parserType, set, file); + } + + /** + * This method is responsible for exporting a set of {@link Rule}s in a specified file. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link Rule}s in text. + * @param set + * a set of {@link Rule}s to export. + * @param file + * a specified file for export. + * @throws Exception + * when I/O operation has failed or has been interrupted or generating of the text fails for some + * reasons. + * @return a string representing the result of the command. + */ + public String exportRules(String parserType, Set set, File file) throws Exception { + return ruleImporter.exportRules(parserType, set, file); + } + + /** + * This method is responsible for importing a set of {@link ModuleType}s from a specified file or URL resource. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link ModuleType}s from text. + * @param url + * is a specified file or URL resource. + * @throws ParsingException + * when parsing of the text fails for some reasons. + * @throws IOException + * when I/O operation has failed or has been interrupted. + * @return a set of module types, representing the result of the command. + */ + public Set importModuleTypes(String parserType, URL url) throws Exception { + return moduleTypeProvider.importModuleTypes(parserType, url); + } + + /** + * This method is responsible for importing a set of {@link Template}s from a specified file or URL resource. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link Template}s from text. + * @param url + * is a specified file or URL resource. + * @throws ParsingException + * is thrown when parsing of the text fails for some reasons. + * @throws IOException + * is thrown when I/O operation has failed or has been interrupted. + * @return a set of templates, representing the result of the command. + */ + public Set importTemplates(String parserType, URL url) throws Exception { + return templateProvider.importTemplates(parserType, url); + } + + /** + * This method is responsible for importing a set of {@link Rule}s from a specified file or URL resource. + * + * @param parserType + * is relevant to the format that you need for conversion of the {@link Rule}s from text. + * @param url + * is a specified file or URL resource. + * @throws ParsingException + * is thrown when parsing of the text fails for some reasons. + * @throws IOException + * is thrown when I/O operation has failed or has been interrupted. + * @return a set of rules, representing the result of the command. + */ + public Set importRules(String parserType, URL url) throws Exception { + return ruleImporter.importRules(parserType, url); + } + + /** + * This method is responsible for removing a set of objects loaded from a specified file or URL resource. + * + * @param providerType + * specifies the provider responsible for removing the objects loaded from a specified file or URL + * resource. + * @param url + * is a specified file or URL resource. + * @return a string representing the result of the command. + */ + public String remove(int providerType, URL url) { + switch (providerType) { + case AutomationCommands.MODULE_TYPE_PROVIDER: + if (moduleTypeProvider != null) { + return moduleTypeProvider.remove(url); + } + break; + case AutomationCommands.TEMPLATE_PROVIDER: + if (templateProvider != null) { + return templateProvider.remove(url); + } + break; + } + return AutomationCommand.FAIL; + } + + /** + * This method is responsible for execution of every particular command and to return the result of the execution. + * + * @param command + * is an identifier of the command. + * @param parameterValues + * is an array of strings which are basis for initializing the options and parameters of the command. The + * order for their description is a random. + * @return understandable for the user message containing information on the outcome of the command. + */ + public String executeCommand(String command, String[] parameterValues) { + AutomationCommand commandInst = parseCommand(command, parameterValues); + if (commandInst != null) { + return commandInst.execute(); + } + return String.format("Command \"%s\" is not supported!", command); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandsPluggable.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandsPluggable.java new file mode 100644 index 00000000000..04f563983d0 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/AutomationCommandsPluggable.java @@ -0,0 +1,424 @@ +/** + * 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.internal.commands; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.io.console.Console; +import org.eclipse.smarthome.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleManager; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.RuleStatus; +import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.TemplateRegistry; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.type.TriggerType; +import org.osgi.service.component.ComponentContext; +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; + +/** + * This class provides functionality for defining and executing automation commands for importing, exporting, removing + * and listing the automation objects. + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + */ +@Component +public class AutomationCommandsPluggable extends AutomationCommands implements ConsoleCommandExtension { + + /** + * This constant defines the command group name. + */ + public static final String NAME = "automation"; + + /** + * This constant describes the commands group. + */ + public static final String DESCRIPTION = "Commands for managing Automation Rules, Templates and ModuleTypes resources."; + + /** + * This constant is defined for compatibility and is used to switch to a particular provider of {@code ModuleType} + * automation objects. + */ + private static final int MODULE_TYPE_REGISTRY = 3; + + /** + * This constant is defined for compatibility and is used to switch to a particular provider of {@code Template} + * automation objects. + */ + private static final int TEMPLATE_REGISTRY = 2; + + /** + * This constant is defined for compatibility and is used to switch to a particular provider of {@code Rule} + * automation objects. + */ + private static final int RULE_REGISTRY = 1; + + /** + * This field holds the reference to the {@code RuleRegistry} providing the {@code Rule} automation objects. + */ + protected RuleRegistry ruleRegistry; + + /** + * This field holds the reference to the {@code RuleManager}. + */ + protected RuleManager ruleManager; + + /** + * This field holds the reference to the {@code TemplateRegistry} providing the {@code Template} automation objects. + */ + protected TemplateRegistry templateRegistry; + + /** + * This field holds the reference to the {@code ModuleTypeRegistry} providing the {@code ModuleType} automation + * objects. + */ + protected ModuleTypeRegistry moduleTypeRegistry; + + /** + * Activating this component - called from DS. + * + * @param componentContext + */ + @Activate + protected void activate(ComponentContext componentContext) { + super.initialize(componentContext.getBundleContext(), moduleTypeRegistry, templateRegistry, ruleRegistry); + } + + /** + * Deactivating this component - called from DS. + */ + @Deactivate + protected void deactivate(ComponentContext componentContext) { + super.dispose(); + } + + /** + * Bind the {@link RuleRegistry} service - called from DS. + * + * @param ruleRegistry ruleRegistry service. + */ + @Reference + protected void setRuleRegistry(RuleRegistry ruleRegistry) { + this.ruleRegistry = ruleRegistry; + } + + /** + * Bind the {@link ModuleTypeRegistry} service - called from DS. + * + * @param moduleTypeRegistry moduleTypeRegistry service. + */ + @Reference + protected void setModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = moduleTypeRegistry; + } + + /** + * Bind the {@link TemplateRegistry} service - called from DS. + * + * @param templateRegistry templateRegistry service. + */ + @Reference + protected void setTemplateRegistry(TemplateRegistry templateRegistry) { + this.templateRegistry = templateRegistry; + } + + /** + * Bind the {@link RuleManager} service - called from DS. + * + * @param ruleManager RuleManager service. + */ + @Reference + protected void setRuleManager(RuleManager ruleManager) { + this.ruleManager = ruleManager; + } + + protected void unsetRuleRegistry(RuleRegistry ruleRegistry) { + this.ruleRegistry = null; + } + + protected void unsetModuleTypeRegistry(ModuleTypeRegistry moduleTypeRegistry) { + this.moduleTypeRegistry = null; + } + + protected void unsetTemplateRegistry(TemplateRegistry templateRegistry) { + this.templateRegistry = null; + } + + protected void unsetRuleManager(RuleManager ruleManager) { + this.ruleManager = null; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length == 0) { + console.println(StringUtils.join(getUsages(), "\n")); + return; + } + + String command = args[0];// the first argument is the subcommand name + + String[] params = new String[args.length - 1];// extract the remaining arguments except the first one + if (params.length > 0) { + System.arraycopy(args, 1, params, 0, params.length); + } + + String res = super.executeCommand(command, params); + if (res == null) { + console.println(String.format("Unsupported command %s", command)); + } else { + console.println(res); + } + } + + @Override + public List getUsages() { + return Arrays.asList(new String[] { + buildCommandUsage(LIST_MODULE_TYPES + " [-st] ", + "lists all Module Types. If filter is present, lists only matching Module Types." + + " If language is missing, the default language will be used."), + buildCommandUsage(LIST_TEMPLATES + " [-st] ", + "lists all Templates. If filter is present, lists only matching Templates." + + " If language is missing, the default language will be used."), + buildCommandUsage(LIST_RULES + " [-st] ", + "lists all Rules. If filter is present, lists only matching Rules"), + buildCommandUsage(REMOVE_MODULE_TYPES + " [-st] ", + "Removes the Module Types, loaded from the given url"), + buildCommandUsage(REMOVE_TEMPLATES + " [-st] ", + "Removes the Templates, loaded from the given url"), + buildCommandUsage(REMOVE_RULE + " [-st] ", "Removes the rule, specified by given UID"), + buildCommandUsage(REMOVE_RULES + " [-st] ", + "Removes the rules. If filter is present, removes only matching Rules"), + buildCommandUsage(IMPORT_MODULE_TYPES + " [-p] [-st] ", + "Imports Module Types from given url. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(IMPORT_TEMPLATES + " [-p] [-st] ", + "Imports Templates from given url. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(IMPORT_RULES + " [-p] [-st] ", + "Imports Rules from given url. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(EXPORT_MODULE_TYPES + " [-p] [-st] ", + "Exports Module Types in a file. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(EXPORT_TEMPLATES + " [-p] [-st] ", + "Exports Templates in a file. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(EXPORT_RULES + " [-p] [-st] ", + "Exports Rules in a file. If parser type missing, \"json\" parser will be set as default"), + buildCommandUsage(ENABLE_RULE + " [-st] ", + "Enables the Rule, specified by given UID. If enable parameter is missing, " + + "the result of the command will be visualization of enabled/disabled state of the rule, " + + "if its value is \"true\" or \"false\", " + + "the result of the command will be to set enable/disable on the Rule.") }); + } + + @Override + public String getCommand() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public Rule getRule(String uid) { + if (ruleRegistry != null) { + return ruleRegistry.get(uid); + } + return null; + } + + @Override + public RuleTemplate getTemplate(String templateUID, Locale locale) { + if (templateRegistry != null) { + return templateRegistry.get(templateUID, locale); + } + return null; + } + + @Override + public Collection getTemplates(Locale locale) { + if (templateRegistry != null) { + return templateRegistry.getAll(locale); + } + return null; + } + + @Override + public ModuleType getModuleType(String typeUID, Locale locale) { + if (moduleTypeRegistry != null) { + return moduleTypeRegistry.get(typeUID, locale); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getTriggers(Locale locale) { + if (moduleTypeRegistry != null) { + return moduleTypeRegistry.getTriggers(locale); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getConditions(Locale locale) { + if (moduleTypeRegistry != null) { + return moduleTypeRegistry.getConditions(locale); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getActions(Locale locale) { + if (moduleTypeRegistry != null) { + return moduleTypeRegistry.getActions(locale); + } + return null; + } + + @Override + public String removeRule(String uid) { + if (ruleRegistry != null) { + if (ruleRegistry.remove(uid) != null) { + return AutomationCommand.SUCCESS; + } else { + return String.format("Rule with id '%s' does not exist.", uid); + } + } + return String.format("%s! RuleRegistry not available!", AutomationCommand.FAIL); + } + + @Override + public String removeRules(String ruleFilter) { + if (ruleRegistry != null) { + for (Rule r : ruleRegistry.getAll()) { + if (r.getUID().contains(ruleFilter)) { + ruleRegistry.remove(r.getUID()); + } + } + return AutomationCommand.SUCCESS; + } + return String.format("%s! RuleRegistry not available!", AutomationCommand.FAIL); + } + + @Override + protected AutomationCommand parseCommand(String command, String[] params) { + if (command.equalsIgnoreCase(IMPORT_MODULE_TYPES)) { + return new AutomationCommandImport(IMPORT_MODULE_TYPES, params, MODULE_TYPE_REGISTRY, this); + } + if (command.equalsIgnoreCase(EXPORT_MODULE_TYPES)) { + return new AutomationCommandExport(EXPORT_MODULE_TYPES, params, MODULE_TYPE_REGISTRY, this); + } + if (command.equalsIgnoreCase(LIST_MODULE_TYPES)) { + return new AutomationCommandList(LIST_MODULE_TYPES, params, MODULE_TYPE_REGISTRY, this); + } + if (command.equalsIgnoreCase(IMPORT_TEMPLATES)) { + return new AutomationCommandImport(IMPORT_TEMPLATES, params, TEMPLATE_REGISTRY, this); + } + if (command.equalsIgnoreCase(EXPORT_TEMPLATES)) { + return new AutomationCommandExport(EXPORT_TEMPLATES, params, TEMPLATE_REGISTRY, this); + } + if (command.equalsIgnoreCase(LIST_TEMPLATES)) { + return new AutomationCommandList(LIST_TEMPLATES, params, TEMPLATE_REGISTRY, this); + } + if (command.equalsIgnoreCase(IMPORT_RULES)) { + return new AutomationCommandImport(IMPORT_RULES, params, RULE_REGISTRY, this); + } + if (command.equalsIgnoreCase(EXPORT_RULES)) { + return new AutomationCommandExport(EXPORT_RULES, params, RULE_REGISTRY, this); + } + if (command.equalsIgnoreCase(LIST_RULES)) { + return new AutomationCommandList(LIST_RULES, params, RULE_REGISTRY, this); + } + if (command.equalsIgnoreCase(REMOVE_TEMPLATES)) { + return new AutomationCommandRemove(REMOVE_TEMPLATES, params, TEMPLATE_REGISTRY, this); + } + if (command.equalsIgnoreCase(REMOVE_MODULE_TYPES)) { + return new AutomationCommandRemove(REMOVE_MODULE_TYPES, params, MODULE_TYPE_REGISTRY, this); + } + if (command.equalsIgnoreCase(REMOVE_RULE)) { + return new AutomationCommandRemove(REMOVE_RULE, params, RULE_REGISTRY, this); + } + if (command.equalsIgnoreCase(REMOVE_RULES)) { + return new AutomationCommandRemove(REMOVE_RULES, params, RULE_REGISTRY, this); + } + if (command.equalsIgnoreCase(ENABLE_RULE)) { + return new AutomationCommandEnableRule(ENABLE_RULE, params, RULE_REGISTRY, this); + } + return null; + } + + /** + * Build a command usage string. + * + * You should always use that function to use a usage string that complies + * to a standard format. + * + * @param description + * the description of the command + * @return a usage string that complies to a standard format + */ + protected String buildCommandUsage(final String description) { + return String.format("%s - %s", getCommand(), description); + } + + /** + * Build a command usage string. + * + * You should always use that function to use a usage string that complies + * to a standard format. + * + * @param syntax + * the syntax format + * @param description + * the description of the command + * @return a usage string that complies to a standard format + */ + protected String buildCommandUsage(final String syntax, final String description) { + return String.format("%s %s - %s", getCommand(), syntax, description); + } + + @Override + public Collection getRules() { + if (ruleRegistry != null) { + return ruleRegistry.getAll(); + } else { + return null; + } + } + + @Override + public RuleStatus getRuleStatus(String ruleUID) { + RuleStatusInfo rsi = ruleManager.getStatusInfo(ruleUID); + return rsi != null ? rsi.getStatus() : null; + } + + @Override + public void setEnabled(String uid, boolean isEnabled) { + ruleManager.setEnabled(uid, isEnabled); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineModuleTypeProvider.java new file mode 100644 index 00000000000..0342dd6d626 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineModuleTypeProvider.java @@ -0,0 +1,277 @@ +/** + * 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.internal.commands; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.openhab.core.automation.template.TemplateProvider; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * This class is implementation of {@link ModuleTypeProvider}. It extends functionality of + * {@link AbstractCommandProvider}. + *

    + * It is responsible for execution of {@link AutomationCommandsPluggable}, corresponding to the {@link ModuleType}s: + *

      + *
    • imports the {@link ModuleType}s from local files or from URL resources + *
    • provides functionality for persistence of the {@link ModuleType}s + *
    • removes the {@link ModuleType}s and their persistence + *
    • lists the {@link ModuleType}s and their details + *
    + *

    + * accordingly to the used command. + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * + */ +public class CommandlineModuleTypeProvider extends AbstractCommandProvider implements ModuleTypeProvider { + + /** + * This field holds a reference to the {@link TemplateProvider} service registration. + */ + @SuppressWarnings("rawtypes") + protected ServiceRegistration mtpReg; + private ModuleTypeRegistry moduleTypeRegistry; + + /** + * This constructor creates instances of this particular implementation of {@link ModuleTypeProvider}. It does not + * add any new functionality to the constructors of the providers. Only provides consistency by invoking the + * parent's constructor. + * + * @param context is the {@code BundleContext}, used for creating a tracker for {@link Parser} services. + * @param moduleTypeRegistry a ModuleTypeRegistry service + */ + public CommandlineModuleTypeProvider(BundleContext context, ModuleTypeRegistry moduleTypeRegistry) { + super(context); + listeners = new LinkedList>(); + mtpReg = bc.registerService(ModuleTypeProvider.class.getName(), this, null); + this.moduleTypeRegistry = moduleTypeRegistry; + } + + /** + * This method differentiates what type of {@link Parser}s is tracked by the tracker. + * For this concrete provider, this type is a {@link ModuleType} {@link Parser}. + * + * @see AbstractCommandProvider#addingService(org.osgi.framework.ServiceReference) + */ + @Override + public Object addingService(@SuppressWarnings("rawtypes") ServiceReference reference) { + if (reference.getProperty(Parser.PARSER_TYPE).equals(Parser.PARSER_MODULE_TYPE)) { + return super.addingService(reference); + } + return null; + } + + /** + * This method is responsible for exporting a set of ModuleTypes in a specified file. + * + * @param parserType is relevant to the format that you need for conversion of the ModuleTypes in text. + * @param set a set of ModuleTypes to export. + * @param file a specified file for export. + * @throws Exception when I/O operation has failed or has been interrupted or generating of the text fails + * for some reasons. + * @see AutomationCommandsPluggable#exportModuleTypes(String, Set, File) + */ + public String exportModuleTypes(String parserType, Set set, File file) throws Exception { + return super.exportData(parserType, set, file); + } + + /** + * This method is responsible for importing a set of ModuleTypes from a specified file or URL resource. + * + * @param parserType is relevant to the format that you need for conversion of the ModuleTypes in text. + * @param url a specified URL for import. + * @throws IOException when I/O operation has failed or has been interrupted. + * @throws ParsingException when parsing of the text fails for some reasons. + * @see AutomationCommandsPluggable#importModuleTypes(String, URL) + */ + public Set importModuleTypes(String parserType, URL url) throws IOException, ParsingException { + Parser parser = parsers.get(parserType); + if (parser != null) { + InputStream is = url.openStream(); + BufferedInputStream bis = new BufferedInputStream(is); + InputStreamReader inputStreamReader = new InputStreamReader(bis); + try { + return importData(url, parser, inputStreamReader); + } finally { + inputStreamReader.close(); + } + } else { + throw new ParsingException(new ParsingNestedException(ParsingNestedException.MODULE_TYPE, null, + new Exception("Parser " + parserType + " not available"))); + } + + } + + @SuppressWarnings("unchecked") + @Override + public ModuleType getModuleType(String UID, Locale locale) { + synchronized (providedObjectsHolder) { + return providedObjectsHolder.get(UID); + } + } + + @Override + public Collection getModuleTypes(Locale locale) { + synchronized (providedObjectsHolder) { + return !providedObjectsHolder.isEmpty() ? providedObjectsHolder.values() + : Collections. emptyList(); + } + } + + /** + * This method is responsible for removing a set of objects loaded from a specified file or URL resource. + * + * @param providerType specifies the provider responsible for removing the objects loaded from a specified file or + * URL resource. + * @param url is a specified file or URL resource. + * @return the string SUCCESS. + */ + public String remove(URL url) { + List portfolio = null; + synchronized (providerPortfolio) { + portfolio = providerPortfolio.remove(url); + } + if (portfolio != null && !portfolio.isEmpty()) { + synchronized (providedObjectsHolder) { + for (String uid : portfolio) { + notifyListeners(providedObjectsHolder.remove(uid)); + } + } + } + return AutomationCommand.SUCCESS; + } + + @Override + public void close() { + if (mtpReg != null) { + mtpReg.unregister(); + mtpReg = null; + } + super.close(); + } + + @Override + protected Set importData(URL url, Parser parser, InputStreamReader inputStreamReader) + throws ParsingException { + Set providedObjects = parser.parse(inputStreamReader); + if (providedObjects != null && !providedObjects.isEmpty()) { + String uid = null; + List portfolio = new ArrayList(); + synchronized (providerPortfolio) { + providerPortfolio.put(url, portfolio); + } + List importDataExceptions = new ArrayList(); + for (ModuleType providedObject : providedObjects) { + List exceptions = new ArrayList(); + uid = providedObject.getUID(); + checkExistence(uid, exceptions); + if (exceptions.isEmpty()) { + portfolio.add(uid); + synchronized (providedObjectsHolder) { + notifyListeners(providedObjectsHolder.put(uid, providedObject), providedObject); + } + } else { + importDataExceptions.addAll(exceptions); + } + } + if (!importDataExceptions.isEmpty()) { + throw new ParsingException(importDataExceptions); + } + } + return providedObjects; + } + + /** + * This method is responsible for checking the existence of {@link ModuleType}s with the same + * UIDs before these objects to be added in the system. + * + * @param uid UID of the newly created {@link ModuleType}, which to be checked. + * @param exceptions accumulates exceptions if {@link ModuleType} with the same UID exists. + */ + protected void checkExistence(String uid, List exceptions) { + if (this.moduleTypeRegistry == null) { + exceptions.add(new ParsingNestedException(ParsingNestedException.MODULE_TYPE, uid, + new IllegalArgumentException("Failed to create Module Type with UID \"" + uid + + "\"! Can't guarantee yet that other Module Type with the same UID does not exist."))); + } + if (moduleTypeRegistry.get(uid) != null) { + exceptions.add(new ParsingNestedException(ParsingNestedException.MODULE_TYPE, uid, + new IllegalArgumentException("Module Type with UID \"" + uid + + "\" already exists! Failed to create a second with the same UID!"))); + } + } + + @Override + public Collection getAll() { + return new LinkedList(providedObjectsHolder.values()); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + protected void notifyListeners(ModuleType oldElement, ModuleType newElement) { + synchronized (listeners) { + for (ProviderChangeListener listener : listeners) { + if (oldElement != null) { + listener.updated(this, oldElement, newElement); + } + listener.added(this, newElement); + } + } + } + + protected void notifyListeners(ModuleType removedObject) { + if (removedObject != null) { + synchronized (listeners) { + for (ProviderChangeListener listener : listeners) { + listener.removed(this, removedObject); + } + } + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineRuleImporter.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineRuleImporter.java new file mode 100644 index 00000000000..ad55978fac8 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineRuleImporter.java @@ -0,0 +1,135 @@ +/** + * 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.internal.commands; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Iterator; +import java.util.Set; + +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * This class is a {@link Rule}s importer. It extends functionality of {@link AbstractCommandProvider}. + *

    + * It is responsible for execution of Automation Commands, corresponding to the {@link Rule}s: + *

      + *
    • imports the {@link Rule}s from local files or from URL resources + *
    • provides functionality for persistence of the {@link Rule}s + *
    • removes the {@link Rule}s and their persistence + *
    • lists the {@link Rule}s and their details + *
    + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * + */ +public class CommandlineRuleImporter extends AbstractCommandProvider { + + private final RuleRegistry ruleRegistry; + + /** + * This constructor creates instances of this particular implementation of Rule Importer. It does not add any new + * functionality to the constructors of the providers. Only provides consistency by invoking the parent's + * constructor. + * + * @param context is the {@link BundleContext}, used for creating a tracker for {@link Parser} services. + * @param ruleRegistry + */ + public CommandlineRuleImporter(BundleContext context, RuleRegistry ruleRegistry) { + super(context); + this.ruleRegistry = ruleRegistry; + } + + /** + * This method differentiates what type of {@link Parser}s is tracked by the tracker. + * For this concrete provider, this type is a {@link Rule} {@link Parser}. + * + * @see AbstractCommandProvider#addingService(org.osgi.framework.ServiceReference) + */ + @Override + public Object addingService(@SuppressWarnings("rawtypes") ServiceReference reference) { + if (reference.getProperty(Parser.PARSER_TYPE).equals(Parser.PARSER_RULE)) { + return super.addingService(reference); + } + return null; + } + + /** + * This method is responsible for exporting a set of Rules in a specified file. + * + * @param parserType is relevant to the format that you need for conversion of the Rules in text. + * @param set a set of Rules to export. + * @param file a specified file for export. + * @throws Exception when I/O operation has failed or has been interrupted or generating of the text fails + * for some reasons. + * @see AutomationCommandsPluggable#exportRules(String, Set, File) + */ + public String exportRules(String parserType, Set set, File file) throws Exception { + return super.exportData(parserType, set, file); + } + + /** + * This method is responsible for importing a set of Rules from a specified file or URL resource. + * + * @param parserType is relevant to the format that you need for conversion of the Rules in text. + * @param url a specified URL for import. + * @throws IOException when I/O operation has failed or has been interrupted. + * @throws ParsingException when parsing of the text fails for some reasons. + * @see AutomationCommandsPluggable#importRules(String, URL) + */ + public Set importRules(String parserType, URL url) throws IOException, ParsingException { + Parser parser = parsers.get(parserType); + if (parser != null) { + InputStreamReader inputStreamReader = new InputStreamReader(new BufferedInputStream(url.openStream())); + try { + return importData(url, parser, inputStreamReader); + } finally { + inputStreamReader.close(); + } + } else { + throw new ParsingException(new ParsingNestedException(ParsingNestedException.RULE, null, + new Exception("Parser " + parserType + " not available"))); + } + } + + @Override + protected Set importData(URL url, Parser parser, InputStreamReader inputStreamReader) + throws ParsingException { + Set providedRules = parser.parse(inputStreamReader); + if (providedRules != null && !providedRules.isEmpty()) { + Iterator i = providedRules.iterator(); + while (i.hasNext()) { + Rule rule = i.next(); + if (rule != null) { + if (ruleRegistry.get(rule.getUID()) != null) { + ruleRegistry.update(rule); + } else { + ruleRegistry.add(rule); + } + } + } + } + return providedRules; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineTemplateProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineTemplateProvider.java new file mode 100644 index 00000000000..368ba4edce8 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/CommandlineTemplateProvider.java @@ -0,0 +1,267 @@ +/** + * 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.internal.commands; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.RuleTemplateProvider; +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.template.TemplateProvider; +import org.openhab.core.automation.template.TemplateRegistry; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * This class is implementation of {@link TemplateProvider}. It extends functionality of {@link AbstractCommandProvider} + *

    + * It is responsible for execution of {@link AutomationCommandsPluggable}, corresponding to the {@link RuleTemplate}s: + *

      + *
    • imports the {@link RuleTemplate}s from local files or from URL resources + *
    • provides functionality for persistence of the {@link RuleTemplate}s + *
    • removes the {@link RuleTemplate}s and their persistence + *
    + * + * @author Ana Dimova - Initial Contribution + * @author Kai Kreuzer - refactored (managed) provider and registry implementation + * + */ +public class CommandlineTemplateProvider extends AbstractCommandProvider implements RuleTemplateProvider { + + /** + * This field holds a reference to the {@link ModuleTypeProvider} service registration. + */ + @SuppressWarnings("rawtypes") + protected ServiceRegistration tpReg; + private final TemplateRegistry templateRegistry; + + /** + * This constructor creates instances of this particular implementation of {@link TemplateProvider}. It does not add + * any new functionality to the constructors of the providers. Only provides consistency by invoking the parent's + * constructor. + * + * @param context is the {@link BundleContext}, used for creating a tracker for {@link Parser} services. + */ + public CommandlineTemplateProvider(BundleContext context, TemplateRegistry templateRegistry) { + super(context); + listeners = new LinkedList>(); + tpReg = bc.registerService(RuleTemplateProvider.class.getName(), this, null); + this.templateRegistry = templateRegistry; + } + + /** + * This method differentiates what type of {@link Parser}s is tracked by the tracker. + * For this concrete provider, this type is a {@link RuleTemplate} {@link Parser}. + * + * @see AbstractCommandProvider#addingService(org.osgi.framework.ServiceReference) + */ + @Override + public Object addingService(@SuppressWarnings("rawtypes") ServiceReference reference) { + if (reference.getProperty(Parser.PARSER_TYPE).equals(Parser.PARSER_TEMPLATE)) { + return super.addingService(reference); + } + return null; + } + + /** + * This method is responsible for exporting a set of RuleTemplates in a specified file. + * + * @param parserType is relevant to the format that you need for conversion of the RuleTemplates in text. + * @param set a set of RuleTemplates to export. + * @param file a specified file for export. + * @throws Exception when I/O operation has failed or has been interrupted or generating of the text fails + * for some reasons. + * @see AutomationCommandsPluggable#exportTemplates(String, Set, File) + */ + public String exportTemplates(String parserType, Set set, File file) throws Exception { + return super.exportData(parserType, set, file); + } + + /** + * This method is responsible for importing a set of RuleTemplates from a specified file or URL resource. + * + * @param parserType is relevant to the format that you need for conversion of the RuleTemplates in text. + * @param url a specified URL for import. + * @throws IOException when I/O operation has failed or has been interrupted. + * @throws ParsingException when parsing of the text fails for some reasons. + * @see AutomationCommandsPluggable#importTemplates(String, URL) + */ + public Set importTemplates(String parserType, URL url) throws IOException, ParsingException { + Parser parser = parsers.get(parserType); + if (parser != null) { + InputStreamReader inputStreamReader = new InputStreamReader(new BufferedInputStream(url.openStream())); + try { + return importData(url, parser, inputStreamReader); + } finally { + inputStreamReader.close(); + } + } else { + throw new ParsingException(new ParsingNestedException(ParsingNestedException.TEMPLATE, null, + new Exception("Parser " + parserType + " not available"))); + } + } + + @Override + public RuleTemplate getTemplate(String UID, Locale locale) { + synchronized (providerPortfolio) { + return providedObjectsHolder.get(UID); + } + } + + @Override + public Collection getTemplates(Locale locale) { + synchronized (providedObjectsHolder) { + return providedObjectsHolder.values(); + } + } + + /** + * This method is responsible for removing a set of objects loaded from a specified file or URL resource. + * + * @param providerType specifies the provider responsible for removing the objects loaded from a specified file or + * URL resource. + * @param url is a specified file or URL resource. + * @return the string SUCCESS. + */ + public String remove(URL url) { + List portfolio = null; + synchronized (providerPortfolio) { + portfolio = providerPortfolio.remove(url); + } + if (portfolio != null && !portfolio.isEmpty()) { + synchronized (providedObjectsHolder) { + for (String uid : portfolio) { + notifyListeners(providedObjectsHolder.remove(uid)); + } + } + } + return AutomationCommand.SUCCESS; + } + + @Override + public void close() { + if (tpReg != null) { + tpReg.unregister(); + tpReg = null; + } + super.close(); + } + + @Override + protected Set importData(URL url, Parser parser, InputStreamReader inputStreamReader) + throws ParsingException { + Set providedObjects = parser.parse(inputStreamReader); + if (providedObjects != null && !providedObjects.isEmpty()) { + List portfolio = new ArrayList(); + synchronized (providerPortfolio) { + providerPortfolio.put(url, portfolio); + } + List importDataExceptions = new ArrayList(); + for (RuleTemplate ruleT : providedObjects) { + List exceptions = new ArrayList(); + String uid = ruleT.getUID(); + checkExistence(uid, exceptions); + if (exceptions.isEmpty()) { + portfolio.add(uid); + synchronized (providedObjectsHolder) { + notifyListeners(providedObjectsHolder.put(uid, ruleT), ruleT); + } + } else { + importDataExceptions.addAll(exceptions); + } + } + if (!importDataExceptions.isEmpty()) { + throw new ParsingException(importDataExceptions); + } + } + return providedObjects; + } + + /** + * This method is responsible for checking the existence of {@link Template}s with the same + * UIDs before these objects to be added in the system. + * + * @param uid UID of the newly created {@link Template}, which to be checked. + * @param exceptions accumulates exceptions if {@link ModuleType} with the same UID exists. + */ + protected void checkExistence(String uid, List exceptions) { + if (templateRegistry == null) { + exceptions.add(new ParsingNestedException(ParsingNestedException.TEMPLATE, uid, + new IllegalArgumentException("Failed to create Rule Template with UID \"" + uid + + "\"! Can't guarantee yet that other Rule Template with the same UID does not exist."))); + } + if (templateRegistry.get(uid) != null) { + exceptions.add(new ParsingNestedException(ParsingNestedException.TEMPLATE, uid, + new IllegalArgumentException("Rule Template with UID \"" + uid + + "\" already exists! Failed to create a second with the same UID!"))); + } + } + + @Override + public Collection getAll() { + return new LinkedList(providedObjectsHolder.values()); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + protected void notifyListeners(RuleTemplate oldElement, RuleTemplate newElement) { + synchronized (listeners) { + for (ProviderChangeListener listener : listeners) { + if (oldElement != null) { + listener.updated(this, oldElement, newElement); + } + listener.added(this, newElement); + } + } + } + + protected void notifyListeners(RuleTemplate removedObject) { + if (removedObject != null) { + synchronized (listeners) { + for (ProviderChangeListener listener : listeners) { + listener.removed(this, removedObject); + } + } + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java new file mode 100644 index 00000000000..43c8dbb2dcf --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Printer.java @@ -0,0 +1,576 @@ +/** + * 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.internal.commands; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.FilterCriteria; +import org.eclipse.smarthome.config.core.ParameterOption; +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.RuleStatus; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.Template; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; + +/** + * This class provides the functionality responsible for printing the automation objects as a result of commands. + * + * @author Ana Dimova - Initial Contribution + * @author Yordan Mihaylov - updates related to api changes + * + */ +public class Printer { + + private static final int TABLE_WIDTH = 100; + private static final int COLUMN_ID = 7; + private static final int COLUMN_UID = 93; + private static final int COLUMN_RULE_UID = 36; + private static final int COLUMN_RULE_NAME = 36; + private static final int COLUMN_RULE_STATUS = 15; + private static final int COLUMN_PROPERTY = 28; + private static final int COLUMN_PROPERTY_VALUE = 72; + private static final int COLUMN_CONFIG_PARAMETER = 20; + private static final int COLUMN_CONFIG_PARAMETER_VALUE = 52; + private static final int COLUMN_CONFIG_PARAMETER_PROP = 16; + private static final int COLUMN_CONFIG_PARAMETER_PROP_VALUE = 36; + private static final String ID = "ID"; + private static final String UID = "UID"; + private static final String NAME = "NAME"; + private static final String STATUS = "STATUS"; + private static final String TAGS = "TAGS"; + private static final String LABEL = "LABEL"; + private static final String VISIBILITY = "VISIBILITY"; + private static final String DESCRIPTION = "DESCRIPTION"; + private static final String CONFIGURATION_DESCRIPTIONS = "CONFIGURATION DESCRIPTIONS "; + private static final String ACTIONS = "ACTIONS"; + private static final String TRIGGERS = "TRIGGERS"; + private static final String CONDITIONS = "CONDITIONS"; + private static final String INPUTS = "INPUTS"; + private static final String OUTPUTS = "OUTPUTS"; + private static final String CHILDREN = "CHILDREN"; + private static final String TYPE = "TYPE"; + private static final String CONFIGURATION = "CONFIGURATION"; + private static final String MIN = "MIN"; + private static final String MAX = "MAX"; + private static final String DEFAULT = "DEFAULT"; + private static final String CONTEXT = "CONTEXT"; + private static final String PATTERN = "PATTERN"; + private static final String OPTIONS = "OPTIONS"; + private static final String STEP_SIZE = "STEP_SIZE"; + private static final String FILTER_CRITERIA = "FILTER CRITERIA "; + private static final String REQUIRED = "REQUIRED"; + private static final String NOT_REQUIRED = "NOT REQUIRED"; + + /** + * This method is responsible for printing the list with indexes, UIDs, names and statuses of the {@link Rule}s. + * + * @param autoCommands + * @param ruleUIDs + * @return + */ + static String printRules(AutomationCommandsPluggable autoCommands, Map ruleUIDs) { + int[] columnWidths = new int[] { COLUMN_ID, COLUMN_RULE_UID, COLUMN_RULE_NAME, COLUMN_RULE_STATUS }; + List columnValues = new ArrayList(); + columnValues.add(ID); + columnValues.add(UID); + columnValues.add(NAME); + columnValues.add(STATUS); + String titleRow = Utils.getRow(columnWidths, columnValues); + + List rulesRows = new ArrayList(); + for (int i = 1; i <= ruleUIDs.size(); i++) { + String id = new Integer(i).toString(); + String uid = ruleUIDs.get(id); + columnValues.set(0, id); + columnValues.set(1, uid); + Rule rule = autoCommands.getRule(uid); + columnValues.set(2, rule.getName()); + columnValues.set(3, autoCommands.getRuleStatus(uid).toString()); + rulesRows.add(Utils.getRow(columnWidths, columnValues)); + } + return Utils.getTableContent(TABLE_WIDTH, columnWidths, rulesRows, titleRow); + } + + /** + * This method is responsible for printing the list with indexes and UIDs of the {@link Template}s. + * + * @param templateUIDs is a map with keys UIDs of the {@link Template}s and values the {@link Template}s. + * @return a formated string, representing the sorted list with indexed UIDs of the available {@link Template}s. + */ + static String printTemplates(Map templateUIDs) { + int[] columnWidths = new int[] { COLUMN_ID, COLUMN_UID }; + List columnTitles = new ArrayList(); + columnTitles.add(ID); + columnTitles.add(UID); + String titleRow = Utils.getRow(columnWidths, columnTitles); + + List templates = new ArrayList(); + collectListRecords(templateUIDs, templates, columnWidths); + return Utils.getTableContent(TABLE_WIDTH, columnWidths, templates, titleRow); + } + + /** + * This method is responsible for printing the list with indexes and UIDs of the {@link ModuleType}s. + * + * @param moduleTypeUIDs is a map with keys UIDs of the {@link ModuleType}s and values the {@link ModuleType}s. + * @return a formated string, representing the sorted list with indexed UIDs of the available {@link ModuleType}s. + */ + static String printModuleTypes(Map moduleTypeUIDs) { + int[] columnWidths = new int[] { COLUMN_ID, COLUMN_UID }; + List columnTitles = new ArrayList(); + columnTitles.add(ID); + columnTitles.add(UID); + String titleRow = Utils.getRow(columnWidths, columnTitles); + + List moduleTypes = new ArrayList(); + collectListRecords(moduleTypeUIDs, moduleTypes, columnWidths); + return Utils.getTableContent(TABLE_WIDTH, columnWidths, moduleTypes, titleRow); + } + + /** + * This method is responsible for printing the {@link Rule}. + * + * @param rule the {@link Rule} for printing. + * @return a formated string, representing the {@link Rule} info. + */ + static String printRule(Rule rule, RuleStatus status) { + int[] columnWidths = new int[] { TABLE_WIDTH }; + List ruleProperty = new ArrayList(); + ruleProperty.add(rule.getUID() + " [ " + status + " ]"); + String titleRow = Utils.getRow(columnWidths, ruleProperty); + + List ruleContent = new ArrayList(); + columnWidths = new int[] { COLUMN_PROPERTY, COLUMN_PROPERTY_VALUE }; + ruleProperty.set(0, UID); + ruleProperty.add(rule.getUID()); + ruleContent.add(Utils.getRow(columnWidths, ruleProperty)); + if (rule.getName() != null) { + ruleProperty.set(0, NAME); + ruleProperty.set(1, rule.getName()); + ruleContent.add(Utils.getRow(columnWidths, ruleProperty)); + } + if (rule.getDescription() != null) { + ruleProperty.set(0, DESCRIPTION); + ruleProperty.set(1, rule.getDescription()); + ruleContent.add(Utils.getRow(columnWidths, ruleProperty)); + } + ruleProperty.set(0, TAGS); + ruleProperty.set(1, getTagsRecord(rule.getTags())); + ruleContent.add(Utils.getRow(columnWidths, ruleProperty)); + + ruleContent.addAll( + collectRecords(columnWidths, CONFIGURATION, rule.getConfiguration().getProperties().entrySet())); + ruleContent.addAll(collectRecords(columnWidths, CONFIGURATION_DESCRIPTIONS, + getConfigurationDescriptionRecords(rule.getConfigurationDescriptions()))); + ruleContent.addAll(collectRecords(columnWidths, TRIGGERS, rule.getTriggers())); + ruleContent.addAll(collectRecords(columnWidths, CONDITIONS, rule.getConditions())); + ruleContent.addAll(collectRecords(columnWidths, ACTIONS, rule.getActions())); + + return Utils.getTableContent(TABLE_WIDTH, columnWidths, ruleContent, titleRow); + } + + /** + * This method is responsible for printing the {@link Template}. + * + * @param template the {@link Template} for printing. + * @return a formated string, representing the {@link Template} info. + */ + static String printTemplate(Template template) { + int[] columnWidths = new int[] { TABLE_WIDTH }; + List templateProperty = new ArrayList(); + templateProperty.add(template.getUID()); + String titleRow = Utils.getRow(columnWidths, templateProperty); + + List templateContent = new ArrayList(); + columnWidths = new int[] { COLUMN_PROPERTY, COLUMN_PROPERTY_VALUE }; + templateProperty.set(0, UID); + templateProperty.add(template.getUID()); + templateContent.add(Utils.getRow(columnWidths, templateProperty)); + if (template.getLabel() != null) { + templateProperty.set(0, LABEL); + templateProperty.set(1, template.getLabel()); + templateContent.add(Utils.getRow(columnWidths, templateProperty)); + } + if (template.getDescription() != null) { + templateProperty.set(0, DESCRIPTION); + templateProperty.set(1, template.getDescription()); + templateContent.add(Utils.getRow(columnWidths, templateProperty)); + } + templateProperty.set(0, VISIBILITY); + templateProperty.set(1, template.getVisibility().toString()); + templateContent.add(Utils.getRow(columnWidths, templateProperty)); + + templateProperty.set(0, TAGS); + templateProperty.set(1, getTagsRecord(template.getTags())); + templateContent.add(Utils.getRow(columnWidths, templateProperty)); + if (template instanceof RuleTemplate) { + templateContent.addAll(collectRecords(columnWidths, CONFIGURATION_DESCRIPTIONS, + getConfigurationDescriptionRecords(((RuleTemplate) template).getConfigurationDescriptions()))); + templateContent.addAll(collectRecords(columnWidths, TRIGGERS, ((RuleTemplate) template).getTriggers())); + templateContent.addAll(collectRecords(columnWidths, CONDITIONS, ((RuleTemplate) template).getConditions())); + templateContent.addAll(collectRecords(columnWidths, ACTIONS, ((RuleTemplate) template).getActions())); + } + return Utils.getTableContent(TABLE_WIDTH, columnWidths, templateContent, titleRow); + } + + /** + * This method is responsible for printing the {@link ModuleType}. + * + * @param moduleType the {@link ModuleType} for printing. + * @return a formated string, representing the {@link ModuleType} info. + */ + static String printModuleType(ModuleType moduleType) { + int[] columnWidths = new int[] { TABLE_WIDTH }; + List moduleTypeProperty = new ArrayList(); + moduleTypeProperty.add(moduleType.getUID()); + String titleRow = Utils.getRow(columnWidths, moduleTypeProperty); + + List moduleTypeContent = new ArrayList(); + columnWidths = new int[] { COLUMN_PROPERTY, COLUMN_PROPERTY_VALUE }; + moduleTypeProperty.set(0, UID); + moduleTypeProperty.add(moduleType.getUID()); + moduleTypeContent.add(Utils.getRow(columnWidths, moduleTypeProperty)); + if (moduleType.getLabel() != null) { + moduleTypeProperty.set(0, LABEL); + moduleTypeProperty.set(1, moduleType.getLabel()); + moduleTypeContent.add(Utils.getRow(columnWidths, moduleTypeProperty)); + } + if (moduleType.getDescription() != null) { + moduleTypeProperty.set(0, DESCRIPTION); + moduleTypeProperty.set(1, moduleType.getDescription()); + moduleTypeContent.add(Utils.getRow(columnWidths, moduleTypeProperty)); + } + moduleTypeProperty.set(0, VISIBILITY); + moduleTypeProperty.set(1, moduleType.getVisibility().toString()); + moduleTypeContent.add(Utils.getRow(columnWidths, moduleTypeProperty)); + + moduleTypeProperty.set(0, TAGS); + moduleTypeProperty.set(1, getTagsRecord(moduleType.getTags())); + moduleTypeContent.add(Utils.getRow(columnWidths, moduleTypeProperty)); + + moduleTypeContent.addAll(collectRecords(columnWidths, CONFIGURATION_DESCRIPTIONS, + getConfigurationDescriptionRecords(moduleType.getConfigurationDescriptions()))); + if (moduleType instanceof TriggerType) { + moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, ((TriggerType) moduleType).getOutputs())); + } + if (moduleType instanceof ConditionType) { + moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, ((ConditionType) moduleType).getInputs())); + } + if (moduleType instanceof ActionType) { + moduleTypeContent.addAll(collectRecords(columnWidths, INPUTS, ((ActionType) moduleType).getInputs())); + moduleTypeContent.addAll(collectRecords(columnWidths, OUTPUTS, ((ActionType) moduleType).getOutputs())); + } + if (moduleType instanceof CompositeTriggerType) { + moduleTypeContent + .addAll(collectRecords(columnWidths, CHILDREN, ((CompositeTriggerType) moduleType).getChildren())); + } + if (moduleType instanceof CompositeConditionType) { + moduleTypeContent.addAll( + collectRecords(columnWidths, CHILDREN, ((CompositeConditionType) moduleType).getChildren())); + } + if (moduleType instanceof CompositeActionType) { + moduleTypeContent + .addAll(collectRecords(columnWidths, CHILDREN, ((CompositeActionType) moduleType).getChildren())); + } + return Utils.getTableContent(TABLE_WIDTH, columnWidths, moduleTypeContent, titleRow); + } + + /** + * This method is responsible for printing the {@link RuleStatus}. + * + * @param ruleUID specifies the rule, which status is requested. + * @param status corresponds to the status of specified rule. + * @return a string representing the response of the command {@link AutomationCommands#ENABLE_RULE}. + */ + static String printRuleStatus(String ruleUID, RuleStatus status) { + List title = new ArrayList(); + title.add(ruleUID + " [ " + status + " ]"); + String titleRow = Utils.getRow(new int[] { TABLE_WIDTH }, title); + List res = Utils.getTableTitle(titleRow, TABLE_WIDTH); + StringBuilder sb = new StringBuilder(); + for (String line : res) { + sb.append(line + Utils.ROW_END); + } + return sb.toString(); + } + + /** + * This method is responsible for printing the strings, representing the auxiliary automation objects. + * + * @param columnWidths represents the column widths of the table. + * @param width represents the table width. + * @param prop is a property name of the property with value the collection of the auxiliary automation objects for + * printing. + * @param list with the auxiliary automation objects for printing. + * @return list of strings, representing the auxiliary automation objects. + */ + @SuppressWarnings("unchecked") + private static List collectRecords(int[] columnWidths, String prop, Collection list) { + List res = new ArrayList(); + boolean isFirst = true; + boolean isList = false; + List values = new ArrayList(); + values.add(prop); + values.add(""); + if (list != null && !list.isEmpty()) { + for (Object element : list) { + if (element instanceof String) { + res.add(Utils.getColumn(columnWidths[0], values.get(0)) + (String) element); + if (isFirst) { + isFirst = false; + values.set(0, ""); + } + } else if (element instanceof Module) { + List moduleRecords = getModuleRecords((Module) element); + for (String elementRecord : moduleRecords) { + res.add(Utils.getColumn(columnWidths[0], values.get(0)) + elementRecord); + if (isFirst) { + isFirst = false; + values.set(0, ""); + } + } + } else { + isList = true; + if (isFirst) { + values.set(1, "["); + res.add(Utils.getRow(columnWidths, values)); + isFirst = false; + } + values.set(0, ""); + if (element instanceof FilterCriteria) { + values.set(1, getFilterCriteriaRecord((FilterCriteria) element)); + } else if (element instanceof ParameterOption) { + values.set(1, getParameterOptionRecord((ParameterOption) element)); + } else if (element instanceof Input) { + values.set(1, getInputRecord((Input) element)); + } else if (element instanceof Output) { + values.set(1, getOutputRecord((Output) element)); + } else if (element instanceof Entry) { + values.set(1, " " + ((Entry) element).getKey() + " = \"" + + ((Entry) element).getValue().toString() + "\""); + } + res.add(Utils.getRow(columnWidths, values)); + } + } + if (isList) { + values.set(0, ""); + values.set(1, "]"); + res.add(Utils.getRow(columnWidths, values)); + } + } + return res; + } + + /** + * This method is responsible for printing the {@link Module}. + * + * @param module the {@link Module} for printing. + * @return a formated string, representing the {@link Module}. + */ + private static List getModuleRecords(Module module) { + int[] columnWidths = new int[] { COLUMN_PROPERTY_VALUE }; + List columnValues = new ArrayList(); + columnValues.add(module.getId()); + List moduleContent = new ArrayList(); + moduleContent.addAll(Utils.getTableTitle(Utils.getRow(columnWidths, columnValues), COLUMN_PROPERTY_VALUE)); + + columnWidths = new int[] { COLUMN_CONFIG_PARAMETER, COLUMN_CONFIG_PARAMETER_VALUE }; + columnValues.set(0, ID); + columnValues.add(module.getId()); + moduleContent.add(Utils.getRow(columnWidths, columnValues)); + + if (module.getLabel() != null) { + columnValues.set(0, LABEL); + columnValues.set(1, module.getLabel()); + moduleContent.add(Utils.getRow(columnWidths, columnValues)); + } + if (module.getDescription() != null) { + columnValues.set(0, DESCRIPTION); + columnValues.set(1, module.getDescription()); + moduleContent.add(Utils.getRow(columnWidths, columnValues)); + } + + columnValues.set(0, TYPE); + columnValues.set(1, module.getTypeUID()); + moduleContent.add(Utils.getRow(columnWidths, columnValues)); + + moduleContent.addAll( + collectRecords(columnWidths, CONFIGURATION, module.getConfiguration().getProperties().entrySet())); + Map inputs = null; + if (module instanceof Condition) { + inputs = ((Condition) module).getInputs(); + } + if (module instanceof Action) { + inputs = ((Action) module).getInputs(); + } + if (inputs != null && !inputs.isEmpty()) { + moduleContent.addAll( + collectRecords(columnWidths, INPUTS, new ArrayList>(inputs.entrySet()))); + } + return moduleContent; + } + + private static String getParameterOptionRecord(ParameterOption option) { + return " value=\"" + option.getValue() + "\", label=\"" + option.getLabel() + "\""; + } + + private static String getFilterCriteriaRecord(FilterCriteria criteria) { + return " name=\"" + criteria.getName() + "\", value=\"" + criteria.getValue() + "\""; + } + + private static String getInputRecord(Input input) { + return " name=\"" + input.getName() + "\", label=\"" + input.getLabel() + "\", decription=\"" + + input.getDescription() + "\", type=\"" + input.getType() + "\", " + + (input.isRequired() ? REQUIRED : NOT_REQUIRED) + + (input.getDefaultValue() != null ? "\", default=\"" + input.getDefaultValue() : ""); + } + + private static String getOutputRecord(Output output) { + return " name=\"" + output.getName() + "\", label=\"" + output.getLabel() + "\", decription=\"" + + output.getDescription() + "\", type=\"" + output.getType() + "\""; + } + + /** + * This method is responsible for printing the set of {@link ConfigDescriptionParameter}s. + * + * @param configDescriptions set of {@link ConfigDescriptionParameter}s for printing. + * @return a formated string, representing the set of {@link ConfigDescriptionParameter}s. + */ + private static List getConfigurationDescriptionRecords( + List configDescriptions) { + List configParamContent = new ArrayList(); + if (configDescriptions != null && !configDescriptions.isEmpty()) { + for (ConfigDescriptionParameter parameter : configDescriptions) { + int[] columnWidths = new int[] { COLUMN_CONFIG_PARAMETER, COLUMN_CONFIG_PARAMETER_PROP, + COLUMN_CONFIG_PARAMETER_PROP_VALUE }; + configParamContent.add(Utils.getColumn(COLUMN_PROPERTY_VALUE, parameter.getName() + " : ")); + List configParamProperty = new ArrayList(); + configParamProperty.add(""); + configParamProperty.add(TYPE); + configParamProperty.add(parameter.getType().toString()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + if (parameter.getLabel() != null) { + configParamProperty.set(1, LABEL); + configParamProperty.set(2, parameter.getLabel()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getDescription() != null) { + configParamProperty.set(1, DESCRIPTION); + configParamProperty.set(2, parameter.getDescription()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getDefault() != null) { + configParamProperty.set(1, DEFAULT); + configParamProperty.set(2, parameter.getDefault()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getContext() != null) { + configParamProperty.set(1, CONTEXT); + configParamProperty.set(2, parameter.getContext()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getPattern() != null) { + configParamProperty.set(1, PATTERN); + configParamProperty.set(2, parameter.getPattern()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getStepSize() != null) { + configParamProperty.set(1, STEP_SIZE); + configParamProperty.set(2, parameter.getStepSize().toString()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getMinimum() != null) { + configParamProperty.set(1, MIN); + configParamProperty.set(2, parameter.getMinimum().toString()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + if (parameter.getMaximum() != null) { + configParamProperty.set(1, MAX); + configParamProperty.set(2, parameter.getMaximum().toString()); + configParamContent.add(Utils.getRow(columnWidths, configParamProperty)); + } + columnWidths = new int[] { COLUMN_CONFIG_PARAMETER_PROP, COLUMN_CONFIG_PARAMETER_PROP_VALUE }; + List options = collectRecords(columnWidths, OPTIONS, parameter.getOptions()); + for (String option : options) { + configParamContent.add(Utils.getColumn(COLUMN_CONFIG_PARAMETER, "") + option); + } + List filters = collectRecords(columnWidths, FILTER_CRITERIA, parameter.getFilterCriteria()); + for (String filter : filters) { + configParamContent.add(Utils.getColumn(COLUMN_CONFIG_PARAMETER, "") + filter); + } + configParamContent + .add(Utils.getColumn(COLUMN_PROPERTY_VALUE, Utils.printChars('-', COLUMN_PROPERTY_VALUE))); + } + } + return configParamContent; + } + + /** + * This method is responsible for printing the set of {@link Input}s or {@link Output}s or {@link Inputs}s. + * + * @param set is the set of {@link Input}s or {@link Output}s or {@link Inputs}s for printing. + * @return a formated string, representing the set of {@link Input}s or {@link Output}s or {@link Input}s. + */ + private static String getTagsRecord(Set tags) { + if (tags == null || tags.size() == 0) { + return "[ ]"; + } + StringBuilder res = new StringBuilder().append("[ "); + int i = 1; + for (String tag : tags) { + if (i < tags.size()) { + res.append(tag + ", "); + } else { + res.append(tag); + } + i++; + } + return res.append(" ]").toString(); + } + + /** + * This method is responsible for constructing the rows of a table with 2 columns - first column is for the + * numbering, second column is for the numbered records. + * + * @param list is the list with row values for printing. + * @param rows is used for accumulation of result + * @param columnWidths represents the column widths of the table. + */ + private static void collectListRecords(Map list, List rows, int[] columnWidths) { + for (int i = 1; i <= list.size(); i++) { + String id = new Integer(i).toString(); + String uid = list.get(id); + List columnValues = new ArrayList(); + columnValues.add(id); + columnValues.add(uid); + rows.add(Utils.getRow(columnWidths, columnValues)); + } + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Utils.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Utils.java new file mode 100644 index 00000000000..018ced4faf1 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/commands/Utils.java @@ -0,0 +1,301 @@ +/** + * 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.internal.commands; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * This class contains methods for facilitating sorting and filtering lists stored in {@link Hashtable}s. + * + * @author Ana Dimova - Initial Contribution + * + */ +public class Utils { + + /** + * These constants are used for drawing the table borders. + */ + static final String ROW_END = "\n"; + static final char FILLER = ' '; + static final char TABLE_DELIMITER = '-'; + + /** + * This method puts in a map, the UIDs of the automation objects. Keys are corresponding to the index into the + * array with strings, which is lexicographically sorted. + * + * @param strings holds the list with sorted lexicographically strings. + * @return an indexed UIDs of the automation objects. + */ + static Map putInHastable(String[] strings) { + Hashtable sorted = new Hashtable(); + for (int i = 0; i < strings.length; i++) { + sorted.put(new Integer(i + 1).toString(), strings[i]); + } + return sorted; + } + + /** + * This method uses the map, indicated by the parameter listObjects for filtering the map, indicated by the + * parameter listUIDs. After that the map with UIDs of the objects will correspond to the map with the + * objects. + * + * @param listObjects holds the list with the objects for filter criteria. + * @param listUIDs holds the list with UIDs of the objects for filtering. + * @return filtered list with UIDs of the objects. + */ + static Map filterList(Map listObjects, Map listUIDs) { + Hashtable filtered = new Hashtable(); + for (final Entry entry : listUIDs.entrySet()) { + final String id = entry.getKey(); + final String uid = entry.getValue(); + Object obj = listObjects.get(uid); + if (obj != null) { + filtered.put(id, uid); + } + } + return filtered; + } + + /** + * This method is responsible for the printing of a table with the number and width of the columns, + * as indicated by the parameter of the method columnWidths and the content of the columns, + * indicated by the parameter values. The table has title with rows, indicated by the + * parameter of the method titleRows. + * + * @param columnWidths represents the number and width of the columns of the table. + * @param values contain the rows of the table. + * @param titleRow contain the rows representing the title of the table. + * @return a string representing the table content + */ + static String getTableContent(int width, int[] columnWidths, List values, String titleRow) { + StringBuilder sb = new StringBuilder(); + List tableRows = collectTableRows(width, columnWidths, values, titleRow); + for (String tableRow : tableRows) { + sb.append(tableRow + ROW_END); + } + return sb.toString(); + } + + /** + * This method is responsible for collecting the content of the rows of a table. + * + * @param width represents the table width. + * @param columnWidths represents the number and width of the columns of the table. + * @param values contain the rows of the table. + * @param titleRow contain the title of the table. + * @return a list with strings representing the rows of a table. + */ + static List collectTableRows(int width, int[] columnWidths, List values, String titleRow) { + List tableRows = getTableTitle(titleRow, width); + for (String value : values) { + tableRows.add(value); + } + tableRows.add(getTableBottom(width)); + return tableRows; + } + + /** + * This method is responsible for the printing of a row of a table with the number and width of the columns, + * as indicated by the parameter of the method columnWidths and the content of the columns, indicated by + * the parameter values. + * + * @param columnWidths indicates the number and width of the columns of the table. + * @param values indicates the content of the columns of the table. + * @return a string representing the row of the table. + */ + static String getRow(int[] columnWidths, List values) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < columnWidths.length; i++) { + sb.append(getColumn(columnWidths[i], values.get(i))); + } + return sb.toString(); + } + + /** + * This method is responsible for the printing a title of a table with width specified by the parameter of + * the method - width and content specified by the parameter of the method - titleRows. + * + * @param titleRow specifies the content of the title. + * @param width specifies the width of the table. + * @return a string representing the title of the table. + */ + static List getTableTitle(String titleRow, int width) { + List res = new ArrayList(); + res.add(printChars(TABLE_DELIMITER, width)); + res.add(titleRow); + res.add(printChars(TABLE_DELIMITER, width)); + return res; + } + + /** + * This method is responsible for the printing a bottom of a table with width specified by the parameter of + * the method - width. + * + * @param width specifies the width of the table. + * @return a string representing the bottom of the table. + */ + static String getTableBottom(int width) { + return printChars(TABLE_DELIMITER, width); + } + + /** + * This method is responsible for the printing a Column with width specified by the parameter of + * the method - width and content specified by the parameter of the method - value. + * + * @param width specifies the width of the column. + * @param value specifies the content of the column. + * @return a string representing the column value of the table. + */ + static String getColumn(int width, String value) { + value = value + FILLER; + return value + printChars(FILLER, width - value.length()); + } + + /** + * This method is responsible for the printing a symbol - ch as many times as specified by the parameter of + * the method - count. + * + * @param ch the specified symbol. + * @param count specifies how many times to append the specified symbol. + * @return a string containing the symbol - ch as many times is specified. + */ + static String printChars(char ch, int count) { + if (count < 1) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(ch); + } + return sb.toString(); + } + + /** + * This method sorts lexicographically the strings. + * + * @param strings holds the list with strings for sorting and indexing. + */ + static void quickSort(String[] strings, int begin, int length) { + int i, j, leftLength, rightLength, t; + String x; + while (length >= 3) { + t = length - 1; + j = t + begin; + i = (t >> 1) + begin; + sort3(strings, begin, i, j); + if (length == 3) { + return; + } + x = strings[i]; + i = begin + 1; + j--; + do { + while (strings[i].compareTo(x) < 0) { + i++; + } + while (strings[j].compareTo(x) > 0) { + j--; + } + if (i < j) { + swap(strings, i, j); + } else { + if (i == j) { + i++; + j--; + } + break; + } + } while (++i <= --j); + leftLength = (j - begin) + 1; + rightLength = (begin - i) + length; + if (leftLength < rightLength) { + if (leftLength > 1) { + quickSort(strings, begin, leftLength); + } + begin = i; + length = rightLength; + } else { + if (rightLength > 1) { + quickSort(strings, i, rightLength); + } + length = leftLength; + } + } + if (length == 2 && strings[begin].compareTo(strings[begin + 1]) > 0) { + swap(strings, begin, begin + 1); + } + } + + /** + * Auxiliary method for sorting lexicographically the strings at the positions x, y and z. + * + * @param a represents the array with the strings for sorting. + * @param x position of the first string. + * @param y position of the second string. + * @param z position of the third string. + */ + private static void sort3(String[] a, int x, int y, int z) { + if (a[x].compareTo(a[y]) > 0) { + if (a[x].compareTo(a[z]) > 0) { + if (a[y].compareTo(a[z]) > 0) { + swap(a, x, z); + } else { + swap3(a, x, y, z); + } + } else { + swap(a, x, y); + } + } else if (a[x].compareTo(a[z]) > 0) { + swap3(a, x, z, y); + } else if (a[y].compareTo(a[z]) > 0) { + swap(a, y, z); + } + } + + /** + * Auxiliary method for sorting lexicographically the strings. Shuffling strings on positions x and y, as the string + * at the position x, goes to the position y, the string at the position y, goes to the position x. + * + * @param a represents the array with the strings for sorting. + * @param x position of the first string. + * @param y position of the second string. + */ + private static void swap(String[] a, int x, int y) { + String t = a[x]; + a[x] = a[y]; + a[y] = t; + } + + /** + * Auxiliary method for sorting lexicographically the strings. Shuffling strings on positions x, y and z, as the + * string + * at the position x, goes to the position z, the string at the position y, goes to the position x and the string + * at the position z, goes to the position y. + * + * @param a represents the array with the strings for sorting. + * @param x position of the first string. + * @param y position of the second string. + * @param z position of the third string. + */ + private static void swap3(String[] a, int x, int y, int z) { + String t = a[x]; + a[x] = a[y]; + a[y] = a[z]; + a[z] = t; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/AbstractCompositeModuleHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/AbstractCompositeModuleHandler.java new file mode 100644 index 00000000000..9b20bd79222 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/AbstractCompositeModuleHandler.java @@ -0,0 +1,119 @@ +/** + * 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.internal.composite; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.TriggerHandler; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.TriggerType; +import org.openhab.core.automation.util.ReferenceResolver; + +/** + * This class is base implementation of all system composite module handlers: {@link CompositeTriggerHandler}, + * {@link CompositeConditionHandler} and {@link CompositeActionHandler}. The instances of these handlers are created by + * {@link CompositeModuleHandlerFactory}. + * The composite module handlers have to serve modules of composite module types. These handlers are responsible to + * propagate configuration properties and input values of composite module to the child modules defined by the composite + * module type and to call the handlers which are responsible for the child modules. + * + * + * @author Yordan Mihaylov - Initial Contribution + * + * @param type of module. It can be {@link Trigger}, {@link Condition} or {@link Action} + * @param type of module type. It can be {@link TriggerType}, {@link ConditionType} or {@link ActionType} + * @param type of module handler. It can be {@link TriggerHandler}, {@link ConditionHandler} or + * {@link ActionHandler} + */ +public abstract class AbstractCompositeModuleHandler + implements ModuleHandler { + + protected LinkedHashMap moduleHandlerMap; + protected M module; + protected MT moduleType; + + /** + * This constructor creates composite module handler base on composite module, module type of the module and map of + * pairs of child module instances and corresponding handlers. + * + * @param module module of composite type. + * @param moduleType composite module type. This is the type of module. + * @param mapModuleToHandler map containing pairs of child modules instances (defined by module type) and their + * handlers + */ + public AbstractCompositeModuleHandler(M module, MT moduleType, LinkedHashMap mapModuleToHandler) { + this.module = module; + this.moduleType = moduleType; + this.moduleHandlerMap = mapModuleToHandler; + } + + /** + * Creates internal composite context which will be used for resolving child module's context. + * + * @param context contains composite inputs and composite configuration. + * @return context that will be passed to the child module + */ + protected Map getCompositeContext(Map context) { + Map result = new HashMap(context); + result.putAll(module.getConfiguration().getProperties()); + return result; + } + + /** + * Creates child context that will be passed to the child handler. + * + * @param child Composite ModuleImpl's child + * @param compositeContext context with which child context will be resolved. + * @return child context ready to be passed to the child for execution. + */ + protected Map getChildContext(Module child, Map compositeContext) { + return ReferenceResolver.getCompositeChildContext(child, compositeContext); + } + + @Override + public void dispose() { + List children = getChildren(); + for (M child : children) { + ModuleHandler childHandler = moduleHandlerMap.remove(child); + if (childHandler != null) { + childHandler.dispose(); + } + } + moduleHandlerMap = null; + } + + @Override + public void setCallback(ModuleHandlerCallback callback) { + List children = getChildren(); + for (M child : children) { + H handler = moduleHandlerMap.get(child); + handler.setCallback(callback); + } + } + + protected abstract List getChildren(); + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeActionHandler.java new file mode 100644 index 00000000000..a9455adb769 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeActionHandler.java @@ -0,0 +1,130 @@ +/** + * 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.internal.composite; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringTokenizer; + +import org.openhab.core.automation.Action; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.util.ReferenceResolver; + +/** + * This class is a handler implementation for {@link CompositeActionType}. The action of type + * {@link CompositeActionType} has to execute handlers of all child actions. The handler has to return outputs of the + * action, base on the outputs of the child actions, into rule context. The outputs of the child actions are not + * visible out of the context of the action. + * + * @author Yordan Mihaylov - Initial Contribution + * + */ +public class CompositeActionHandler extends AbstractCompositeModuleHandler + implements ActionHandler { + + public static final String REFERENCE = "reference"; + + private final Map compositeOutputs; + + /** + * Create a system handler for modules of {@link CompositeActionType} type. + * + * @param action parent action module instance. The action which has {@link CompositeActionType} type. + * @param mt {@link CompositeActionType} instance of the parent module + * @param mapModuleToHandler map of pairs child action module to its action handler + * @param ruleUID UID of rule where the parent action is part of. + */ + public CompositeActionHandler(Action action, CompositeActionType mt, + LinkedHashMap mapModuleToHandler, String ruleUID) { + super(action, mt, mapModuleToHandler); + compositeOutputs = getCompositeOutputMap(moduleType.getOutputs()); + } + + /** + * The method calls handlers of child action, collect their outputs and sets the output of the parent action. + * + * @see org.openhab.core.automation.handler.ActionHandler#execute(java.util.Map) + */ + @Override + public Map execute(Map context) { + final Map result = new HashMap(); + final List children = getChildren(); + final Map compositeContext = getCompositeContext(context); + for (Action child : children) { + ActionHandler childHandler = moduleHandlerMap.get(child); + Map childContext = Collections.unmodifiableMap(getChildContext(child, compositeContext)); + Map childResults = childHandler.execute(childContext); + if (childResults != null) { + for (Entry childResult : childResults.entrySet()) { + String childOuputName = child.getId() + "." + childResult.getKey(); + Output output = compositeOutputs.get(childOuputName); + if (output != null) { + String childOuputRef = output.getReference(); + if (childOuputRef != null && childOuputRef.length() > childOuputName.length()) { + childOuputRef = childOuputRef.substring(childOuputName.length()); + result.put(output.getName(), ReferenceResolver + .resolveComplexDataReference(childResult.getValue(), childOuputRef)); + } else { + result.put(output.getName(), childResult.getValue()); + } + } + } + } + + } + return result.size() > 0 ? result : null; + } + + /** + * Create a map of links between child outputs and parent outputs. These links are base on the refecences defined in + * the outputs of parent action. + * + * @param outputs outputs of the parent action. The action of {@link CompositeActionType} + * @return map of links between child action outputs and parent output + */ + protected Map getCompositeOutputMap(List outputs) { + Map result = new HashMap(11); + if (outputs != null) { + for (Output output : outputs) { + String refs = output.getReference(); + if (refs != null) { + String ref; + StringTokenizer st = new StringTokenizer(refs, ","); + while (st.hasMoreTokens()) { + ref = st.nextToken().trim(); + int i = ref.indexOf('.'); + if (i != -1) { + int j = ReferenceResolver.getNextRefToken(ref, i + 1); + if (j != -1) { + ref = ref.substring(0, j); + } + } + result.put(ref, output); + } + } + } + } + return result; + } + + @Override + protected List getChildren() { + return moduleType.getChildren(); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeConditionHandler.java new file mode 100644 index 00000000000..eab9abee5fd --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeConditionHandler.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.internal.composite; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.ConditionHandler; +import org.openhab.core.automation.type.CompositeConditionType; + +/** + * This class is a handler implementation for {@link CompositeConditionType}. The condition which has + * {@link CompositeConditionType} module type will be satisfied only when all child conditions (defined + * by its {@link CompositeConditionType}) are satisfied. + * + * @author Yordan Mihaylov - Initial Contribution + * + */ +public class CompositeConditionHandler + extends AbstractCompositeModuleHandler + implements ConditionHandler { + + public CompositeConditionHandler(Condition condition, CompositeConditionType mt, + LinkedHashMap mapModuleToHandler, String ruleUID) { + super(condition, mt, mapModuleToHandler); + } + + /** + * The method calls handlers of child modules and return true only when they all are satisfied. + * + * @see org.openhab.core.automation.handler.ConditionHandler#isSatisfied(java.util.Map) + */ + @Override + public boolean isSatisfied(Map context) { + List children = getChildren(); + Map compositeContext = getCompositeContext(context); + for (Condition child : children) { + Map childContext = Collections.unmodifiableMap(getChildContext(child, compositeContext)); + ConditionHandler childHandler = moduleHandlerMap.get(child); + boolean isSatisfied = childHandler.isSatisfied(childContext); + if (!isSatisfied) { + return false; + } + } + return true; + } + + @Override + protected List getChildren() { + return moduleType.getChildren(); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java new file mode 100644 index 00000000000..1dfcbb23f9a --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeModuleHandlerFactory.java @@ -0,0 +1,194 @@ +/** + * 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.internal.composite; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.Trigger; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.BaseModuleHandlerFactory; +import org.openhab.core.automation.handler.ConditionHandler; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.ModuleHandlerFactory; +import org.openhab.core.automation.handler.TriggerHandler; +import org.openhab.core.automation.internal.ModuleImpl; +import org.openhab.core.automation.internal.RuleEngineImpl; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeRegistry; +import org.openhab.core.automation.util.ReferenceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is a factory for system module handler for modules of composite module types: {@link CompositeTriggerType} + * , {@link CompositeConditionType} and {@link CompositeActionType}. The composite module type is a type which contains + * one or more internal (child) modules and these modules have access to configuration properties and inputs of + * composite module. The outputs of module of composite type (if they exists) are set these handlers and they are base + * on the values of child module outputs. + * The {@link CompositeModuleHandlerFactory} is a system handler factory and it is not registered as service in OSGi + * framework, but it will be used by the rule engine to serve composite module types without any action of the user. + * + * + * @author Yordan Mihaylov - Initial Contribution + */ +public class CompositeModuleHandlerFactory extends BaseModuleHandlerFactory implements ModuleHandlerFactory { + + private final ModuleTypeRegistry mtRegistry; + private final RuleEngineImpl ruleEngine; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * The constructor of system handler factory for composite module types. + * + * @param context is a bundle context + * @param mtManager is a module type manager + * @param re is a rule engine + */ + public CompositeModuleHandlerFactory(ModuleTypeRegistry mtRegistry, RuleEngineImpl re) { + this.mtRegistry = mtRegistry; + this.ruleEngine = re; + } + + @Override + public void deactivate() { + super.deactivate(); + } + + /** + * It is system factory and must not be registered as service. This method is not used. + * + * @see org.openhab.core.automation.handler.ModuleHandlerFactory#getTypes() + */ + @Override + public Collection getTypes() { + return new ArrayList<>(); + } + + @SuppressWarnings({ "unchecked" }) + @Override + public void ungetHandler(Module module, String childModulePrefix, ModuleHandler handler) { + ModuleHandler handlerOfModule = getHandlers().get(childModulePrefix + module.getId()); + if (handlerOfModule instanceof AbstractCompositeModuleHandler) { + AbstractCompositeModuleHandler h = (AbstractCompositeModuleHandler) handlerOfModule; + Set modules = h.moduleHandlerMap.keySet(); + if (modules != null) { + for (ModuleImpl child : modules) { + ModuleHandler childHandler = h.moduleHandlerMap.get(child); + ModuleHandlerFactory mhf = ruleEngine.getModuleHandlerFactory(child.getTypeUID()); + mhf.ungetHandler(child, childModulePrefix + ":" + module.getId(), childHandler); + } + } + } + String ruleId = getRuleId(childModulePrefix); + super.ungetHandler(module, ruleId, handler); + } + + private String getRuleId(String childModulePrefix) { + int i = childModulePrefix.indexOf(':'); + String ruleId = i != -1 ? childModulePrefix.substring(0, i) : childModulePrefix; + return ruleId; + } + + @Override + public ModuleHandler internalCreate(Module module, String ruleUID) { + ModuleHandler handler = null; + String moduleType = module.getTypeUID(); + ModuleType mt = mtRegistry.get(moduleType); + if (mt instanceof CompositeTriggerType) { + List childModules = ((CompositeTriggerType) mt).getChildren(); + LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(), + module.getConfiguration(), childModules, ruleUID); + if (mapModuleToHandler != null) { + handler = new CompositeTriggerHandler((Trigger) module, (CompositeTriggerType) mt, mapModuleToHandler, + ruleUID); + } + } else if (mt instanceof CompositeConditionType) { + List childModules = ((CompositeConditionType) mt).getChildren(); + LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(), + module.getConfiguration(), childModules, ruleUID); + if (mapModuleToHandler != null) { + handler = new CompositeConditionHandler((Condition) module, (CompositeConditionType) mt, + mapModuleToHandler, ruleUID); + } + } else if (mt instanceof CompositeActionType) { + List childModules = ((CompositeActionType) mt).getChildren(); + LinkedHashMap mapModuleToHandler = getChildHandlers(module.getId(), + module.getConfiguration(), childModules, ruleUID); + if (mapModuleToHandler != null) { + handler = new CompositeActionHandler((Action) module, (CompositeActionType) mt, mapModuleToHandler, + ruleUID); + } + } + if (handler != null) { + logger.debug("Set module handler: {} -> {} of rule {}.", module.getId(), + handler.getClass().getSimpleName() + "(" + moduleType + ")", ruleUID); + } else { + logger.debug("Not found module handler {} for moduleType {} of rule {}.", module.getId(), moduleType, + ruleUID); + } + return handler; + } + + /** + * This method associates module handlers to the child modules of composite module types. It links module types of + * child modules to the rule which contains this composite module. It also resolve links between child configuration + * properties and configuration of composite module see: + * {@link ReferenceResolver#updateConfiguration(Configuration, Map, Logger)}. + * + * @param compositeConfig configuration values of composite module. + * @param childModules list of child modules + * @param childModulePrefix defines UID of child module. The rule id is not enough for prefix when a composite type + * is used more then one time in one and the same rule. For example the prefix can be: + * ruleId:compositeModuleId:compositeModileId2. + * @return map of pairs of module and its handler. Return null when some of the child modules can not find its + * handler. + */ + @SuppressWarnings("unchecked") + private LinkedHashMap getChildHandlers(String compositeModuleId, + Configuration compositeConfig, List childModules, String childModulePrefix) { + LinkedHashMap mapModuleToHandler = new LinkedHashMap(); + for (T child : childModules) { + String ruleId = getRuleId(childModulePrefix); + ruleEngine.updateMapModuleTypeToRule(ruleId, child.getTypeUID()); + ModuleHandlerFactory childMhf = ruleEngine.getModuleHandlerFactory(child.getTypeUID()); + if (childMhf == null) { + mapModuleToHandler.clear(); + mapModuleToHandler = null; + return null; + } + ReferenceResolver.updateConfiguration(child.getConfiguration(), compositeConfig.getProperties(), logger); + MT childHandler = (MT) childMhf.getHandler(child, childModulePrefix + ":" + compositeModuleId); + + if (childHandler == null) { + mapModuleToHandler.clear(); + mapModuleToHandler = null; + return null; + } + mapModuleToHandler.put(child, childHandler); + } + return mapModuleToHandler; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeTriggerHandler.java new file mode 100644 index 00000000000..c52b64f2889 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/composite/CompositeTriggerHandler.java @@ -0,0 +1,169 @@ +/** + * 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.internal.composite; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.RuleStatus; +import org.openhab.core.automation.RuleStatusInfo; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.TriggerHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.automation.type.CompositeTriggerType; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.util.ReferenceResolver; + +/** + * This class is a handler implementation for {@link CompositeTriggerType}. The trigger which has + * {@link CompositeTriggerType} has to be notified by the handlers of child triggers and it will be triggered when some + * of them is triggered. The handler has to put outputs of the trigger, base on the outputs of the child triggers, into + * rule context. The outputs of the child triggers are not visible out of context of the trigger. + * + * @author Yordan Mihaylov - Initial Contribution + * + */ +public class CompositeTriggerHandler + extends AbstractCompositeModuleHandler + implements TriggerHandler, TriggerHandlerCallback { + + private TriggerHandlerCallback callback; + + /** + * Constructor of this system handler. + * + * @param trigger trigger of composite type (parent trigger). + * @param mt module type of parent trigger + * @param mapModuleToHandler map of pairs child triggers to their handlers + * @param ruleUID UID of rule where the parent trigger is part of + */ + public CompositeTriggerHandler(Trigger trigger, CompositeTriggerType mt, + LinkedHashMap mapModuleToHandler, String ruleUID) { + super(trigger, mt, mapModuleToHandler); + } + + /** + * This method is called by the child triggers defined by the {@link CompositeTriggerType} of parent trigger. + * The method goes through the outputs of the parent trigger and fill them base on the ouput's reference value. + * The ouput's reference value can contain more then one references to the child outputs separated by comma. In this + * case the method will try to fill the output value in sequence defined in the reference value. The letter + * reference can be overwritten by the previous ones. + * + * @see org.openhab.core.automation.handler.TriggerHandlerCallback#triggered(org.openhab.core.automation.Trigger, + * java.util.Map) + */ + @Override + public void triggered(Trigger trigger, Map context) { + if (callback != null) { + List outputs = moduleType.getOutputs(); + Map result = new HashMap(11); + for (Output output : outputs) { + String refs = output.getReference(); + if (refs != null) { + String ref; + StringTokenizer st = new StringTokenizer(refs, ","); + while (st.hasMoreTokens()) { + ref = st.nextToken().trim(); + int i = ref.indexOf('.'); + if (i != -1) { + String childModuleId = ref.substring(0, i); + if (trigger.getId().equals(childModuleId)) { + ref = ref.substring(i + 1); + } + } + Object value = null; + int idx = ReferenceResolver.getNextRefToken(ref, 1); + if (idx < ref.length()) { + String outputId = ref.substring(0, idx); + value = ReferenceResolver.resolveComplexDataReference(context.get(outputId), + ref.substring(idx + 1)); + } else { + value = context.get(ref); + } + if (value != null) { + result.put(output.getName(), value); + } + } + } + } + callback.triggered(module, result); + } + } + + /** + * The {@link CompositeTriggerHandler} sets itself as callback to the child triggers and store the callback to the + * rule engine. In this way the trigger of composite type will be notified always when some of the child triggers + * are triggered and has an opportunity to set the outputs of parent trigger to the rule context. + * + * @see org.openhab.core.automation.handler.TriggerHandler#setTriggerHandlerCallback(org.openhab.core.automation.handler.TriggerHandlerCallback) + */ + @Override + public void setCallback(@Nullable ModuleHandlerCallback callback) { + this.callback = (TriggerHandlerCallback) callback; + if (callback instanceof TriggerHandlerCallback) {// could be called with 'null' from dispose and might not be a + // trigger callback + List children = getChildren(); + for (Trigger child : children) { + TriggerHandler handler = moduleHandlerMap.get(child); + handler.setCallback(this); + } + } + } + + @Override + public void dispose() { + setCallback(null); + super.dispose(); + } + + @Override + protected List getChildren() { + return moduleType.getChildren(); + } + + @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/src/main/java/org/openhab/core/automation/internal/module/exception/UncomparableException.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/exception/UncomparableException.java new file mode 100644 index 00000000000..a5790a51910 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/exception/UncomparableException.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.internal.module.exception; + +/** + * This Exception is used as an indicator for not matching types during comparation + * + * @author Benedikt Niehues - Initial contribution and API + * + */ +public class UncomparableException extends Exception { + + private static final long serialVersionUID = 4891205711357448390L; + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java new file mode 100644 index 00000000000..e446f647ea3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/factory/CoreModuleHandlerFactory.java @@ -0,0 +1,194 @@ +/** + * 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.internal.module.factory; + +import java.util.Arrays; +import java.util.Collection; + +import org.eclipse.smarthome.core.events.EventPublisher; +import org.eclipse.smarthome.core.items.ItemRegistry; +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.handler.ModuleHandlerFactory; +import org.openhab.core.automation.internal.module.handler.ChannelEventTriggerHandler; +import org.openhab.core.automation.internal.module.handler.CompareConditionHandler; +import org.openhab.core.automation.internal.module.handler.GenericEventConditionHandler; +import org.openhab.core.automation.internal.module.handler.GenericEventTriggerHandler; +import org.openhab.core.automation.internal.module.handler.ItemCommandActionHandler; +import org.openhab.core.automation.internal.module.handler.ItemCommandTriggerHandler; +import org.openhab.core.automation.internal.module.handler.ItemStateConditionHandler; +import org.openhab.core.automation.internal.module.handler.ItemStateTriggerHandler; +import org.openhab.core.automation.internal.module.handler.RuleEnablementActionHandler; +import org.openhab.core.automation.internal.module.handler.RunRuleActionHandler; +import org.osgi.framework.BundleContext; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This HandlerFactory creates ModuleHandlers to control items within the + * RuleManager. It contains basic Triggers, Conditions and Actions. + * + * @author Benedikt Niehues - Initial contribution and API + * @author Kai Kreuzer - refactored and simplified customized module handling + * + */ +@Component +public class CoreModuleHandlerFactory extends BaseModuleHandlerFactory implements ModuleHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(CoreModuleHandlerFactory.class); + + private static final Collection TYPES = Arrays.asList(ItemCommandTriggerHandler.MODULE_TYPE_ID, + ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID, ItemStateTriggerHandler.CHANGE_MODULE_TYPE_ID, + ItemStateConditionHandler.ITEM_STATE_CONDITION, ItemCommandActionHandler.ITEM_COMMAND_ACTION, + GenericEventTriggerHandler.MODULE_TYPE_ID, ChannelEventTriggerHandler.MODULE_TYPE_ID, + GenericEventConditionHandler.MODULETYPE_ID, GenericEventConditionHandler.MODULETYPE_ID, + CompareConditionHandler.MODULE_TYPE, RuleEnablementActionHandler.UID, RunRuleActionHandler.UID); + + private ItemRegistry itemRegistry; + private EventPublisher eventPublisher; + + private BundleContext bundleContext; + + @Activate + protected void activate(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public Collection getTypes() { + return TYPES; + } + + /** + * the itemRegistry was added (called by serviceTracker) + * + * @param itemRegistry + */ + @Reference + protected void setItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + for (ModuleHandler handler : getHandlers().values()) { + if (handler instanceof ItemStateConditionHandler) { + ((ItemStateConditionHandler) handler).setItemRegistry(this.itemRegistry); + } else if (handler instanceof ItemCommandActionHandler) { + ((ItemCommandActionHandler) handler).setItemRegistry(this.itemRegistry); + } + } + } + + /** + * unsetter for itemRegistry (called by serviceTracker) + * + * @param itemRegistry + */ + protected void unsetItemRegistry(ItemRegistry itemRegistry) { + for (ModuleHandler handler : getHandlers().values()) { + if (handler instanceof ItemStateConditionHandler) { + ((ItemStateConditionHandler) handler).unsetItemRegistry(this.itemRegistry); + } else if (handler instanceof ItemCommandActionHandler) { + ((ItemCommandActionHandler) handler).unsetItemRegistry(this.itemRegistry); + } + } + this.itemRegistry = null; + } + + /** + * setter for the eventPublisher (called by serviceTracker) + * + * @param eventPublisher + */ + @Reference + protected void setEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + for (ModuleHandler handler : getHandlers().values()) { + if (handler instanceof ItemCommandActionHandler) { + ((ItemCommandActionHandler) handler).setEventPublisher(eventPublisher); + } + } + } + + /** + * unsetter for eventPublisher (called by serviceTracker) + * + * @param eventPublisher + */ + protected void unsetEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = null; + for (ModuleHandler handler : getHandlers().values()) { + if (handler instanceof ItemCommandActionHandler) { + ((ItemCommandActionHandler) handler).unsetEventPublisher(eventPublisher); + } + } + } + + @Override + protected synchronized ModuleHandler internalCreate(final Module module, final String ruleUID) { + logger.trace("create {} -> {} : {}", module.getId(), module.getTypeUID(), ruleUID); + final String moduleTypeUID = module.getTypeUID(); + if (module instanceof Trigger) { + // Handle triggers + + if (GenericEventTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) { + return new GenericEventTriggerHandler((Trigger) module, bundleContext); + } else if (ChannelEventTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) { + return new ChannelEventTriggerHandler((Trigger) module, bundleContext); + } else if (ItemCommandTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID)) { + return new ItemCommandTriggerHandler((Trigger) module, bundleContext); + } else if (ItemStateTriggerHandler.CHANGE_MODULE_TYPE_ID.equals(moduleTypeUID) + || ItemStateTriggerHandler.UPDATE_MODULE_TYPE_ID.equals(moduleTypeUID)) { + return new ItemStateTriggerHandler((Trigger) module, bundleContext); + } + } else if (module instanceof Condition) { + // Handle conditions + if (ItemStateConditionHandler.ITEM_STATE_CONDITION.equals(moduleTypeUID)) { + ItemStateConditionHandler handler = new ItemStateConditionHandler((Condition) module); + handler.setItemRegistry(itemRegistry); + return handler; + } else if (GenericEventConditionHandler.MODULETYPE_ID.equals(moduleTypeUID)) { + return new GenericEventConditionHandler((Condition) module); + } else if (CompareConditionHandler.MODULE_TYPE.equals(moduleTypeUID)) { + return new CompareConditionHandler((Condition) module); + } + } else if (module instanceof Action) { + // Handle actions + + if (ItemCommandActionHandler.ITEM_COMMAND_ACTION.equals(moduleTypeUID)) { + final ItemCommandActionHandler postCommandActionHandler = new ItemCommandActionHandler((Action) module); + postCommandActionHandler.setEventPublisher(eventPublisher); + postCommandActionHandler.setItemRegistry(itemRegistry); + return postCommandActionHandler; + } else if (RuleEnablementActionHandler.UID.equals(moduleTypeUID)) { + return new RuleEnablementActionHandler((Action) module); + } else if (RunRuleActionHandler.UID.equals(moduleTypeUID)) { + return new RunRuleActionHandler((Action) module); + } + } + + logger.error("The ModuleHandler is not supported:{}", moduleTypeUID); + return null; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java new file mode 100644 index 00000000000..9e4251a0423 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java @@ -0,0 +1,142 @@ +/** + * 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.internal.module.handler; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.openhab.core.automation.Action; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.automation.type.Output; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ActionHandler which is dynamically created upon annotation on services + * + * @author Stefan Triller - initial contribution + * + */ +public class AnnotationActionHandler extends BaseModuleHandler implements ActionHandler { + + private static final String MODULE_RESULT = "result"; + + private final Logger logger = LoggerFactory.getLogger(AnnotationActionHandler.class); + + private final Method method; + private final ActionType moduleType; + private final Object actionProvider; + + public AnnotationActionHandler(Action module, ActionType mt, Method method, Object actionProvider) { + super(module); + + this.method = method; + this.moduleType = mt; + this.actionProvider = actionProvider; + } + + @Override + public Map execute(Map context) { + Map output = new HashMap<>(); + + Annotation[][] annotations = method.getParameterAnnotations(); + List args = new ArrayList<>(); + + for (int i = 0; i < annotations.length; i++) { + Annotation[] annotationsOnParam = annotations[i]; + if (annotationsOnParam != null && annotationsOnParam.length == 1) { + if (annotationsOnParam[0] instanceof ActionInput) { + ActionInput inputAnnotation = (ActionInput) annotationsOnParam[0]; + // check if the moduleType has a configdescription with this input + if (hasInput(moduleType, inputAnnotation.name())) { + args.add(i, context.get(inputAnnotation.name())); + } else { + logger.error( + "Annotated method defines input '{}' but the module type '{}' does not specify an input with this name.", + inputAnnotation.name(), moduleType); + return output; + } + } + } else { + // no annotation on parameter, try to fetch the generic parameter from the context + args.add(i, context.get("p" + i)); + } + } + + Object result = null; + try { + result = method.invoke(this.actionProvider, args.toArray()); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + logger.error("Could not call method '{}' from module type '{}'.", method, moduleType.getUID(), e); + } + if (result != null) { + if (result instanceof Map) { + try { + Map resultMap = (Map) result; + for (Entry entry : resultMap.entrySet()) { + if (hasOutput(moduleType, entry.getKey())) { + output.put(entry.getKey(), entry.getValue()); + } + } + } catch (ClassCastException ex) { + logger.error( + "The return type of action method '{}' from module type '{}' should be Map, because {}", + method, moduleType.getUID(), ex.getMessage()); + } + // we allow simple data types as return values and put them under the context key "result". + } else if (result instanceof Boolean) { + output.put(MODULE_RESULT, (boolean) result); + } else if (result instanceof String) { + output.put(MODULE_RESULT, result); + } else if (result instanceof Integer) { + output.put(MODULE_RESULT, result); + } else if (result instanceof Double) { + output.put(MODULE_RESULT, (double) result); + } else if (result instanceof Float) { + output.put(MODULE_RESULT, (float) result); + } else { + logger.warn("Non compatible return type '{}' on action method.", result.getClass()); + } + } + + return output; + } + + private boolean hasInput(ActionType moduleType, String in) { + for (Input i : moduleType.getInputs()) { + if (i.getName().equals(in)) { + return true; + } + } + return false; + } + + private boolean hasOutput(ActionType moduleType, String out) { + for (Output o : moduleType.getOutputs()) { + if (o.getName().equals(out)) { + return true; + } + } + return false; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java new file mode 100644 index 00000000000..ea56ec7bc9d --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ChannelEventTriggerHandler.java @@ -0,0 +1,125 @@ +/** + * 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.internal.module.handler; + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventFilter; +import org.eclipse.smarthome.core.events.EventSubscriber; +import org.eclipse.smarthome.core.thing.events.ChannelTriggeredEvent; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for trigger channels with specific events + * + * @author Stefan Triller - Initial contribution + * + */ +public class ChannelEventTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber, EventFilter { + + private final Logger logger = LoggerFactory.getLogger(ChannelEventTriggerHandler.class); + + public static final String MODULE_TYPE_ID = "core.ChannelEventTrigger"; + + private final String eventOnChannel; + private final String channelUID; + private final String TOPIC = "smarthome/channels/*/triggered"; + private final Set types = new HashSet(); + private final BundleContext bundleContext; + + private final String CFG_CHANNEL_EVENT = "event"; + private final String CFG_CHANNEL = "channelUID"; + + @SuppressWarnings("rawtypes") + private ServiceRegistration eventSubscriberRegistration; + + public ChannelEventTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module); + + this.eventOnChannel = (String) module.getConfiguration().get(CFG_CHANNEL_EVENT); + this.channelUID = (String) module.getConfiguration().get(CFG_CHANNEL); + this.bundleContext = bundleContext; + this.types.add("ChannelTriggeredEvent"); + + Dictionary properties = new Hashtable(); + properties.put("event.topics", TOPIC); + eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, + properties); + } + + @Override + public void receive(Event event) { + if (callback != null) { + logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), + event.getTopic(), event.getType(), event.getPayload()); + + Map values = new HashMap<>(); + values.put("event", event); + + ((TriggerHandlerCallback) callback).triggered(this.module, values); + } + } + + @Override + public boolean apply(Event event) { + logger.trace("->FILTER: {}:{}", event.getTopic(), TOPIC); + + boolean eventMatches = false; + if (event instanceof ChannelTriggeredEvent) { + ChannelTriggeredEvent cte = (ChannelTriggeredEvent) event; + if (cte.getTopic().contains(this.channelUID)) { + logger.trace("->FILTER: {}:{}", cte.getEvent(), eventOnChannel); + eventMatches = true; + if (eventOnChannel != null && !eventOnChannel.isEmpty() && !eventOnChannel.equals(cte.getEvent())) { + eventMatches = false; + } + } + } + return eventMatches; + } + + @Override + public EventFilter getEventFilter() { + return this; + } + + @Override + public Set getSubscribedEventTypes() { + return types; + } + + /** + * do the cleanup: unregistering eventSubscriber... + */ + @Override + public void dispose() { + super.dispose(); + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + eventSubscriberRegistration = null; + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java new file mode 100644 index 00000000000..79470c94d84 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/CompareConditionHandler.java @@ -0,0 +1,195 @@ +/** + * 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.internal.module.handler; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.TypeParser; +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.internal.module.exception.UncomparableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Generic Comparation Condition + * + * @author Benedikt Niehues - Initial contribution and API + * + */ +public class CompareConditionHandler extends BaseModuleHandler implements ConditionHandler { + + public final Logger logger = LoggerFactory.getLogger(CompareConditionHandler.class); + + public static final String MODULE_TYPE = "core.GenericCompareCondition"; + + public static final String INPUT_LEFT_OBJECT = "input"; + public static final String INPUT_LEFT_FIELD = "inputproperty"; + public static final String RIGHT_OP = "right"; + public static final String OPERATOR = "operator"; + + public CompareConditionHandler(Condition module) { + super(module); + } + + @Override + public boolean isSatisfied(Map context) { + Object operatorObj = this.module.getConfiguration().get(OPERATOR); + String operator = (operatorObj != null && operatorObj instanceof String) ? (String) operatorObj : null; + Object rightObj = this.module.getConfiguration().get(RIGHT_OP); + String rightOperandString = (rightObj != null && rightObj instanceof String) ? (String) rightObj : null; + Object leftObjFieldNameObj = this.module.getConfiguration().get(INPUT_LEFT_FIELD); + String leftObjectFieldName = (leftObjFieldNameObj != null && leftObjFieldNameObj instanceof String) + ? (String) leftObjFieldNameObj : null; + if (rightOperandString == null || operator == null) { + return false; + } else { + Object leftObj = context.get(INPUT_LEFT_OBJECT); + Object toCompare = getCompareValue(leftObj, leftObjectFieldName); + Object rightValue = getRightOperandValue(rightOperandString, toCompare); + if (rightValue == null) { + if (leftObj != null) { + logger.info("unsupported type for compare condition: {}", leftObj.getClass()); + } else { + logger.info("unsupported type for compare condition: null ({})", + module.getInputs().get(INPUT_LEFT_FIELD)); + } + return false; + } + try { + switch (operator) { + case "eq": + case "EQ": + case "=": + case "==": + case "equals": + case "EQUALS": + // EQUALS + if (toCompare == null) { + if (rightOperandString.equals("null") || rightOperandString.equals("")) { + return true; + } else { + return false; + } + } else { + return toCompare.equals(rightValue); + } + case "gt": + case "GT": + case ">": + // Greater + if (toCompare == null || rightValue == null) { + return false; + } else { + return compare(toCompare, rightValue) > 0; + } + case "gte": + case "GTE": + case ">=": + case "=>": + // Greater or equal + if (toCompare == null || rightValue == null) { + return false; + } else { + return compare(toCompare, rightValue) >= 0; + } + case "lt": + case "LT": + case "<": + if (toCompare == null || rightValue == null) { + return false; + } else { + return compare(toCompare, rightValue) < 0; + } + case "lte": + case "LTE": + case "<=": + case "=<": + if (toCompare == null || rightValue == null) { + return false; + } else { + return compare(toCompare, rightValue) <= 0; + } + case "matches": + // Matcher... + if (toCompare instanceof String && rightValue != null && rightValue instanceof String) { + return ((String) toCompare).matches((String) rightValue); + } + default: + break; + } + } catch (UncomparableException e) { + // values can not be compared, so assume that the condition is not satisfied + return false; + } + + return false; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private int compare(Object a, Object b) throws UncomparableException { + if (Comparable.class.isAssignableFrom(a.getClass()) && a.getClass().equals(b.getClass())) { + try { + return ((Comparable) a).compareTo(b); + } catch (ClassCastException e) { + // should never happen but to be save here! + throw new UncomparableException(); + } + } + throw new UncomparableException(); + } + + private Object getRightOperandValue(String rightOperandString2, Object toCompare) { + if (rightOperandString2.equals("null")) { + return rightOperandString2; + } + if (toCompare instanceof State) { + List> stateTypeList = new ArrayList>(); + stateTypeList.add(((State) toCompare).getClass()); + return TypeParser.parseState(stateTypeList, rightOperandString2); + } else if (toCompare instanceof Integer) { + return Integer.parseInt(rightOperandString2); + } else if (toCompare instanceof String) { + return rightOperandString2; + } else if (toCompare instanceof Long) { + return Long.parseLong(rightOperandString2); + } else if (toCompare instanceof Double) { + return Double.parseDouble(rightOperandString2); + } + return null; + } + + private Object getCompareValue(Object leftObj, String leftObjFieldName) { + if (leftObj == null || leftObjFieldName == null || leftObjFieldName.isEmpty() || leftObj instanceof String + || leftObj instanceof Integer || leftObj instanceof Long || leftObj instanceof Double) { + return leftObj; + } else { + try { + Method m = leftObj.getClass().getMethod( + "get" + leftObjFieldName.substring(0, 1).toUpperCase() + leftObjFieldName.substring(1)); + return m.invoke(leftObj); + } catch (NoSuchMethodException | SecurityException | StringIndexOutOfBoundsException + | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return null; + } + } + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DayOfWeekConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DayOfWeekConditionHandler.java new file mode 100644 index 00000000000..aacc2e4f224 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DayOfWeekConditionHandler.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.internal.module.handler; + +import java.util.Calendar; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a ConditionHandler implementation, which checks the current day of the week against a specified list. + * + * @author Kai Kreuzer - Initial Contribution + * + */ +public class DayOfWeekConditionHandler extends BaseModuleHandler implements ConditionHandler { + + private final Logger logger = LoggerFactory.getLogger(DayOfWeekConditionHandler.class); + + public static final String MODULE_TYPE_ID = "timer.DayOfWeekCondition"; + public static final String MODULE_CONTEXT_NAME = "MODULE"; + + private static final String CFG_DAYS = "days"; + + private final Set days; + + @SuppressWarnings("unchecked") + public DayOfWeekConditionHandler(Condition module) { + super(module); + try { + days = new HashSet<>(); + for (String day : (Iterable) module.getConfiguration().get(CFG_DAYS)) { + switch (day.toUpperCase()) { + case "SUN": + days.add(Calendar.SUNDAY); + break; + case "MON": + days.add(Calendar.MONDAY); + break; + case "TUE": + days.add(Calendar.TUESDAY); + break; + case "WED": + days.add(Calendar.WEDNESDAY); + break; + case "THU": + days.add(Calendar.THURSDAY); + break; + case "FRI": + days.add(Calendar.FRIDAY); + break; + case "SAT": + days.add(Calendar.SATURDAY); + break; + default: + logger.warn("Ignoring illegal weekday '{}'", day); + break; + } + } + } catch (RuntimeException e) { + throw new IllegalArgumentException("'days' parameter must be an array of strings."); + } + } + + @Override + public boolean isSatisfied(Map context) { + int dow = Calendar.getInstance().get(Calendar.DAY_OF_WEEK); + return days.contains(dow); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java new file mode 100644 index 00000000000..3e0b4cf12ab --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericCronTriggerHandler.java @@ -0,0 +1,78 @@ +/** + * 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.internal.module.handler; + +import org.eclipse.smarthome.core.scheduler.CronScheduler; +import org.eclipse.smarthome.core.scheduler.SchedulerRunnable; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.eclipse.smarthome.core.scheduler.ScheduledCompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for Triggers which trigger the rule + * based on a cron expression. The cron expression can be set with the + * configuration. + * + * @author Christoph Knauf - Initial Contribution + * @author Yordan Mihaylov - Remove Quarz lib dependency + * + */ +public class GenericCronTriggerHandler extends BaseTriggerModuleHandler implements SchedulerRunnable { + + private final Logger logger = LoggerFactory.getLogger(GenericCronTriggerHandler.class); + + public static final String MODULE_TYPE_ID = "timer.GenericCronTrigger"; + public static final String CALLBACK_CONTEXT_NAME = "CALLBACK"; + public static final String MODULE_CONTEXT_NAME = "MODULE"; + + private static final String CFG_CRON_EXPRESSION = "cronExpression"; + private final CronScheduler scheduler; + private final String expression; + private ScheduledCompletableFuture schedule; + + public GenericCronTriggerHandler(Trigger module, CronScheduler scheduler) { + super(module); + this.scheduler = scheduler; + this.expression = (String) module.getConfiguration().get(CFG_CRON_EXPRESSION); + } + + @Override + public synchronized void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + scheduleJob(); + } + + private void scheduleJob() { + schedule = scheduler.schedule(this, expression); + logger.debug("Scheduled cron job '{}' for trigger '{}'.", module.getConfiguration().get(CFG_CRON_EXPRESSION), + module.getId()); + } + + @Override + public synchronized void dispose() { + super.dispose(); + if (schedule != null) { + schedule.cancel(true); + logger.debug("cancelled job for trigger '{}'.", module.getId()); + } + } + + @Override + public void run() { + ((TriggerHandlerCallback) callback).triggered(module, null); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventConditionHandler.java new file mode 100644 index 00000000000..c71cf937884 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventConditionHandler.java @@ -0,0 +1,76 @@ +/** + * 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.internal.module.handler; + +import java.util.Map; + +import org.eclipse.smarthome.core.events.Event; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the implementation of a event condition which checks if inputs matches configured values. + * + * @author Benedikt Niehues - Initial contribution and API + * @author Kai Kreuzer - refactored and simplified customized module handling + * + */ +public class GenericEventConditionHandler extends BaseModuleHandler implements ConditionHandler { + public final Logger logger = LoggerFactory.getLogger(GenericEventConditionHandler.class); + + public static final String MODULETYPE_ID = "core.GenericEventCondition"; + + private static final String TOPIC = "topic"; + private static final String EVENTTYPE = "eventType"; + private static final String SOURCE = "source"; + private static final String PAYLOAD = "payload"; + + public GenericEventConditionHandler(Condition module) { + super(module); + } + + private boolean isConfiguredAndMatches(String keyParam, String value) { + Object mo = module.getConfiguration().get(keyParam); + String configValue = mo != null && mo instanceof String ? (String) mo : null; + if (configValue != null) { + if (keyParam.equals(PAYLOAD)) { + // automatically adding wildcards only for payload matching + configValue = configValue.startsWith("*") ? configValue : ".*" + configValue; + configValue = configValue.endsWith("*") ? configValue : configValue + ".*"; + } + if (value != null) { + return value.matches(configValue); + } else { + return false; + } + } else { + return true; + } + } + + @Override + public boolean isSatisfied(Map inputs) { + Event event = inputs.get("event") != null ? (Event) inputs.get("event") : null; + if (event != null) { + return isConfiguredAndMatches(TOPIC, event.getTopic()) && isConfiguredAndMatches(SOURCE, event.getSource()) + && isConfiguredAndMatches(PAYLOAD, event.getPayload()) + && isConfiguredAndMatches(EVENTTYPE, event.getType()); + } + return false; + + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventTriggerHandler.java new file mode 100644 index 00000000000..31acb578273 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/GenericEventTriggerHandler.java @@ -0,0 +1,142 @@ +/** + * 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.internal.module.handler; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventFilter; +import org.eclipse.smarthome.core.events.EventSubscriber; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for Triggers which trigger the rule + * if an event occurs. The eventType, eventSource and topic can be set with the + * configuration. It is an generic approach which makes it easier to specify + * more concrete event based triggers with the composite module approach of the + * automation component. Each GenericTriggerHandler instance registers as + * EventSubscriber, so the dispose method must be called for unregistering the + * service. + * + * @author Benedikt Niehues - Initial contribution and API + * @author Kai Kreuzer - refactored and simplified customized module handling + * + */ +public class GenericEventTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber, EventFilter { + + private final Logger logger = LoggerFactory.getLogger(GenericEventTriggerHandler.class); + + private final String source; + private String topic; + private final Set types; + private final BundleContext bundleContext; + + public static final String MODULE_TYPE_ID = "core.GenericEventTrigger"; + + private static final String CFG_EVENT_TOPIC = "eventTopic"; + private static final String CFG_EVENT_SOURCE = "eventSource"; + private static final String CFG_EVENT_TYPES = "eventTypes"; + + @SuppressWarnings("rawtypes") + private ServiceRegistration eventSubscriberRegistration; + + public GenericEventTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module); + this.source = (String) module.getConfiguration().get(CFG_EVENT_SOURCE); + this.topic = (String) module.getConfiguration().get(CFG_EVENT_TOPIC); + if (module.getConfiguration().get(CFG_EVENT_TYPES) != null) { + this.types = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(((String) module.getConfiguration().get(CFG_EVENT_TYPES)).split(",")))); + } else { + this.types = Collections.emptySet(); + } + this.bundleContext = bundleContext; + Dictionary properties = new Hashtable(); + properties.put("event.topics", topic); + eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, + properties); + logger.trace("Registered EventSubscriber: Topic: {} Type: {} Source: {}", topic, types, source); + } + + @Override + public Set getSubscribedEventTypes() { + return types; + } + + @Override + public EventFilter getEventFilter() { + return this; + } + + @Override + public void receive(Event event) { + if (callback != null) { + logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), + event.getTopic(), event.getType(), event.getPayload()); + if (!event.getTopic().contains(source)) { + return; + } + Map values = new HashMap<>(); + values.put("event", event); + + ((TriggerHandlerCallback) callback).triggered(this.module, values); + } + } + + /** + * @return the topic + */ + public String getTopic() { + return topic; + } + + /** + * @param topic + * the topic to set + */ + public void setTopic(String topic) { + this.topic = topic; + } + + /** + * do the cleanup: unregistering eventSubscriber... + */ + @Override + public void dispose() { + super.dispose(); + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + eventSubscriberRegistration = null; + } + } + + @Override + public boolean apply(Event event) { + logger.trace("->FILTER: {}:{}", event.getTopic(), source); + return event.getTopic().contains(source); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java new file mode 100644 index 00000000000..070d4eb79eb --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandActionHandler.java @@ -0,0 +1,140 @@ +/** + * 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.internal.module.handler; + +import java.util.Map; + +import org.eclipse.smarthome.core.events.EventPublisher; +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.ItemCommandEvent; +import org.eclipse.smarthome.core.items.events.ItemEventFactory; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.TypeParser; +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 implementation of an ActionHandler. It sends commands for items. + * + * @author Benedikt Niehues - Initial contribution and API + * @author Kai Kreuzer - refactored and simplified customized module handling + * @author Stefan Triller - use command from input first and if not set, use command from configuration + * + */ +public class ItemCommandActionHandler extends BaseModuleHandler implements ActionHandler { + + private final Logger logger = LoggerFactory.getLogger(ItemCommandActionHandler.class); + + public static final String ITEM_COMMAND_ACTION = "core.ItemCommandAction"; + private static final String ITEM_NAME = "itemName"; + private static final String COMMAND = "command"; + + private ItemRegistry itemRegistry; + private EventPublisher eventPublisher; + + /** + * constructs a new ItemCommandActionHandler + * + * @param module + * @param moduleTypes + */ + public ItemCommandActionHandler(Action module) { + super(module); + } + + /** + * setter for itemRegistry, used by DS + * + * @param itemRegistry + */ + public void setItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + } + + /** + * unsetter for itemRegistry, used by DS + * + * @param itemRegistry + */ + public void unsetItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = null; + } + + /** + * setter for eventPublisher used by DS + * + * @param eventPublisher + */ + public void setEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + /** + * unsetter for eventPublisher used by DS + * + * @param eventPublisher + */ + public void unsetEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = null; + } + + @Override + public void dispose() { + this.eventPublisher = null; + this.itemRegistry = null; + } + + @Override + public Map execute(Map inputs) { + String itemName = (String) module.getConfiguration().get(ITEM_NAME); + String command = (String) module.getConfiguration().get(COMMAND); + + if (itemName != null && eventPublisher != null && itemRegistry != null) { + try { + Item item = itemRegistry.getItem(itemName); + + Command commandObj = null; + Object cmd = inputs.get(COMMAND); + + if (cmd instanceof Command) { + if (item.getAcceptedCommandTypes().contains(cmd.getClass())) { + commandObj = (Command) cmd; + } + } else { + commandObj = TypeParser.parseCommand(item.getAcceptedCommandTypes(), command); + } + if (commandObj != null) { + ItemCommandEvent itemCommandEvent = ItemEventFactory.createCommandEvent(itemName, commandObj); + logger.debug("Executing ItemCommandAction on Item {} with Command {}", + itemCommandEvent.getItemName(), itemCommandEvent.getItemCommand()); + eventPublisher.post(itemCommandEvent); + } else { + logger.warn("Command '{}' is not valid for item '{}'.", command, itemName); + } + } catch (ItemNotFoundException e) { + logger.error("Item with name {} not found in ItemRegistry.", itemName); + } + } else { + logger.error( + "Command was not posted because either the configuration was not correct or a service was missing: ItemName: {}, Command: {}, eventPublisher: {}, ItemRegistry: {}", + itemName, command, eventPublisher, itemRegistry); + } + return null; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java new file mode 100644 index 00000000000..fa66b9384e0 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemCommandTriggerHandler.java @@ -0,0 +1,120 @@ +/** + * 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.internal.module.handler; + +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventFilter; +import org.eclipse.smarthome.core.events.EventSubscriber; +import org.eclipse.smarthome.core.items.events.ItemCommandEvent; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for Triggers which trigger the rule + * if an item receives a command. The eventType and command value can be set with the + * configuration. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +public class ItemCommandTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber, EventFilter { + + private final Logger logger = LoggerFactory.getLogger(ItemCommandTriggerHandler.class); + + private final String itemName; + private final String command; + private final String topic; + + private final Set types; + private final BundleContext bundleContext; + + public static final String MODULE_TYPE_ID = "core.ItemCommandTrigger"; + + private static final String CFG_ITEMNAME = "itemName"; + private static final String CFG_COMMAND = "command"; + + @SuppressWarnings("rawtypes") + private ServiceRegistration eventSubscriberRegistration; + + public ItemCommandTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module); + this.itemName = (String) module.getConfiguration().get(CFG_ITEMNAME); + this.command = (String) module.getConfiguration().get(CFG_COMMAND); + this.types = Collections.singleton(ItemCommandEvent.TYPE); + this.bundleContext = bundleContext; + Dictionary properties = new Hashtable(); + this.topic = "smarthome/items/" + itemName + "/command"; + properties.put("event.topics", topic); + eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, + properties); + } + + @Override + public Set getSubscribedEventTypes() { + return types; + } + + @Override + public EventFilter getEventFilter() { + return this; + } + + @Override + public void receive(Event event) { + if (callback != null) { + logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), + event.getTopic(), event.getType(), event.getPayload()); + Map values = new HashMap<>(); + if (event instanceof ItemCommandEvent) { + Command command = ((ItemCommandEvent) event).getItemCommand(); + if (this.command == null || this.command.equals(command.toFullString())) { + values.put("command", command); + values.put("event", event); + ((TriggerHandlerCallback) callback).triggered(this.module, values); + } + } + } + } + + /** + * do the cleanup: unregistering eventSubscriber... + */ + @Override + public void dispose() { + super.dispose(); + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + eventSubscriberRegistration = null; + } + } + + @Override + public boolean apply(Event event) { + logger.trace("->FILTER: {}:{}", event.getTopic(), itemName); + return event.getTopic().equals(topic); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java new file mode 100644 index 00000000000..4dc64d36bad --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java @@ -0,0 +1,137 @@ +/** + * 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.internal.module.handler; + +import java.util.Map; + +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.library.types.DecimalType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.TypeParser; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ConditionHandler implementation to check item state + * + * @author Benedikt Niehues - Initial contribution and API + * @author Kai Kreuzer - refactored and simplified customized module handling + * + */ +public class ItemStateConditionHandler extends BaseModuleHandler implements ConditionHandler { + + private final Logger logger = LoggerFactory.getLogger(ItemStateConditionHandler.class); + + public static final String ITEM_STATE_CONDITION = "core.ItemStateCondition"; + + private ItemRegistry itemRegistry; + + /** + * Constants for Config-Parameters corresponding to Definition in + * ItemModuleTypeDefinition.json + */ + private static final String ITEM_NAME = "itemName"; + private static final String OPERATOR = "operator"; + private static final String STATE = "state"; + + public ItemStateConditionHandler(Condition condition) { + super(condition); + } + + /** + * setter for itemRegistry, used by DS + * + * @param itemRegistry + */ + public void setItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + } + + /** + * unsetter for itemRegistry used by DS + * + * @param itemRegistry + */ + public void unsetItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = null; + } + + @Override + public void dispose() { + itemRegistry = null; + } + + @Override + public boolean isSatisfied(Map inputs) { + String itemName = (String) module.getConfiguration().get(ITEM_NAME); + String state = (String) module.getConfiguration().get(STATE); + String operator = (String) module.getConfiguration().get(OPERATOR); + if (operator == null || state == null || itemName == null) { + logger.error("Module is not well configured: itemName={} operator={} state = {}", itemName, operator, + state); + return false; + } + if (itemRegistry == null) { + logger.error("The ItemRegistry is not available to evaluate the condition."); + return false; + } + try { + Item item = itemRegistry.getItem(itemName); + State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state); + State itemState = item.getState(); + logger.debug("ItemStateCondition '{}'checking if {} (State={}) {} {}", module.getId(), itemName, itemState, + operator, compareState); + switch (operator) { + case "=": + logger.debug("ConditionSatisfied --> {}", itemState.equals(compareState)); + return itemState.equals(compareState); + case "!=": + return !itemState.equals(compareState); + case "<": + if (itemState instanceof DecimalType && compareState instanceof DecimalType) { + return ((DecimalType) itemState).compareTo((DecimalType) compareState) < 0; + } + break; + case "<=": + case "=<": + if (itemState instanceof DecimalType && compareState instanceof DecimalType) { + return ((DecimalType) itemState).compareTo((DecimalType) compareState) <= 0; + } + break; + case ">": + if (itemState instanceof DecimalType && compareState instanceof DecimalType) { + return ((DecimalType) itemState).compareTo((DecimalType) compareState) > 0; + } + break; + case ">=": + case "=>": + if (itemState instanceof DecimalType && compareState instanceof DecimalType) { + return ((DecimalType) itemState).compareTo((DecimalType) compareState) >= 0; + } + break; + default: + break; + } + } catch (ItemNotFoundException e) { + logger.error("Item with Name {} not found in itemRegistry", itemName); + return false; + } + return false; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java new file mode 100644 index 00000000000..2595a7394d1 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateTriggerHandler.java @@ -0,0 +1,150 @@ +/** + * 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.internal.module.handler; + +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.events.Event; +import org.eclipse.smarthome.core.events.EventFilter; +import org.eclipse.smarthome.core.events.EventSubscriber; +import org.eclipse.smarthome.core.items.events.GroupItemStateChangedEvent; +import org.eclipse.smarthome.core.items.events.ItemStateChangedEvent; +import org.eclipse.smarthome.core.items.events.ItemStateEvent; +import org.eclipse.smarthome.core.types.State; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for Triggers which trigger the rule + * if an item state event occurs. The eventType and state value can be set with the + * configuration. + * + * @author Kai Kreuzer - Initial contribution and API + * @author Simon Merschjohann + * + */ +public class ItemStateTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber, EventFilter { + private final Logger logger = LoggerFactory.getLogger(ItemStateTriggerHandler.class); + + private final String itemName; + private final String state; + private final String previousState; + private Set types; + private final BundleContext bundleContext; + + public static final String UPDATE_MODULE_TYPE_ID = "core.ItemStateUpdateTrigger"; + public static final String CHANGE_MODULE_TYPE_ID = "core.ItemStateChangeTrigger"; + + private static final String CFG_ITEMNAME = "itemName"; + private static final String CFG_STATE = "state"; + private static final String CFG_PREVIOUS_STATE = "previousState"; + + @SuppressWarnings("rawtypes") + private ServiceRegistration eventSubscriberRegistration; + + public ItemStateTriggerHandler(Trigger module, BundleContext bundleContext) { + super(module); + this.itemName = (String) module.getConfiguration().get(CFG_ITEMNAME); + this.state = (String) module.getConfiguration().get(CFG_STATE); + this.previousState = (String) module.getConfiguration().get(CFG_PREVIOUS_STATE); + if (UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { + this.types = Collections.singleton(ItemStateEvent.TYPE); + } else { + HashSet set = new HashSet<>(); + set.add(ItemStateChangedEvent.TYPE); + set.add(GroupItemStateChangedEvent.TYPE); + this.types = Collections.unmodifiableSet(set); + } + this.bundleContext = bundleContext; + Dictionary properties = new Hashtable(); + properties.put("event.topics", "smarthome/items/" + itemName + "/*"); + eventSubscriberRegistration = this.bundleContext.registerService(EventSubscriber.class.getName(), this, + properties); + } + + @Override + public Set getSubscribedEventTypes() { + return types; + } + + @Override + public EventFilter getEventFilter() { + return this; + } + + @Override + public void receive(Event event) { + if (callback != null) { + logger.trace("Received Event: Source: {} Topic: {} Type: {} Payload: {}", event.getSource(), + event.getTopic(), event.getType(), event.getPayload()); + Map values = new HashMap<>(); + if (event instanceof ItemStateEvent && UPDATE_MODULE_TYPE_ID.equals(module.getTypeUID())) { + State state = ((ItemStateEvent) event).getItemState(); + if ((this.state == null || this.state.equals(state.toFullString()))) { + values.put("state", state); + } + } else if (event instanceof ItemStateChangedEvent && CHANGE_MODULE_TYPE_ID.equals(module.getTypeUID())) { + State state = ((ItemStateChangedEvent) event).getItemState(); + State oldState = ((ItemStateChangedEvent) event).getOldItemState(); + + if (stateMatches(this.state, state) && stateMatches(this.previousState, oldState)) { + values.put("oldState", oldState); + values.put("newState", state); + } + } + if (!values.isEmpty()) { + values.put("event", event); + ((TriggerHandlerCallback) callback).triggered(this.module, values); + } + } + } + + private boolean stateMatches(String requiredState, State state) { + if (requiredState == null) { + return true; + } + + String reqState = requiredState.trim(); + return reqState.isEmpty() || reqState.equals(state.toFullString()); + } + + /** + * do the cleanup: unregistering eventSubscriber... + */ + @Override + public void dispose() { + super.dispose(); + if (eventSubscriberRegistration != null) { + eventSubscriberRegistration.unregister(); + eventSubscriberRegistration = null; + } + } + + @Override + public boolean apply(Event event) { + logger.trace("->FILTER: {}:{}", event.getTopic(), itemName); + return event.getTopic().contains("smarthome/items/" + itemName + "/"); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RuleEnablementActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RuleEnablementActionHandler.java new file mode 100644 index 00000000000..8b4519eaea3 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RuleEnablementActionHandler.java @@ -0,0 +1,106 @@ +/** + * 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.internal.module.handler; + +import java.util.List; +import java.util.Map; + +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.RuleRegistry; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is a handler for RuleEnablementAction module type. + * It enables or disables the rules which's UIDs are passed by the 'ruleUIDs' property. + * !!! If a rule's status is NOT_INITIALIZED that rule can't be enabled. !!! + * + *
    + *Example:
    + *
    + *"id": "RuleAction",
    + *"type": "core.RuleEnablementAction",
    + *"configuration": {
    + *     "enable": true,
    + *     "ruleUIDs": ["UID1", "UID2", "UID3"]
    + * }
    + * 
    + * + * @author Plamen Peev - Initial contribution and API + * @author Kai Kreuzer - use rule engine instead of registry + * + */ +public class RuleEnablementActionHandler extends BaseModuleHandler implements ActionHandler { + + /** + * This filed contains the type of this handler so it can be recognized from the factory. + */ + public static final String UID = "core.RuleEnablementAction"; + + /** + * This field is a key to the 'enable' property of the {@link Action}. + */ + private static final String ENABLE_KEY = "enable"; + + /** + * This field is a key to the 'rulesUIDs' property of the {@link Action}. + */ + private static final String RULE_UIDS_KEY = "ruleUIDs"; + + /** + * This logger is used to log warning message if at some point {@link RuleRegistry} service becomes unavailable. + */ + private final Logger logger = LoggerFactory.getLogger(RuleEnablementActionHandler.class); + + /** + * This field stores the UIDs of the rules to which the action will be applied. + */ + private final List UIDs; + + /** + * This field stores the value for the setEnabled() method of {@link RuleRegistry}. + */ + private final boolean enable; + + @SuppressWarnings("unchecked") + public RuleEnablementActionHandler(final Action module) { + super(module); + final Configuration config = module.getConfiguration(); + + final Boolean enable = (Boolean) config.get(ENABLE_KEY); + if (enable == null) { + throw new IllegalArgumentException("'enable' property can not be null."); + } + this.enable = enable.booleanValue(); + + UIDs = (List) config.get(RULE_UIDS_KEY); + if (UIDs == null) { + throw new IllegalArgumentException("'ruleUIDs' property can not be null."); + } + } + + @Override + public Map execute(Map context) { + for (String uid : UIDs) { + if (callback != null) { + callback.setEnabled(uid, enable); + } else { + logger.warn("Action is not applied to {} because rule engine is not available.", uid); + } + } + return null; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java new file mode 100644 index 00000000000..0e1ce13fe31 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/RunRuleActionHandler.java @@ -0,0 +1,103 @@ +/** + * 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.internal.module.handler; + +import java.util.List; +import java.util.Map; + +import org.eclipse.smarthome.config.core.Configuration; +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 class is a handler for RunRuleAction module type. It runs the rules + * which's UIDs are passed by the 'ruleUIDs' property. If a rule's status is not + * IDLE that rule can not run! + * + *
    + *Example:
    + *
    + *"id": "RuleAction",
    + *"type": "core.RunRuleAction",
    + *"configuration": {
    + *     "ruleUIDs": ["UID1", "UID2", "UID3"]
    + * }
    + * 
    + * + * @author Benedikt Niehues - Initial contribution + * @author Kai Kreuzer - use rule engine instead of registry + * + */ +public class RunRuleActionHandler extends BaseModuleHandler implements ActionHandler { + + /** + * The UID for this handler for identification in the factory. + */ + public static final String UID = "core.RunRuleAction"; + + /** + * the key for the 'rulesUIDs' property of the {@link Action}. + */ + private static final String RULE_UIDS_KEY = "ruleUIDs"; + private static final String CONSIDER_CONDITIONS_KEY = "considerConditions"; + + /** + * The logger + */ + private final Logger logger = LoggerFactory.getLogger(RunRuleActionHandler.class); + + /** + * the UIDs of the rules to be executed. + */ + private final List ruleUIDs; + + /** + * boolean to express if the conditions should be considered, defaults to + * true; + */ + private boolean considerConditions = true; + + @SuppressWarnings("unchecked") + public RunRuleActionHandler(final Action module) { + super(module); + final Configuration config = module.getConfiguration(); + if (config.getProperties().isEmpty()) { + throw new IllegalArgumentException("'Configuration' can not be empty."); + } + + ruleUIDs = (List) config.get(RULE_UIDS_KEY); + if (ruleUIDs == null) { + throw new IllegalArgumentException("'ruleUIDs' property must not be null."); + } + if (config.get(CONSIDER_CONDITIONS_KEY) != null && config.get(CONSIDER_CONDITIONS_KEY) instanceof Boolean) { + this.considerConditions = ((Boolean) config.get(CONSIDER_CONDITIONS_KEY)).booleanValue(); + } + } + + @Override + public Map execute(Map context) { + // execute each rule after the other; at the moment synchronously + for (String uid : ruleUIDs) { + if (callback != null) { + callback.runNow(uid, considerConditions, context); + } else { + logger.warn("Action is not applied to {} because rule engine is not available.", uid); + } + } + // no outputs from this module + return null; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayConditionHandler.java new file mode 100644 index 00000000000..0d1023e33d4 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayConditionHandler.java @@ -0,0 +1,102 @@ +/** + * 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.internal.module.handler; + +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import org.eclipse.smarthome.config.core.Configuration; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.automation.handler.ConditionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ConditionHandler implementation for time based conditions. + * + * @author Dominik Schlierf - initial contribution + * + */ +public class TimeOfDayConditionHandler extends BaseModuleHandler implements ConditionHandler { + + private final Logger logger = LoggerFactory.getLogger(TimeOfDayConditionHandler.class); + + public static final String MODULE_TYPE_ID = "core.TimeOfDayCondition"; + + /** + * Constants for Config-Parameters corresponding to Definition in + * TimeOfDayConditionHandler.json + */ + private static final String START_TIME = "startTime"; + private static final String END_TIME = "endTime"; + /** + * The start time of the user configured time span. + */ + private final LocalTime startTime; + /** + * The end time of the user configured time span. + */ + private final LocalTime endTime; + + public TimeOfDayConditionHandler(Condition condition) { + super(condition); + Configuration configuration = module.getConfiguration(); + String startTimeConfig = (String) configuration.get(START_TIME); + String endTimeConfig = (String) configuration.get(END_TIME); + startTime = startTimeConfig == null ? null : LocalTime.parse(startTimeConfig).truncatedTo(ChronoUnit.MINUTES); + endTime = endTimeConfig == null ? null : LocalTime.parse(endTimeConfig).truncatedTo(ChronoUnit.MINUTES); + } + + @Override + public boolean isSatisfied(Map inputs) { + + if (startTime == null || endTime == null) { + logger.warn("Time condition with id {} is not well configured: startTime={} endTime = {}", module.getId(), + startTime, endTime); + return false; + } + + LocalTime currentTime = LocalTime.now().truncatedTo(ChronoUnit.MINUTES); + + // If the current time equals the start time, the condition is always true. + if (currentTime.equals(startTime)) { + logger.debug("Time condition with id {} evaluated, that the current time {} equals the start time: {}", + module.getId(), currentTime, startTime); + return true; + } + // If the start time is before the end time, the condition will evaluate as true, + // if the current time is between the start time and the end time. + if (startTime.isBefore(endTime)) { + if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) { + logger.debug("Time condition with id {} evaluated, that {} is between {} and {}.", module.getId(), + currentTime, startTime, endTime); + return true; + } + } + // If the start time is set after the end time, the time values wrap around the midnight mark. + // So if the start time is 19:00 and the end time is 07:00, the condition will be true from + // 19:00 to 23:59 and 00:00 to 07:00. + else if (currentTime.isAfter(LocalTime.MIDNIGHT) && currentTime.isBefore(endTime) + || currentTime.isAfter(startTime) && currentTime.isBefore(LocalTime.MAX)) { + logger.debug("Time condition with id {} evaluated, that {} is between {} and {}, or between {} and {}.", + module.getId(), currentTime, LocalTime.MIDNIGHT, endTime, startTime, + LocalTime.MAX.truncatedTo(ChronoUnit.MINUTES)); + return true; + } + // If none of these conditions apply false is returned. + return false; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.java new file mode 100644 index 00000000000..c62765aed2e --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimeOfDayTriggerHandler.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.internal.module.handler; + +import java.text.MessageFormat; + +import org.eclipse.smarthome.core.scheduler.CronScheduler; +import org.eclipse.smarthome.core.scheduler.SchedulerRunnable; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.eclipse.smarthome.core.scheduler.ScheduledCompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an ModuleHandler implementation for Triggers which trigger the rule + * at a specific time (format 'hh:mm'). + * + * @author Kai Kreuzer - Initial Contribution + * + */ +public class TimeOfDayTriggerHandler extends BaseTriggerModuleHandler implements SchedulerRunnable { + + private final Logger logger = LoggerFactory.getLogger(TimeOfDayTriggerHandler.class); + + public static final String MODULE_TYPE_ID = "timer.TimeOfDayTrigger"; + public static final String MODULE_CONTEXT_NAME = "MODULE"; + + private static final String CFG_TIME = "time"; + + private final CronScheduler scheduler; + private final String expression; + private ScheduledCompletableFuture schedule; + + public TimeOfDayTriggerHandler(Trigger module, CronScheduler scheduler) { + super(module); + this.scheduler = scheduler; + String time = module.getConfiguration().get(CFG_TIME).toString(); + try { + String[] parts = time.split(":"); + expression = MessageFormat.format("* {1} {0} * * *", Integer.parseInt(parts[0]), + Integer.parseInt(parts[1])); + } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) { + throw new IllegalArgumentException("'time' parameter '" + time + "' is not in valid format 'hh:mm'.", e); + } + } + + @Override + public synchronized void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + scheduleJob(); + } + + private void scheduleJob() { + schedule = scheduler.schedule(this, expression); + logger.debug("Scheduled job for trigger '{}' at '{}' each day.", module.getId(), + module.getConfiguration().get(CFG_TIME)); + } + + @Override + public void run() { + ((TriggerHandlerCallback) callback).triggered(module, null); + } + + @Override + public synchronized void dispose() { + super.dispose(); + if (schedule != null) { + schedule.cancel(true); + logger.debug("cancelled job for trigger '{}'.", module.getId()); + } + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java new file mode 100644 index 00000000000..f6034a75f35 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/TimerModuleHandlerFactory.java @@ -0,0 +1,88 @@ +/** + * 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.internal.module.handler; + +import java.util.Arrays; +import java.util.Collection; + +import org.eclipse.smarthome.core.scheduler.CronScheduler; +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.handler.ModuleHandlerFactory; +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; + +/** + * This HandlerFactory creates TimerTriggerHandlers to control items within the + * RuleManager. + * + * @author Christoph Knauf - initial contribution + * @author Kai Kreuzer - added new module types + * + */ +@Component(immediate = true, service = ModuleHandlerFactory.class) +public class TimerModuleHandlerFactory extends BaseModuleHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(TimerModuleHandlerFactory.class); + + public static final String THREADPOOLNAME = "ruletimer"; + private static final Collection TYPES = Arrays + .asList(new String[] { GenericCronTriggerHandler.MODULE_TYPE_ID, TimeOfDayTriggerHandler.MODULE_TYPE_ID, + TimeOfDayConditionHandler.MODULE_TYPE_ID, DayOfWeekConditionHandler.MODULE_TYPE_ID }); + + private CronScheduler scheduler; + + @Override + @Deactivate + public void deactivate() { + super.deactivate(); + } + + @Reference + protected void setCronScheduler(CronScheduler scheduler) { + this.scheduler = scheduler; + } + + protected void unsetCronScheduler(CronScheduler scheduler) { + this.scheduler = null; + } + + @Override + public Collection getTypes() { + return TYPES; + } + + @Override + protected ModuleHandler internalCreate(Module module, String ruleUID) { + logger.trace("create {} -> {}", module.getId(), module.getTypeUID()); + String moduleTypeUID = module.getTypeUID(); + if (GenericCronTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) { + return new GenericCronTriggerHandler((Trigger) module, scheduler); + } else if (TimeOfDayTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) { + return new TimeOfDayTriggerHandler((Trigger) module, scheduler); + } else if (TimeOfDayConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) { + return new TimeOfDayConditionHandler((Condition) module); + } else if (DayOfWeekConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) { + return new DayOfWeekConditionHandler((Condition) module); + } else { + logger.error("The module handler type '{}' is not supported.", moduleTypeUID); + } + return null; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java new file mode 100644 index 00000000000..387cb297436 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java @@ -0,0 +1,230 @@ +/** + * 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.internal.module.provider; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.smarthome.config.core.ConfigConstants; +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.AnnotatedActions; +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.internal.module.handler.AnnotationActionHandler; +import org.openhab.core.automation.module.provider.AnnotationActionModuleTypeHelper; +import org.openhab.core.automation.module.provider.ModuleInformation; +import org.openhab.core.automation.module.provider.i18n.ModuleTypeI18nService; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +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 provider parses annotated {@link AnnotatedActions}s and creates action module types, as well as their handlers + * from them + * + * @author Stefan Triller - initial contribution + * + */ +@Component(service = { ModuleTypeProvider.class, ModuleHandlerFactory.class }) +public class AnnotatedActionModuleTypeProvider extends BaseModuleHandlerFactory implements ModuleTypeProvider { + + private final Collection> changeListeners = ConcurrentHashMap.newKeySet(); + private Map> moduleInformation = new ConcurrentHashMap<>(); + private final AnnotationActionModuleTypeHelper helper = new AnnotationActionModuleTypeHelper(); + + private ModuleTypeI18nService moduleTypeI18nService; + + @Override + @Deactivate + protected void deactivate() { + this.moduleInformation = null; + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + changeListeners.add(listener); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + changeListeners.remove(listener); + } + + @Override + public Collection getAll() { + Collection moduleTypes = new ArrayList<>(); + for (String moduleUID : moduleInformation.keySet()) { + ModuleType mt = helper.buildModuleType(moduleUID, this.moduleInformation); + if (mt != null) { + moduleTypes.add(mt); + } + } + return moduleTypes; + } + + @SuppressWarnings("unchecked") + @Override + public T getModuleType(String UID, Locale locale) { + return (T) localizeModuleType(UID, locale); + } + + @SuppressWarnings("unchecked") + @Override + public Collection getModuleTypes(Locale locale) { + List result = new ArrayList<>(); + for (Entry> entry : moduleInformation.entrySet()) { + ModuleType localizedModuleType = localizeModuleType(entry.getKey(), locale); + if (localizedModuleType != null) { + result.add((T) localizedModuleType); + } + } + return result; + } + + private ModuleType localizeModuleType(String uid, Locale locale) { + Set mis = moduleInformation.get(uid); + if (mis != null && !mis.isEmpty()) { + ModuleInformation mi = mis.iterator().next(); + + Bundle bundle = FrameworkUtil.getBundle(mi.getActionProvider().getClass()); + ModuleType mt = helper.buildModuleType(uid, moduleInformation); + + ModuleType localizedModuleType = moduleTypeI18nService.getModuleTypePerLocale(mt, locale, bundle); + return localizedModuleType; + } + return null; + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void addActionProvider(AnnotatedActions actionProvider, Map properties) { + Collection moduleInformations = helper.parseAnnotations(actionProvider); + + String configName = getConfigNameFromService(properties); + + for (ModuleInformation mi : moduleInformations) { + mi.setConfigName(configName); + + ModuleType oldType = null; + if (this.moduleInformation.containsKey(mi.getUID())) { + oldType = helper.buildModuleType(mi.getUID(), this.moduleInformation); + Set availableModuleConfigs = this.moduleInformation.get(mi.getUID()); + availableModuleConfigs.add(mi); + } else { + Set configs = ConcurrentHashMap.newKeySet(); + configs.add(mi); + this.moduleInformation.put(mi.getUID(), configs); + } + + ModuleType mt = helper.buildModuleType(mi.getUID(), this.moduleInformation); + if (mt != null) { + for (ProviderChangeListener l : changeListeners) { + if (oldType != null) { + l.updated(this, oldType, mt); + } else { + l.added(this, mt); + } + } + } + } + } + + public void removeActionProvider(AnnotatedActions actionProvider, Map properties) { + Collection moduleInformations = helper.parseAnnotations(actionProvider); + + String configName = getConfigNameFromService(properties); + + for (ModuleInformation mi : moduleInformations) { + mi.setConfigName(configName); + ModuleType oldType = null; + + Set availableModuleConfigs = this.moduleInformation.get(mi.getUID()); + if (availableModuleConfigs != null) { + if (availableModuleConfigs.size() > 1) { + oldType = helper.buildModuleType(mi.getUID(), this.moduleInformation); + availableModuleConfigs.remove(mi); + } else { + this.moduleInformation.remove(mi.getUID()); + } + + ModuleType mt = helper.buildModuleType(mi.getUID(), this.moduleInformation); + for (ProviderChangeListener l : changeListeners) { + if (oldType != null) { + l.updated(this, oldType, mt); + } else { + l.removed(this, mt); + } + } + } + } + } + + private String getConfigNameFromService(Map properties) { + Object o = properties.get(ConfigConstants.SERVICE_CONTEXT); + String configName = null; + if (o instanceof String) { + configName = (String) o; + } + return configName; + } + + // HandlerFactory: + + @Override + public Collection getTypes() { + return moduleInformation.keySet(); + } + + @Override + protected ModuleHandler internalCreate(Module module, String ruleUID) { + if (module instanceof Action) { + Action actionModule = (Action) module; + + if (moduleInformation.containsKey(actionModule.getTypeUID())) { + ModuleInformation finalMI = helper.getModuleInformationForIdentifier(actionModule, moduleInformation, + false); + + if (finalMI != null) { + ActionType moduleType = helper.buildModuleType(module.getTypeUID(), this.moduleInformation); + return new AnnotationActionHandler(actionModule, moduleType, finalMI.getMethod(), + finalMI.getActionProvider()); + } + } + } + return null; + } + + @Reference + protected void setModuleTypeI18nService(ModuleTypeI18nService moduleTypeI18nService) { + this.moduleTypeI18nService = moduleTypeI18nService; + } + + protected void unsetModuleTypeI18nService(ModuleTypeI18nService moduleTypeI18nService) { + this.moduleTypeI18nService = null; + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java new file mode 100644 index 00000000000..79aaebf3a22 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.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.openhab.core.automation.internal.parser.gson; + +import java.io.OutputStreamWriter; +import java.util.Set; + +import org.eclipse.smarthome.config.core.Configuration; +import org.eclipse.smarthome.config.core.ConfigurationDeserializer; +import org.eclipse.smarthome.config.core.ConfigurationSerializer; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.type.CompositeActionType; +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.CompositeTriggerType; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Abstract class that can be used by the parsers for the different entity types. + * + * @author Kai Kreuzer - Initial contribution + * @author Ana Dimova - add Instance Creators + * + * @param the type of the entities to parse + */ +public abstract class AbstractGSONParser implements Parser { + + // A Gson instance to use by the parsers + protected static Gson gson = new GsonBuilder() // + .registerTypeAdapter(CompositeActionType.class, new ActionInstanceCreator()) // + .registerTypeAdapter(CompositeConditionType.class, new ConditionInstanceCreator()) // + .registerTypeAdapter(CompositeTriggerType.class, new TriggerInstanceCreator()) // + .registerTypeAdapter(Configuration.class, new ConfigurationDeserializer()) // + .registerTypeAdapter(Configuration.class, new ConfigurationSerializer()) // + .create(); + + @Override + public void serialize(Set dataObjects, OutputStreamWriter writer) throws Exception { + gson.toJson(dataObjects, writer); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ActionInstanceCreator.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ActionInstanceCreator.java new file mode 100644 index 00000000000..62ee2ea37e4 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ActionInstanceCreator.java @@ -0,0 +1,34 @@ +/** + * 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.internal.parser.gson; + +import java.lang.reflect.Type; + +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.CompositeActionType; + +import com.google.gson.InstanceCreator; + +/** + * This class creates {@link ActionType} instances. + * + * @author Ana Dimova - Initial Contribution + * + */ +public class ActionInstanceCreator implements InstanceCreator { + + @Override + public CompositeActionType createInstance(Type type) { + return new CompositeActionType(null, null, null, null, null); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ConditionInstanceCreator.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ConditionInstanceCreator.java new file mode 100644 index 00000000000..a24f94c4094 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ConditionInstanceCreator.java @@ -0,0 +1,34 @@ +/** + * 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.internal.parser.gson; + +import java.lang.reflect.Type; + +import org.openhab.core.automation.type.CompositeConditionType; +import org.openhab.core.automation.type.ConditionType; + +import com.google.gson.InstanceCreator; + +/** + * This class creates {@link ConditionType} instances. + * + * @author Ana Dimova - Initial Contribution + * + */ +public class ConditionInstanceCreator implements InstanceCreator { + + @Override + public CompositeConditionType createInstance(Type type) { + return new CompositeConditionType(null, null, null, null); + } +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java new file mode 100644 index 00000000000..7a0e913b976 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeGSONParser.java @@ -0,0 +1,107 @@ +/** + * 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.internal.parser.gson; + +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.openhab.core.automation.dto.ActionTypeDTOMapper; +import org.openhab.core.automation.dto.CompositeActionTypeDTO; +import org.openhab.core.automation.dto.CompositeConditionTypeDTO; +import org.openhab.core.automation.dto.CompositeTriggerTypeDTO; +import org.openhab.core.automation.dto.ConditionTypeDTOMapper; +import org.openhab.core.automation.dto.ModuleTypeDTO; +import org.openhab.core.automation.dto.TriggerTypeDTOMapper; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.TriggerType; +import org.osgi.service.component.annotations.Component; + +/** + * This class can parse and serialize sets of {@link ModuleType}. + * + * @author Kai Kreuzer - Initial Contribution + * + */ +@Component(immediate = true, service = Parser.class, property = { "parser.type=parser.module.type", "format=json" }) +public class ModuleTypeGSONParser extends AbstractGSONParser { + + public ModuleTypeGSONParser() { + } + + @Override + public Set parse(InputStreamReader reader) throws ParsingException { + try { + ModuleTypeParsingContainer mtContainer = gson.fromJson(reader, ModuleTypeParsingContainer.class); + Set result = new HashSet<>(); + addAll(result, mtContainer.triggers); + addAll(result, mtContainer.conditions); + addAll(result, mtContainer.actions); + return result; + } catch (Exception e) { + throw new ParsingException(new ParsingNestedException(ParsingNestedException.MODULE_TYPE, null, e)); + } + } + + @Override + public void serialize(Set dataObjects, OutputStreamWriter writer) throws Exception { + Map> map = createMapByType(dataObjects); + gson.toJson(map, writer); + } + + private void addAll(Set result, List moduleTypes) { + if (moduleTypes != null) { + for (ModuleTypeDTO mt : moduleTypes) { + if (mt instanceof CompositeTriggerTypeDTO) { + result.add(TriggerTypeDTOMapper.map((CompositeTriggerTypeDTO) mt)); + } else if (mt instanceof CompositeConditionTypeDTO) { + result.add(ConditionTypeDTOMapper.map((CompositeConditionTypeDTO) mt)); + } else if (mt instanceof CompositeActionTypeDTO) { + result.add(ActionTypeDTOMapper.map((CompositeActionTypeDTO) mt)); + } + } + } + } + + private Map> createMapByType(Set dataObjects) { + Map> map = new HashMap>(); + + List triggers = new ArrayList(); + List conditions = new ArrayList(); + List actions = new ArrayList(); + for (ModuleType moduleType : dataObjects) { + if (moduleType instanceof TriggerType) { + triggers.add((TriggerType) moduleType); + } else if (moduleType instanceof ConditionType) { + conditions.add((ConditionType) moduleType); + } else if (moduleType instanceof ActionType) { + actions.add((ActionType) moduleType); + } + } + map.put("triggers", triggers); + map.put("conditions", conditions); + map.put("actions", actions); + return map; + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeParsingContainer.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeParsingContainer.java new file mode 100644 index 00000000000..d80fd4fb976 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/ModuleTypeParsingContainer.java @@ -0,0 +1,34 @@ +/** + * 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.internal.parser.gson; + +import java.util.List; + +import org.openhab.core.automation.dto.CompositeActionTypeDTO; +import org.openhab.core.automation.dto.CompositeConditionTypeDTO; +import org.openhab.core.automation.dto.CompositeTriggerTypeDTO; + +/** + * This is a helper data structure for GSON that represents the JSON format used when having different module types + * within a single input stream. + * + * @author Kai Kreuzer - Initial contribution + */ +public class ModuleTypeParsingContainer { + + public List triggers; + + public List conditions; + + public List actions; +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/RuleGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/RuleGSONParser.java new file mode 100644 index 00000000000..735d555950b --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/RuleGSONParser.java @@ -0,0 +1,73 @@ +/** + * 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.internal.parser.gson; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.dto.RuleDTO; +import org.openhab.core.automation.dto.RuleDTOMapper; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.osgi.service.component.annotations.Component; + +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +/** + * This class can parse and serialize sets of {@link Rule}s. + * + * @author Kai Kreuzer - Initial Contribution + * + */ +@Component(immediate = true, service = Parser.class, property = { "parser.type=parser.rule", "format=json" }) +public class RuleGSONParser extends AbstractGSONParser { + + @Override + public Set parse(InputStreamReader reader) throws ParsingException { + JsonReader jr = new JsonReader(reader); + try { + Set rules = new HashSet<>(); + if (jr.hasNext()) { + JsonToken token = jr.peek(); + if (JsonToken.BEGIN_ARRAY.equals(token)) { + List ruleDtos = gson.fromJson(jr, new TypeToken>() { + }.getType()); + for (RuleDTO ruleDto : ruleDtos) { + rules.add(RuleDTOMapper.map(ruleDto)); + } + } else { + RuleDTO ruleDto = gson.fromJson(jr, RuleDTO.class); + rules.add(RuleDTOMapper.map(ruleDto)); + } + return rules; + } + } catch (Exception e) { + throw new ParsingException(new ParsingNestedException(ParsingNestedException.RULE, null, e)); + } finally { + try { + jr.close(); + } catch (IOException e) { + } + } + return Collections.emptySet(); + } + +} diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/TemplateGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/TemplateGSONParser.java new file mode 100644 index 00000000000..33999b73b57 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/TemplateGSONParser.java @@ -0,0 +1,72 @@ +/** + * 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.internal.parser.gson; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.openhab.core.automation.dto.RuleTemplateDTO; +import org.openhab.core.automation.dto.RuleTemplateDTOMapper; +import org.openhab.core.automation.parser.Parser; +import org.openhab.core.automation.parser.ParsingException; +import org.openhab.core.automation.parser.ParsingNestedException; +import org.openhab.core.automation.template.Template; +import org.osgi.service.component.annotations.Component; + +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +/** + * This class can parse and serialize sets of {@link Template}s. + * + * @author Kai Kreuzer - Initial Contribution + * + */ +@Component(immediate = true, service = Parser.class, property = { "parser.type=parser.template", "format=json" }) +public class TemplateGSONParser extends AbstractGSONParser