mavenize openHAB and integrate mavenized ESH repository (#467)

* mavenize openHAB and integrate mavenized ESH repository

Signed-off-by: Markus Rathgeb <maggu2810@gmail.com>
This commit is contained in:
Markus Rathgeb 2019-01-28 12:07:31 +00:00 committed by Kai Kreuzer
parent 45a7ce27cb
commit a37cceab67
2951 changed files with 237830 additions and 2417 deletions

25
.gitignore vendored
View File

@ -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/

View File

@ -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

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.compile-model</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

134
bom/compile-model/pom.xml Normal file
View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.compile-model</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM :: Compile Model</name>
<properties>
<emf.1.version>2.12.0</emf.1.version>
<emf.3.version>2.11.0</emf.3.version>
<emf.mwe.version>1.3.21.201705291010</emf.mwe.version>
<emf.mwe2.version>2.9.1.201705291010</emf.mwe2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.generator</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.osgi</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- EMF -->
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.common</artifactId>
<version>${emf.1.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore</artifactId>
<version>${emf.1.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.change</artifactId>
<version>${emf.3.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.xmi</artifactId>
<version>${emf.1.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.codegen</artifactId>
<version>${emf.3.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.codegen.ecore</artifactId>
<version>${emf.1.version}</version>
<scope>compile</scope>
</dependency>
<!-- EMF MWE2 -->
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.mwe2.launch</artifactId>
<version>${emf.mwe2.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.common.types</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.osgi</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Xbase -->
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase.lib</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
</dependency>
<!-- for the model.*.ide bundles -->
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase.ide</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.ide</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
</dependency>
<!-- TEST -->
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext</artifactId>
<version>${xtext.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

17
bom/compile/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.compile</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

271
bom/compile/pom.xml Normal file
View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.compile</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM :: Compile</name>
<description>The dependencies that are used to compile the core bundles</description>
<properties>
<jetty.version>9.3.25.v20180904</jetty.version>
</properties>
<dependencies>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.core</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.cmpn</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.annotation</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.annotation</artifactId>
<version>2.2.100</version>
<!--<scope>compile</scope> -->
</dependency>
<!-- Annotation API -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.2</version>
<scope>compile</scope>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.commons-httpclient</artifactId>
<version>3.1_7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
<!-- EclipseSource JAX RS -->
<dependency>
<groupId>com.eclipsesource.jaxrs</groupId>
<artifactId>publisher</artifactId>
<version>5.3.1</version>
<scope>compile</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
<scope>compile</scope>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.9.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.2</version>
<scope>compile</scope>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<!-- JmDNS -->
<dependency>
<groupId>org.jmdns</groupId>
<artifactId>jmdns</artifactId>
<version>3.5.5</version>
<scope>compile</scope>
</dependency>
<!-- Joda Time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.2</version>
<scope>compile</scope>
</dependency>
<!-- jUPnP -->
<dependency>
<groupId>org.jupnp</groupId>
<artifactId>org.jupnp</artifactId>
<version>2.5.1</version>
<scope>compile</scope>
</dependency>
<!-- MapDB -->
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>1.0.9</version>
<scope>compile</scope>
</dependency>
<!-- Measurement -->
<dependency>
<groupId>javax.measure</groupId>
<artifactId>unit-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>tec.uom</groupId>
<artifactId>uom-se</artifactId>
<version>1.0.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>tec.uom.lib</groupId>
<artifactId>uom-lib-common</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
<!-- Paho MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
</dependency>
<!-- Quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
<!-- Serial Communication -->
<!-- <dependency> <groupId>org.eclipse.kura</groupId> <artifactId>org.eclipse.soda.dk.comm</artifactId> <version>1.2.201</version>
<scope>compile</scope> </dependency> -->
<dependency>
<groupId>org.vesalainen.comm</groupId>
<artifactId>javaxcomm</artifactId>
<version>1.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.neuronrobotics</groupId>
<artifactId>nrjavaserial</artifactId>
<version>3.14.0</version>
<scope>compile</scope>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.5</version>
<scope>compile</scope>
</dependency>
<!-- WR RS API -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
</dependency>
<!-- XStream -->
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.xstream</artifactId>
<version>1.4.7_1</version>
<scope>compile</scope>
</dependency>
<!-- <dependency> <groupId></groupId> <artifactId></artifactId> <version></version> <scope>compile</scope> </dependency> -->
</dependencies>
</project>

17
bom/openhab-core/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.esh-core</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

531
bom/openhab-core/pom.xml Normal file
View File

@ -0,0 +1,531 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.openhab-core</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM :: openHAB Core</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test.magic</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.boot</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.compat1x</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.karaf</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.thing</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.audio</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.voice</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.binding.xml</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.extension.sample</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.id</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.persistence</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.scheduler</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.thing.xml</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.transform</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.auth.jaas</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.auth.oauth2client</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.net</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.console</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.http</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.mdns</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.serial</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.console.eclipse</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.console.rfc147</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.console.karaf</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.http.auth</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.http.auth.basic</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.monitor</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.auth</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.log</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.mdns</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.optimize</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.sitemap</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.sse</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.voice</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.dbus</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.mqtt</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.serial.javacomm</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.serial.rxtx</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.serial.rxtx.rfc2217</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.upnp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.jetty.certificate</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.mdns</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial.linuxsysfs</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.upnp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.dispatch</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.serial</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.xml</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.media</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script.rulesupport</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.rest</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.sitemap</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.item</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.item.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.item.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.persistence</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.persistence.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.script</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.rule</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.rule.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.script.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.sitemap.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.thing</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.thing.ide</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.lsp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.persistence.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.rule.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.script.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.sitemap.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.thing.runtime</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.ui</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.ui.dashboard</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.ui.icon</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.storage.json</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.storage.mapdb</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

28
bom/pom.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core</groupId>
<artifactId>org.openhab.core.reactor</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM</name>
<modules>
<module>compile</module>
<module>compile-model</module>
<module>runtime</module>
<module>runtime-index</module>
<module>test</module>
<module>test-index</module>
<module>openhab-core</module>
</modules>
</project>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.runtime-index</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

40
bom/runtime-index/pom.xml Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.runtime-index</artifactId>
<name>openHAB Core :: BOM :: Runtime Index</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.runtime</artifactId>
<type>pom</type>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

17
bom/runtime/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.runtime</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

590
bom/runtime/pom.xml Normal file
View File

@ -0,0 +1,590 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.runtime</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM :: Runtime</name>
<properties>
<pax.web.version>7.2.3</pax.web.version>
<jetty.version>9.4.11.v20180605</jetty.version>
</properties>
<dependencies>
<dependency>
<groupId>org.osgi.enroute</groupId>
<artifactId>impl-index</artifactId>
<version>7.0.0</version>
<type>pom</type>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.http.jetty</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.osgi.enroute</groupId>
<artifactId>debug-bundles</artifactId>
<version>7.0.0</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<!-- BEG: logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.log</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>osgi.core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.logback</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<!-- END: logging -->
<!-- Apache Commons -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.commons-httpclient</artifactId>
<version>3.1_7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.orbit.bundles</groupId>
<artifactId>com.google.gson</artifactId>
<version>2.7.0.v20170129-0911</version>
<scope>compile</scope>
</dependency>
<!-- JmDNS -->
<dependency>
<groupId>org.jmdns</groupId>
<artifactId>jmdns</artifactId>
<version>3.5.5</version>
<scope>compile</scope>
</dependency>
<!-- jUPnP -->
<dependency>
<groupId>org.jupnp</groupId>
<artifactId>org.jupnp</artifactId>
<version>2.5.1</version>
<scope>compile</scope>
</dependency>
<!-- MapDB -->
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>1.0.9</version>
<scope>compile</scope>
</dependency>
<!-- Measurement -->
<dependency>
<groupId>javax.measure</groupId>
<artifactId>unit-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>tec.uom</groupId>
<artifactId>uom-se</artifactId>
<version>1.0.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>tec.uom.lib</groupId>
<artifactId>uom-lib-common</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
<!-- XStream -->
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.xstream</artifactId>
<version>1.4.7_1</version>
<scope>compile</scope>
</dependency>
<!-- Pax Web -->
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-api</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-extender-whiteboard</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-jetty</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-jsp</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-runtime</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-spi</artifactId>
<version>${pax.web.version}</version>
<scope>compile</scope>
</dependency>
<!-- Persistence -->
<!-- <dependency> -->
<!-- <groupId>org.hibernate</groupId> -->
<!-- <artifactId>hibernate-osgi</artifactId> -->
<!-- <version>5.2.12.Final</version> -->
<!-- </dependency> -->
<!-- BEG: Needed by Hibernate, but not present as OSGi bundles in Hibernate's dependencies -->
<!-- <dependency> -->
<!-- <groupId>org.apache.servicemix.bundles</groupId> -->
<!-- <artifactId>org.apache.servicemix.bundles.antlr</artifactId> -->
<!-- <version>2.7.7_5</version> -->
<!-- </dependency> -->
<!-- <dependency> -->
<!-- <groupId>org.apache.servicemix.bundles</groupId> -->
<!-- <artifactId>org.apache.servicemix.bundles.dom4j</artifactId> -->
<!-- <version>1.6.1_5</version> -->
<!-- </dependency> -->
<!-- END: Needed by Hibernate, but not present as OSGi bundles in Hibernate's dependencies -->
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-continuation</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-deploy</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jaas</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jndi</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jaspi</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-common</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-client-impl</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-servlet</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-xml</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
<!-- Xbean -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-bundleutils</artifactId>
<version>4.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-finder</artifactId>
<version>4.6</version>
<scope>compile</scope>
</dependency>
<!-- Google Commons / Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>15.0</version>
<scope>compile</scope>
</dependency>
<!-- EMF, Xtext -->
<dependency>
<groupId>de.maggu2810.p2redist</groupId>
<artifactId>org.antlr.runtime</artifactId>
<version>3.2.0.v201101311130</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.common</artifactId>
<version>2.12.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore</artifactId>
<version>2.12.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.change</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.xmi</artifactId>
<version>2.12.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtend</groupId>
<artifactId>org.eclipse.xtend.lib</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtend</groupId>
<artifactId>org.eclipse.xtend.lib.macro</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.common.types</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.ide</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.util</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase.ide</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase.lib</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-api</artifactId>
<version>2.4.0-b34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>aopalliance-repackaged</artifactId>
<version>2.4.0-b34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
<version>2.4.0-b34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-locator</artifactId>
<version>2.4.0-b34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>osgi-resource-locator</artifactId>
<version>1.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-utils</artifactId>
<version>2.4.0-b34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.1.1</version>
<scope>compile</scope>
</dependency>
<!-- Joda Time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.2</version>
<scope>compile</scope>
</dependency>
<!-- ... -->
<dependency>
<groupId>com.eclipsesource.jaxrs</groupId>
<artifactId>publisher</artifactId>
<version>5.3.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

27
bom/test-index/.classpath Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

23
bom/test-index/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.test-index</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

40
bom/test-index/pom.xml Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.test-index</artifactId>
<name>openHAB Core :: BOM :: Test Index</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test</artifactId>
<type>pom</type>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

17
bom/test/.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.bom.test</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

59
bom/test/pom.xml Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.reactor.bom</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.bom.test</artifactId>
<packaging>pom</packaging>
<name>openHAB Core :: BOM :: Test</name>
<properties>
<jetty.version>9.3.25.v20180904</jetty.version>
</properties>
<dependencies>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.enroute.junit.wrapper</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.enroute.hamcrest.wrapper</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.13.0</version>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.codehaus.groovy</groupId> -->
<!-- <artifactId>groovy-all</artifactId> -->
<!-- <version>2.5.5</version> -->
<!-- <type>pom</type> -->
<!-- </dependency> -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.5.5</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.audio</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding/<project>=UTF-8
encoding/ESH-INF=UTF-8
encoding/src=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -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.

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.audio</artifactId>
<name>openHAB Core :: Bundles :: Audio</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.console</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.http</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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);
}
}

View File

@ -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 <a href="http://bit.ly/1TUW93t">WAV Format</a>
* @see <a href="http://bit.ly/1oRMKOt">Supported codecs</a>
* @see <a href="http://bit.ly/1TUWSlk">RIFF container format</a>
*/
public static final String CONTAINER_WAVE = "WAVE";
/**
* OGG container format
*
* @see <a href="http://bit.ly/1oRMWNE">OGG</a>
*/
public static final String CONTAINER_OGG = "OGG";
/**
* PCM Signed
*
* @see <a href="http://wiki.multimedia.cx/?title=PCM#PCM_Types">PCM Types</a>
*/
public static final String CODEC_PCM_SIGNED = "PCM_SIGNED";
/**
* PCM Unsigned
*
* @see <a href="http://wiki.multimedia.cx/?title=PCM#PCM_Types">PCM Types</a>
*/
public static final String CODEC_PCM_UNSIGNED = "PCM_UNSIGNED";
/**
* PCM A-law
*
* @see <a href="http://wiki.multimedia.cx/?title=PCM#PCM_Types">PCM Types</a>
*/
public static final String CODEC_PCM_ALAW = "ALAW";
/**
* PCM u-law
*
* @see <a href="http://wiki.multimedia.cx/?title=PCM#PCM_Types">PCM Types</a>
*/
public static final String CODEC_PCM_ULAW = "ULAW";
/**
* MP3 Codec
*
* @see <a href="http://wiki.multimedia.cx/index.php?title=MP3">MP3 Codec</a>
*/
public static final String CODEC_MP3 = "MP3";
/**
* Vorbis Codec
*
* @see <a href="http://xiph.org/vorbis/doc/">Vorbis</a>
*/
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 <a href="http://bit.ly/1OTydad">Bit Depth</a>
*/
private final Integer bitDepth;
/**
* Bit rate
*
* @see <a href="http://bit.ly/1OTy5rk">Bit Rate</a>
*/
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 <a href="http://bit.ly/1OTydad">Bit Depth</a>
* @return Bit depth
*/
public Integer getBitDepth() {
return bitDepth;
}
/**
* Gets bit rate
*
* @see <a href="http://bit.ly/1OTy5rk">Bit Rate</a>
* @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<AudioFormat> inputs, Set<AudioFormat> 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<AudioFormat> 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 : "") + "]";
}
}

View File

@ -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);
}

View File

@ -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<AudioSource> 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<AudioSink> 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<String> 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<String> getSinkIds(String pattern);
}

View File

@ -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<AudioFormat> getSupportedFormats();
/**
* Gets a set containing all supported audio stream formats
*
* @return A Set containing all supported audio stream formats
*/
public Set<Class<? extends AudioStream>> 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;
}

View File

@ -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<AudioFormat> 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;
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.smarthome.core.audio;
/**
* Thrown when a requested {@link AudioStream} is not supported by an {@link AudioSource} or {@link AudioSink}
* implementation
*
* @author Christoph Weitkamp - Initial contribution and API
*
*/
public class UnsupportedAudioStreamException extends AudioException {
private static final long serialVersionUID = 1L;
/**
* Unsupported {@link AudioStream}
*/
private Class<? extends AudioStream> unsupportedAudioStreamClass;
/**
* Constructs a new exception with the specified detail message, unsupported format, and cause.
*
* @param message The message
* @param unsupportedAudioStreamClass The unsupported audio stream class
* @param cause The cause
*/
public UnsupportedAudioStreamException(String message, Class<? extends AudioStream> unsupportedAudioStreamClass,
Throwable cause) {
super(message, cause);
this.unsupportedAudioStreamClass = unsupportedAudioStreamClass;
}
/**
* Constructs a new exception with the specified detail message and unsupported format.
*
* @param message The message
* @param unsupportedAudioStreamClass The unsupported audio stream class
*/
public UnsupportedAudioStreamException(String message, Class<? extends AudioStream> unsupportedAudioStreamClass) {
this(message, unsupportedAudioStreamClass, null);
}
/**
* Gets the unsupported audio stream class.
*
* @return The unsupported audio stream class
*/
public Class<? extends AudioStream> getUnsupportedAudioStreamClass() {
return unsupportedAudioStreamClass;
}
}

View File

@ -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<String> getUsages() {
return Arrays.asList(new String[] {
buildCommandUsage(SUBCMD_PLAY + " [<sink>] <filename>",
"plays a sound file from the sounds folder through the optionally specified audio sink(s)"),
buildCommandUsage(SUBCMD_PLAY + " <sink> <filename> <volume>",
"plays a sound file from the sounds folder through the specified audio sink(s) with the specified volume"),
buildCommandUsage(SUBCMD_STREAM + " [<sink>] <url>",
"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<AudioSource> 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<AudioSink> 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;
}
}

View File

@ -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<String, AudioSource> audioSources = new ConcurrentHashMap<>();
private final Map<String, AudioSink> audioSinks = new ConcurrentHashMap<>();
/**
* default settings filled through the service configuration
*/
private String defaultSource;
private String defaultSink;
@Activate
protected void activate(Map<String, Object> config) {
modified(config);
}
@Deactivate
protected void deactivate() {
}
@Modified
void modified(Map<String, Object> 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<AudioSource> 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<AudioSink> getAllSinks() {
return new HashSet<>(audioSinks.values());
}
@Override
public Set<String> getSourceIds(String pattern) {
String regex = pattern.replace("?", ".?").replace("*", ".*?");
Set<String> 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<String> getSinkIds(String pattern) {
String regex = pattern.replace("?", ".?").replace("*", ".*?");
Set<String> matchedSinkIds = new HashSet<>();
for (String sinkId : audioSinks.keySet()) {
if (sinkId.matches(regex)) {
matchedSinkIds.add(sinkId);
}
}
return matchedSinkIds;
}
@Override
public Collection<ParameterOption> 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());
}
}

View File

@ -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<String, AudioStream> oneTimeStreams = new ConcurrentHashMap<>();
private final Map<String, FixedLengthAudioStream> multiTimeStreams = new ConcurrentHashMap<>();
private final Map<String, Long> 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<String, FixedLengthAudioStream> getMultiTimeStreams() {
return Collections.unmodifiableMap(multiTimeStreams);
}
private String getRelativeURL(String streamId) {
return SERVLET_NAME + "/" + streamId;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="http://eclipse.org/smarthome/schemas/config-description/v1.0.0"
xsi:schemaLocation="http://eclipse.org/smarthome/schemas/config-description/v1.0.0
http://eclipse.org/smarthome/schemas/config-description-1.0.0.xsd">
<config-description uri="system:audio">
<parameter name="defaultSource" type="text" required="false">
<label>Default Source</label>
<description>The default audio source to use if no other is specified.</description>
</parameter>
<parameter name="defaultSink" type="text" required="false">
<label>Default Sink</label>
<description>The default audio sink to use if no other is specified.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.auth.jaas</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
encoding/src=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -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.

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.auth.jaas</artifactId>
<name>openHAB Core :: Bundles :: JAAS Authentication</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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<Principal> 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<String, Object> properties) {
modified(properties);
}
@Deactivate
protected void deactivate(Map<String, Object> properties) {
}
@Modified
protected void modified(Map<String, Object> properties) {
if (properties == null) {
realmName = null;
return;
}
Object propertyValue = properties.get("realmName");
if (propertyValue != null) {
if (propertyValue instanceof String) {
realmName = (String) propertyValue;
} else {
realmName = propertyValue.toString();
}
} else {
// value could be unset, we should reset it value
realmName = null;
}
}
@Override
public boolean supports(Class<? extends Credentials> type) {
return UsernamePasswordCredentials.class.isAssignableFrom(type);
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.auth.oauth2client</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
encoding/src=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.auth.oauth2client</artifactId>
<name>openHAB Core :: Bundles :: OAuth2Client</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.net</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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
}

View File

@ -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<AccessTokenRefreshListener> 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();
}
}

View File

@ -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 <a href="https://tools.ietf.org/html/rfc6749#section-4.1.1">rfc6749 section-4.1.1</a>
*/
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 <a href="https://tools.ietf.org/html/rfc6749#section-4.3">rfc6749 section-4.3</a>
*
* @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 <a href="https://tools.ietf.org/html/rfc6749#section-6">rfc6749 section-6</a>
*
* @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 <a href="https://tools.ietf.org/html/rfc6749#section-4.1.3">rfc6749 section-4.1.3</a>
*
* @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 <a href="https://tools.ietf.org/html/rfc6749#section-4.4">rfc6749 section-4.4</a>
*
* @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<ContentResponse>) () -> {
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);
}
}
}

View File

@ -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<String, OAuthClientService> 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;
}
}

View File

@ -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);
}

View File

@ -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. <handle>.LastUsed = system-time-milliseconds
* 3. <handle>.AccessTokenResponse = Json of AccessTokenResponse
* 4. <handle>.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<String> allHandles = new HashSet<>(); // must be initialized
private @NonNullByDefault({}) StorageFacade storageFacade;
private final Set<StorageCipher> allAvailableStorageCiphers = new LinkedHashSet<>();
private Optional<StorageCipher> storageCipher = Optional.empty();
private final Logger logger = LoggerFactory.getLogger(OAuthStoreHandlerImpl.class);
@Activate
public void activate(Map<String, Object> 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<String> storage;
private final Lock storageLock = new ReentrantLock(); // for all operations on the storage
private final Gson gson;
public StorageFacade(Storage<String> storage) {
this.storage = storage;
// Add adapters for LocalDateTime
gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class,
(JsonDeserializer<LocalDateTime>) (json, typeOfT, context) -> LocalDateTime
.parse(json.getAsString()))
.registerTypeAdapter(LocalDateTime.class,
(JsonSerializer<LocalDateTime>) (date, type,
jsonSerializationContext) -> new JsonPrimitive(date.toString()))
.setPrettyPrinting().create();
}
public Set<String> getAllHandlesFromIndex() {
Set<String> 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<String> 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);
}
}
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<String, Object> 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;
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.automation.module.media</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
encoding/src=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -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.

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.automation.module.media</artifactId>
<name>openHAB Core :: Bundles :: Automation Media Modules</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.voice</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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<ModuleType> 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<ConfigDescriptionParameter> 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<ConfigDescriptionParameter> 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<ParameterOption> getSoundOptions() {
List<ParameterOption> 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<ParameterOption> getSinkOptions(Locale locale) {
List<ParameterOption> options = new ArrayList<>();
for (AudioSink sink : audioManager.getAllSinks()) {
options.add(new ParameterOption(sink.getId(), sink.getLabel(locale)));
}
return options;
}
@Override
public void addProviderChangeListener(ProviderChangeListener<ModuleType> listener) {
// does nothing because this provider does not change
}
@Override
public Collection<ModuleType> getAll() {
return getModuleTypes(null);
}
@Override
public void removeProviderChangeListener(ProviderChangeListener<ModuleType> 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;
}
}

View File

@ -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<String> 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<String> 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;
}
}

View File

@ -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<String, Object> 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<String> getDefaultPresets() {
return Collections.singleton("media");
}
@Override
public Collection<String> getPresets() {
return Collections.singleton("media");
}
@Override
public Collection<String> getTypes() {
return elements.keySet();
}
@Override
public Object get(String scriptIdentifier, String type) {
return elements.get(type);
}
@Override
public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
return elements;
}
@Override
public void unload(String scriptIdentifier) {
}
}

View File

@ -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<Action> 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<String, Object> execute(Map<String, Object> 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;
}
}

View File

@ -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<Action> 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<String, Object> execute(Map<String, Object> context) {
String text = module.getConfiguration().get(PARAM_TEXT).toString();
String sink = (String) module.getConfiguration().get(PARAM_SINK);
voiceManager.say(text, null, sink);
return null;
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.automation.module.script.rulesupport</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding/<project>=UTF-8
encoding/ESH-INF=UTF-8
encoding/src=UTF-8

View File

@ -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

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

Some files were not shown because too many files have changed in this diff Show More